1. 程式人生 > >Python爬蟲三:抓取鏈家已成交二手房資訊(58W資料)

Python爬蟲三:抓取鏈家已成交二手房資訊(58W資料)

環境:Windows7+python3.6+Pycharm2017

目標:抓取鏈家北京地區已成交二手房資訊(無需登入),如下圖,戶型、朝向、成交時間價格等,儲存到csv。最後一共抓取約58W資料,程式執行8h。

--------全部文章: 京東爬蟲 、鏈家爬蟲美團爬蟲微信公眾號爬蟲字型反爬---------

一、開啟北京二手房網頁https://bj.lianjia.com/ershoufang/,預設顯示的是在售二手房資訊,一共45634套,但是隻顯示了100頁,每頁30條,這3000條資訊是沒有任何反爬的,可以直接抓取,如果要抓取全部45634條,應該要按小區來。本文主要討論已成交二手房資訊,資料量更大,難度也要高一點。

 

二、點選頁面右上角成交,切換到已成交二手房資訊,顯示一共有73W條資料,但是也只顯示100頁,每頁30條共3000條資訊。而且還有個問題就是近30天內成交的房源的成交時間、價格資訊是不顯示的。我們可以右鍵檢查進入開發者模式,在網頁的html程式碼中找到房源的詳情頁面的url,然後進入詳情頁面抓取成交時間、價格。

 

三、如何抓取儘可能多的房源資訊

現在問題就是73W已成交二手房資訊,怎麼能儘可能多的抓下來。 辦法就是這些房源通過分類來抓取,比如分不同區域,價格,小區,這樣可以抓到更多的資料。本文選用按小區抓取。點選頁面上方小區,進入如下頁面,再點選返回全部小區列表。顯示一共有11435個小區,雖然下面翻頁只有30頁,但是我們可以通過構造url來翻頁,實測可以翻到100頁,100頁後都是重複的,共3000個小區。每頁的url如下:

 

四、如何抓取每個小區的已成交二手房資訊

點選某個小區如北京新天地,進入小區詳情頁面,下拉找到北京新天地小區成交記錄,點選下面的檢視全部成交記錄,即可得到該小區全部已成交房源資訊。通過左上角房源總數2133套,除以每頁30套,我們可以得到該小區已成交房源一共有多少頁。近30天內成交的進入詳情頁面抓取。觀察頁面的url  https://bj.lianjia.com/chengjiao/c1111027375945/ ,觀察規律就是最後的一串數字是變化的,是每個小區的id。翻頁的規律如下:

    所以我們的思路就是先抓取每個小區的id,然後構造小區成交房源頁面的url,通過房源總數來得知該小區一共有多少頁,翻頁抓取。近30天內成交的需要進入詳情頁面抓取,其他的直接在列表頁面就可以。

 

五、抓取小區id

一共100頁,每頁的url如下,也很簡單,直接每個li標籤中的data-id屬性就是小區的id。注意的是該頁面有對ip訪問次數做限制,單ip連續訪問好像是25頁就會被封,所以需要採用代理ip,這裡每個ip只抓取20頁。還有一點需要注意的就是抓取的id需要做排重,此處用的set。還有就是對於第一次訪問沒有得到資料的頁面需要再次訪問。

 實際一共抓取到2990個id,儲存到本地csv,程式碼如下。

import requests
from lxml import etree
import csv
import time
import json
#單執行緒抓取小區id前100頁資訊
def get_xiaoqu(x,p):
    head = {'Host': 'bj.lianjia.com',
            'Referer': 'https://bj.lianjia.com/chengjiao/',
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'
            }
    n=x*20+1
    l=list(range(n,n+20))
    for i in l:
        url = 'https://bj.lianjia.com/xiaoqu/pg' + str(i)
        try:
            r = requests.get(url, headers=head, proxies=p, timeout=3)
            html = etree.HTML(r.text)
            datas=html.xpath('//li[@class="clear xiaoquListItem"]/@data-id')
            title=html.xpath('//li[@class="clear xiaoquListItem"]/div[@class="info"]/div[@class="title"]/a/text()')
            print('No:' + str(x), 'page:' + str(i), len(s), len(datas), len(title))
            #如果當前頁沒有返回資料,將當前頁數加到列表l末尾,再次抓取
            if len(datas)==0:
                print(url)
                l.append(i)
            else:
                for data in datas:
                    s.add(data)
        # 如果當前頁訪問出現異常,將當前頁數加到列表l末尾,再次抓取
        except Exception as e:
            l.append(i)
            print(e)

    print('      ****No:'+str(x)+' finish')
#本人購買的代理獲取方式,需要根據你們自己的修改。函式功能獲取n個ip,並以列表形式返回,每個元素為字典:{'https':'https://118.120.228.202:4286'}
def get_ip(n):
    url='XXXXXXXXXXXXXXXXXXXXXXXX'
    r=requests.get(url)
    html=json.loads(r.text)
    proxies=[]
    for i in range(n):
        a=html['data'][i]['ip']
        b=html['data'][i]['port']
        val='https://'+str(a)+':'+str(b)
        p={'https':val}
        proxies.append(p)
    return(proxies)

if __name__=='__main__':
    global s
    #將id儲存在set中,達到排重效果
    s = set()
    #該頁面網站會禁ip,所以每個ip只訪問20頁
    for x in range(5):
        now=time.time()
        ls = get_ip(1)
        p=ls[0]
        get_xiaoqu(x,p)
        print(time.time()-now)
    print('******************')
    print('抓取完成')
    #將抓取的id儲存到本地csv
    with open('xiaoqu_id.csv', 'a', newline='', encoding='gb18030')as f:
        write = csv.writer(f)
        for data in s:
            write.writerow([data])
        f.close()







 

六、對每個小區已成交房源資訊進行抓取 

本文沒有開多執行緒(開5個執行緒跑了幾分鐘好像也沒遇到問題),也沒遇到封ip,就沒加代理。邏輯很簡單,parse_xiaoqu(url,pa) 函式用於爬取小區具體一頁的房源資訊,先抓取第一頁資料,獲取房源資訊的同時獲得該小區房源總數,然後確定該小區一共有多少頁。然後就是對每一頁呼叫parse_xiaoqu(url,pa)進行抓取。主要注意點有:

1、對於第一次抓取失敗的頁面,包括timeout這種異常和無異常但是返回0條房源資訊兩種情況,都需要對這些頁面進行第二次的抓取。parse_xiaoqu(url,pa)返回值中有一個1或0就是用以標記本次抓取是否成功。第一次抓取失敗的,第二次抓取成功的資料還挺多的。

2、爬取過程中遇到報錯中斷,可以通過已經抓取的小區數量,修改range(0,2990)函式的第一個引數達到斷點後續抓。

程式碼如下,程式碼應該是把小區id匯入就可以直接執行的。

import requests
from lxml import etree
import csv
import time
import threading
#小區具體一頁房源資訊的抓取,輸入為當前頁面url,當前爬取頁數pa。返回資料為小區房源總數num,該頁抓取的房源資訊home_list,狀態碼1或0(1表示成功)
def parse_xiaoqu(url,pa):
    head = {'Host': 'bj.lianjia.com',
            'Referer': 'https://bj.lianjia.com/chengjiao/',
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'

            }
    r = requests.get(url, headers=head,timeout=5)
    html = etree.HTML(r.text)
    num = html.xpath('//div[@class="content"]//div[@class="total fl"]/span/text()')[0]
    num = int(num)
    datas = html.xpath('//li/div[@class="info"]')
    print('小區房源總數:', num,'第%d頁房源數:'%pa,len(datas))
    print(url)
    if len(datas)==0:
        return(num,[],0)   #伺服器無返回資料,狀態碼返回0
    house_list=[]
    for html1 in datas:
        title = html1.xpath('div[@class="title"]/a/text()')
        info = html1.xpath('div[@class="address"]/div[@class="houseInfo"]/text()')
        floor = html1.xpath('div[@class="flood"]/div[@class="positionInfo"]/text()')
        info[0] = info[0].replace('\xa0','')  #該條資訊中有個html語言的空格符號 需要去掉,不然gbk編碼會報錯,gb18030顯示問號
        date = html1.xpath('div[@class="address"]/div[@class="dealDate"]/text()')
        #30天內成交的進入詳情頁面抓取
        if date[0] == '近30天內成交':
            p_url = html1.xpath('div[@class="title"]/a/@href')
            r = requests.get(p_url[0], headers=head,timeout=5)
            html = etree.HTML(r.text)
            price = html.xpath('//div[@class="overview"]/div[@class="info fr"]/div[@class="price"]/span/i/text()')
            unitprice = html.xpath('//div[@class="overview"]/div[@class="info fr"]/div[@class="price"]/b/text()')
            date = html.xpath('//div[@class="house-title LOGVIEWDATA LOGVIEW"]/div[@class="wrapper"]/span/text()')
            #有的房源資訊沒有價格資訊,顯示暫無價格
            if len(price)==0:
                price.append('暫無價格')
            if len(unitprice)==0:
                unitprice.append('暫無單價')
            date[0] = date[0].replace('鏈家成交', '')
            a = [title[0], info[0], floor[0], date[0], price[0], unitprice[0]]
            house_list.append(a)
            print(title[0], info[0], floor[0], date[0], price[0], unitprice[0])
        else:
            price = html1.xpath('div[@class="address"]/div[@class="totalPrice"]/span/text()')
            unitprice = html1.xpath('div[@class="flood"]/div[@class="unitPrice"]/span/text()')
            if len(price) == 0:
                price = ['暫無價格']
            if len(unitprice) == 0:
                unitprice = ['暫無單價']
            a = [title[0], info[0], floor[0], date[0], price[0], unitprice[0]]
            house_list.append(a)
            print(title[0], info[0], floor[0], date[0], price[0], unitprice[0])
    print('                *********************         ','第%d頁完成!'%pa)
    return (num,house_list,1)

#抓取某小區所有已成交二手房資訊,排重後存入本地csv,輸入為小區id,返回抓取到的該小區的房源總數
def crow_xiaoqu(id):
    url='https://bj.lianjia.com/chengjiao/c%d/'%int(id)
    h_list=[]      #儲存該小區抓取的所有房源資訊
    fail_list=[]   #儲存第一次抓取失敗的頁數,第一遍抓取完成後對這些頁數再次抓取
    try:
        #爬取小區第一頁資訊
        result=parse_xiaoqu(url,1)
    except:
        #如果第一頁資訊第一次爬取失敗,sleep2秒再次爬取
        time.sleep(2)
        result=parse_xiaoqu(url,1)
    #獲取該小區房源總數num
    num = result[0]
    #如果無資料返回,sleep2秒再爬取一次
    if num == 0:
        time.sleep(2)
        result=parse_xiaoqu(url,1)
        num = result[0]
    new_list = result[1]
    pages=1
    for data in new_list:
        if data not in h_list:
            h_list.append(data)
    # 確定當前小區房源頁數pages
    if num > 30:
        if num % 30 == 0:
            pages = num // 30
        else:
            pages = num // 30 + 1
    for pa in range(2,pages+1):
        new_url = 'https://bj.lianjia.com/chengjiao/pg'+str(pa)+'c'+str(id)
        try:
            result=parse_xiaoqu(new_url,pa)
            status=result[2]
            if status==1:
                new_list=result[1]
                #排重後存入h_list
                for data in new_list:
                    if data not in h_list:
                        h_list.append(data)
            else:
                fail_list.append(pa)
        except Exception as e:
            fail_list.append(pa)
            print(e)
    print('   開始抓取第一次失敗頁面')
    for pa in fail_list:
        new_url = 'https://bj.lianjia.com/chengjiao/pg' + str(pa) + 'c' + str(id)
        print(new_url)
        try:
            result = parse_xiaoqu(new_url,pa)
            status = result[2]
            if status == 1:
                new_list = result[1]
                for data in new_list:
                    if data not in h_list:
                        h_list.append(data)
            else:
                pass
        except Exception as e:
            print(e)
    print('    抓取完成,開始儲存資料')
    #一個小區的資料全部抓完後存入csv
    with open('lianjia_123.csv','a',newline='',encoding='gb18030')as f:
        write=csv.writer(f)
        for data in h_list:
            write.writerow(data)
    #返回抓取到的該小區房源總數
    return(len(h_list))
if __name__=='__main__':
    counts=0    #記錄爬取到的房源總數
    now=time.time()
    id_list=[]
    with open('xiaoqu_id.csv','r')as f:
        read=csv.reader(f)
        for id in read:
            id_list.append(id[0])
    m=0
    #可以通過修改range函式的起始引數來達到斷點續抓
    for x in range(0,2990):
        m+=1    #記錄一共抓取了多少個小區
        print('    開始抓取第'+str(m)+'個小區')
        time.sleep(1)
        count=crow_xiaoqu(id_list[x])
        counts=counts+count
        #列印已經抓取的小區數量,房源數量,所花的時間
        print('     已經抓取'+str(m)+'個小區  '+str(counts)+'條房源資訊',time.time()-now)
   

七、總結

    一共抓取了 58W條資料,程式跑了8h,速度一般。主要缺點就是程式不夠頑強,中途中斷了很多次,需要人工的再次啟動,修改range引數才能斷點續抓。還有就是1.1W個小區也就抓了3000個,但是從房源資料來看73W資料抓了58w,大概80%。

水平有限,希望大家指正。

--------全部文章: 京東爬蟲 、鏈家爬蟲美團爬蟲微信公眾號爬蟲字型反爬---------

歡迎關注個人公眾號,更多案例持續更新!