动态爬取链家二手房成交记录并保存至Excel
一、先观察网页结构
链家成交记录网址:https://bj.lianjia.com/chengjiao/
每页有30条成交记录,点击记录提示要下载APP才能查看详细信息。不管它,我们直接审查元素,找到成交记录的链接,点击打开。如下图
链接后面有一串数字,应该是这个成交记录的id号,由于记录每日更新,我们每次爬取完成之后用一个txt文本保存最新记录的id号,以便下次准确定位爬取结束的位置。
接下来回到成交记录页面,观察其翻页时网址的变化:
https://bj.lianjia.com/chengjiao/pg2/
https://bj.lianjia.com/chengjiao/pg3/
地址后加上:/pg+页数 就可以翻页了。
然后在详细信息页面观察要爬取的内容,如图:
包括基本属性、交易属性、成交额、成交单价和成交日期,全部爬取。
接下来老规矩,审查元素,找到这些内容所在的标签:
标签位置已经清楚,说一下爬取的基本思路:
先获取30个记录的链接,然后依次爬取信息,最后打开下一页,循环往复。
这是为了在第一次爬取的时候(有100页),一旦出现异常,可以将已爬取的先保存。
二、代码解析
需要的包有numpy,pandas,BeautifulSoup,re,urllib等
代码解析如下:
1、getbsobj函数
def getbsobj(url):
try:
html = urlopen(url,timeout=3)
except (HTTPError,socket.timeout):
return None
return BeautifulSoup(html,'html.parser')
定义一个返回bs对象的函数,包括一些异常处理。
2、getLinksList函数
def getLinksList(url,n):
'''
:param url: 链接
:param n: 第n页
:return: 链接列表
'''
ls = []
bsobj=getbsobj(url+'/pg%d'%n)
if not bsobj:
print('该页打不开')
return None
while True:
aTagList = bsobj.find('ul', {'class': 'listContent'}).findAll('a', {'class': 'img'})
if len(aTagList)>0:
print('打开成功')
break
else:
print('进入休眠')
time.sleep(60)
bsobj=getbsobj(url+'/pg%d'%n)
for aTag in aTagList:
if 'href' in aTag.attrs:
ls.append(aTag['href'])
return ls
定义获取30条成交记录链接的函数,以列表形式返回。
链接在ul标签下的a标签中。
中间有一个循环,原因是有的时候网页能打开,但是成交记录却没有显示,这个时候需要尝试重新打开,直到出现成交记录为止。没打开可能是因为访问过于频繁,所以打不开的时候进入休眠,60秒。
3、getInfo函数
infoArray = []
一条记录的信息由一个一维列表保存,而infoArray是保存获取的所有记录信息的二维列表。
tags = bsobj.find('div',{'class':'introContent'}).findAll('li')
获取保存基本属性和交易属性内容的标签列表。
info = list()
info.append(bsobj.head.title.get_text().split()[0])
info是保存一条记录的所有信息的临时列表。
这里添加的是小区名字。
for tag in tags:
info.append(tag.get_text().strip()[4:])
这里添加的是基本属性和交易属性的内容
ul = bsobj.find('ul',{'class':'record_list'})
info.append(ul.li.span.get_text().strip())
text = ul.li.p.get_text().strip()
a = re.search(r'\d+元/平',text)
if a ==None:
info.append('无')
else:
info.append(a.group()[:-3])
b = re.search(r'\d+-\d+-\d+', text)
if b == None:
info.append(text)
else:
info.append(b.group())
这里添加的是成交额、单价和成交日期,因为和上面的属性不在一起,需要另外处理。
infoArray.append(info)
添加一条记录
arr = np.array(infoArray)
将最终的infoArray转换成矩阵arr,并返回。
4、checkid函数
在动态爬取的时候检查该id是否已经获取,若已获取,那么说明已经爬取到上一次运行时的最新记录,此时已经无需继续爬取。
def checkid(ls,id):
'''
:param ls: 链接列表
:param id: 成交id
:return: bool
'''
links = []
tag = False
for lk in ls:
if lk[-17:-5]!=str(id):
links.append(lk)
else:break
if len(links)!=len(ls):
tag = True
ls = links
return tag,ls
这里对列表中的每一条链接进行检查,若该id与上一次保存的id相同,则该链接及其之后的链接全部丢弃,返回一个bool值和处理后的列表。
5、getIndex函数
用于获取保存至Excel时需要的属性标签
def getIndex(url):
cols = ['小区名字']
ls = getLinksList(url, 1)
bs = getbsobj(ls[0])
tags = bs.find('div', {'class': 'introContent'}).findAll('li')
for tag in tags:
cols.append(tag.span.get_text().strip())
cols += ['成交额(万元)','单价(元/平)','日期']
new_id = ls[0][-17:-5]
return cols,new_id
6、download函数
主函数
def download(url,start,end):
从第start页开始,爬取到end页为止(包括end页)
cols,new_id = getIndex(url)
获取属性列表和最新记录的id号
ls = getLinksList(url,i)
获取链接列表
tag,ls = checkid(ls,id)
检查id
arr=getInfo(ls)
获取数据矩阵
df_new = pd.DataFrame(arr,columns=cols)
df = df.append(df_new,ignore_index=True)
转化为DataFrame格式。
df_old = pd.read_excel('链家成交数据.xlsx')
df_old = df.append(df_old, ignore_index=True)
df_old.to_excel('链家成交数据.xlsx')
保存为Excel。
三、输出结果
四、完整代码
from bs4 import BeautifulSoup
from urllib.request import urlopen
from urllib.error import HTTPError
import pandas as pd
import numpy as np
import re
import socket,time
url = 'https://bj.lianjia.com/chengjiao'
def getbsobj(url):
try:
html = urlopen(url,timeout=3)
except (HTTPError,socket.timeout):
return None
return BeautifulSoup(html,'html.parser')
def getLinksList(url,n):
'''
:param url: 链接
:param n: 第n页
:return: 链接列表
'''
ls = []
bsobj=getbsobj(url+'/pg%d'%n)
if not bsobj:
print('该页打不开')
return None
while True:
aTagList = bsobj.find('ul', {'class': 'listContent'}).findAll('a', {'class': 'img'})
if len(aTagList)>0:
print('打开成功')
break
else:
print('进入休眠')
time.sleep(60)
bsobj=getbsobj(url+'/pg%d'%n)
for aTag in aTagList:
if 'href' in aTag.attrs:
ls.append(aTag['href'])
return ls
def getInfo(ls):
'''
:param ls:链接列表
:return: 数组
'''
infoArray = []
i = 1
for lk in ls:
print('正在获取第%d条信息'%i)
bsobj = getbsobj(lk)
if not bsobj:continue
tags = bsobj.find('div',{'class':'introContent'}).findAll('li')
info = list()
info.append(bsobj.head.title.get_text().split()[0])
for tag in tags:
info.append(tag.get_text().strip()[4:])
ul = bsobj.find('ul',{'class':'record_list'})
info.append(ul.li.span.get_text().strip())
text = ul.li.p.get_text().strip()
a = re.search(r'\d+元/平',text)
if a ==None:
info.append('无')
else:
info.append(a.group()[:-3])
b = re.search(r'\d+-\d+-\d+', text)
if b == None:
info.append(text)
else:
info.append(b.group())
infoArray.append(info)
i += 1
print('-'*20)
arr = np.array(infoArray)
return arr
def checkid(ls,id):
'''
:param ls: 链接列表
:param id: 成交id
:return: bool
'''
links = []
tag = False
for lk in ls:
if lk[-17:-5]!=str(id):
links.append(lk)
else:break
if len(links)!=len(ls):
tag = True
ls = links
return tag,ls
def getIndex(url):
cols = ['小区名字']
ls = getLinksList(url, 1)
bs = getbsobj(ls[0])
tags = bs.find('div', {'class': 'introContent'}).findAll('li')
for tag in tags:
cols.append(tag.span.get_text().strip())
cols += ['成交额(万元)','单价(元/平)','日期']
new_id = ls[0][-17:-5]
return cols,new_id
def download(url,start,end):
i = start
fn = open('id.txt','r')
id = fn.readline()
fn.close()
cols,new_id = getIndex(url)
df = pd.DataFrame()
try:
while True:
print('正在获取第%d页链接'%i)
ls = getLinksList(url,i)
if not ls:
print(ls)
print(1)
break
#print(ls)
tag,ls = checkid(ls,id)
#print(ls)
arr=getInfo(ls)
df_new = pd.DataFrame(arr,columns=cols)
df = df.append(df_new,ignore_index=True)
if tag:
print(2)
break
i += 1
if i>end:
print(3)
break
except Exception as e:
print(e)
else:
df_old = pd.read_excel('链家成交数据.xlsx')
df_old = df.append(df_old, ignore_index=True)
df_old.to_excel('链家成交数据.xlsx')
fn = open('id.txt', 'w')
fn.write(new_id)
fn.close()
print('程序执行完毕!')
if __name__ == '__main__':
download(url,1,20)
进行第一次爬取的时候,上述代码需要稍作修改,try的后面加个finally,将已爬取的内容保存,防止出现异常带来的损失。之后的爬取就可以用上述代码了。根据观察,链家数据每天的更新大约有3至16页不等,因此download函数参数取1和20。
正常结束应该是先print(2),然后print(程序执行完毕),假如某天更新了21页,那么会先print(3),然后print(程序执行完毕),这时需要修改参数再次执行,当然,也可以直接把参数20改为100,这样除非出现莫名异常,一般都可以正常执行完。