环境:Windows7+python3.6+Pycharm2017
目标:抓取链家北京地区已成交二手房信息(无需登录),如下图,户型、朝向、成交时间价格等,保存到csv。最后一共抓取约58W数据,程序运行8h。
---全部文章: 京东爬虫 、链家爬虫、美团爬虫、微信公众号爬虫、字体反爬、Django笔记、阿里云部署、vi\vim入门----
一、打开北京二手房网页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如下:
第2页:https://bj.lianjia.com/xiaoqu/pg2/
第3页:https://bj.lianjia.com/xiaoqu/pg3/
第100页:https://bj.lianjia.com/xiaoqu/pg100/
四、如何抓取每个小区的已成交二手房信息
点击某个小区如北京新天地,进入小区详情页面,下拉找到北京新天地小区成交记录,点击下面的查看全部成交记录,即可得到该小区全部已成交房源信息。通过左上角房源总数2133套,除以每页30套,我们可以得到该小区已成交房源一共有多少页。近30天内成交的进入详情页面抓取。观察页面的url https://bj.lianjia.com/chengjiao/c1111027375945/ ,观察规律就是最后的一串数字是变化的,是每个小区的id。翻页的规律如下:
第二页:https://bj.lianjia.com/chengjiao/pg2c1111027375945/
第三页:https://bj.lianjia.com/chengjiao/pg3c1111027375945/
所以我们的思路就是先抓取每个小区的id,然后构造小区成交房源页面的url,通过房源总数来得知该小区一共有多少页,翻页抓取。近30天内成交的需要进入详情页面抓取,其他的直接在列表页面就可以。
五、抓取小区id
一共100页,每页的url如下,也很简单,直接每个li标签中的data-id属性就是小区的id。注意的是该页面有对ip访问次数做限制,单ip连续访问好像是25页就会被封,所以需要采用代理ip,这里每个ip只抓取20页。还有一点需要注意的就是抓取的id需要做排重,此处用的set。还有就是对于第一次访问没有得到数据的页面需要再次访问。
https://bj.lianjia.com/xiaoqu/pg2/
https://bj.lianjia.com/xiaoqu/pg3/
实际一共抓取到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%。
水平有限,希望大家指正。
---全部文章: 京东爬虫 、链家爬虫、美团爬虫、微信公众号爬虫、字体反爬、Django笔记、阿里云部署、vi\vim入门----
https://blog.csdn.net/xing851483876/article/details/81408281