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%。
水平有限,希望大家指正。
--------全部文章: 京東爬蟲 、鏈家爬蟲、美團爬蟲、微信公眾號爬蟲、字型反爬---------
歡迎關注個人公眾號,更多案例持續更新!