最近一直在看论文,也有很久没有coding了,感觉对爬虫的技术有些生疏,我觉得即使现在手头没有在做这方面的东西,经常爬点对技术保鲜还是很重要的。所以这次我打算爬链家的房价数据,目的主要是对爬虫和Python的东西作一个巩固,然后做一个分析。
以链家广州为例查看网页结构,可以看到它是下图这样的:
看起来内容元素的结构十分清晰,分类很好,都是我们想要的东西。
链家对爬虫的容忍度挺高的,不会封IP,也没有要求登录,是我们练手的好题材(不过大家要适可而止,人家的服务器也不是无底洞)
我的环境:Python 3.6,jupyter notebook
爬虫主要有两个部分:下载模块和解析模块。
下载模块
在之前写爬虫的时候,我发现下载模块的代码重复度很高,无论对什么网址,需要解决的东西大致有三个:
- User-Agent,用来模拟浏览器,爬虫本质上是一个下载器,需要通过加入一些浏览器的标识信息使得服务器以为这是一个来自浏览器的请求。
- IP代理,大部分的反爬虫策略都是通过屏蔽IP地址来限制爬虫的,当同一个IP短时间内访问过于频繁,就会被认为是爬虫,从而返回403 forbidden的结果。一般来说,免费IP代理都很垃圾,不是很慢就是不能用,天下没有免费的午餐,建议使用付费IP代理。
- Cookie,对于一些需要登录才可以查看的网页(微博,豆瓣等),需要从浏览器获取上次成功登录的cookie,携带这个cookie去访问,才能通过。
我把这些常用代码写成了一个类(这些代码在这里),但是我们这次用不到,所以我只从中摘取了一部分:
import requests
from lxml import etree
import random
import json
import pandas as pd
from pandas.io.json import json_normalize
import math
import re# 随机获取一个UserAgent
def getUserAgent():UA_list = ["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) App leWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53","Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13","Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; QIHU 360EE)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; Maxthon/3.0)","Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50","Mozilla/5.0 (Macintosh; U; IntelMac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1Safari/534.50","Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1","Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6","Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6","Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5","Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3","Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3","Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3","Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3","Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24","Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"]return random.choice(UA_list)# 使用requests获取HTML页面
def getHTML(url):global invalid_ip_countheaders = {'User-Agent': getUserAgent(),'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',}try:web_data=requests.get(url,headers=headers,timeout=20)#超时时间为20秒status_code=web_data.status_coderetry_count=0while(str(status_code)!='200' and retry_count<5):print('status code: ',status_code,' retry downloading url: ',url , ' ...')web_data=requests.get(url,headers=headers,timeout=20)status_code=web_data.status_coderetry_count+=1if str(status_code)=='200':return web_data.content.decode('utf-8')else:return "ERROR"except Exception as e:print(e)return "ERROR"
解析模块
通常在解析网页之前,第一个需要分析的地方就是怎么才能拿到下一页的链接。
- 我们可以检查“下一页”按钮,提取每个页面的“下一页”中的链接,从而得到下一页的url
- 我们可以不断点击第1页,第2页,第3页,观察浏览器的地址栏有没有什么规律,通过修改url模板来得到下一页
- 我们可以使用浏览器的调试功能,在点击下一页的时候进行网络抓包,看看浏览器的请求是什么,比如Chrome我们就可以按F12打开开发人员工具:
我们很幸运的看到了在每翻一页的时候浏览器向服务器发送了这样一条请求:
https://gz.fang.lianjia.com/loupan/pg2/?_t=1
把它输入浏览器地址栏可以看到返回了一整篇的代码:
没错这正是我们想要的!
仔细观察可以发现,这不是什么乱码,这是下一页网页的内容,它现在是使用Json格式返回了,浏览器根据网址的模板将数据填充上去渲染就成了我们所看到的页面了,我们现在可以直接拿到这些原始数据,也就意味着省去了从网页中解析的步骤,多么的方便。
所以我们就通过这个网址模板逐页发送请求,然后使用json包进行解析。
我们打算最后把它存成pandas的CSV文件,这样方便我们后续进行分析,所以就不考虑数据库了。
代码如下:
"""
传入的city是一个元组:(城市名,城市url)
"""
def getDetail(city):url=city[1]print('searching city: ',city[0],'...')url='https:'+url+'/loupan/'print('url is: ',url) #拼接成完整urlhtml=getHTML(url) #下载网页selector=etree.HTML(html)"""获取最大页数"""try:maxPage=selector.xpath('//div[@class="page-box"]/@data-total-count')maxPage=math.ceil(int(maxPage[0])/10)except Exception as e:maxPage=1print('max page is: ',maxPage)df=pd.DataFrame() # 初始化dataframefor page in range(1,maxPage+1):print('fecthing page',page,'...')url=url+str(page)+'/?_t=1' #构造每一页的urlresult=json.loads(getHTML(url)) #获取网页内容df_iter=json_normalize(result['data']['list']) #格式化为dataframedf=df.append(df_iter) #将每一页的数据拼接file_path='./loupan/'+city[0]+'.csv'df.to_csv(file_path,index=False,encoding='utf-8')
现在只是一个特定城市的代码,那么说好的全国呢?
链家对于全国的城市的列表在首页的底部:
通过解析这些元素可以获得背后的每个城市的链接
代码如下:
# 获取全国所有的已知城市
url='https://gz.fang.lianjia.com/loupan/pg1/'
html=getHTML(url)
selector=etree.HTML(html)
cities_url_template=selector.xpath('//div[@class="city-change animated"]/div[@class="fc-main clear"]//ul//li/div//a/@href')
cities_name=selector.xpath('//div[@class="city-change animated"]/div[@class="fc-main clear"]//ul//li/div//a/text()')
cities=list(zip(cities_name,cities_url_template))
for city in cities:#city 是一个元组 (城市名,城市url)getDetail(city)
执行上面的代码,大概十几分钟就可以爬完全国的新房房价数据了:
仅仅爬新房的话,数据量有点小,因为你可以看到有些城市的房源也就个位数,没多少新房,倒是二手房的数量在每个城市都比较多,我们接下来打算再爬二手房的信息。
二手房
再爬二手房的时候就没这么幸运了,通过观察浏览器的抓包轨迹可以发现,所有的请求相应都是直接将网页返回,而不是返回json字段了。真是令人脑壳疼,那就意味着我们不能偷懒,而是必须老老实实解析网页了。
下载模块跟新房的那部分代码是一样的,所以这次我们只需要考虑解析的部分就好了。
首先要确定的是我们需要什么数据。我打算提取以下几个可能有用的类别:
- 名称
- 链接
- 房屋信息
- 楼层
- 房龄
- 地区
- 关注的人
- 标签
- 总价
- 单位面积价格
因为通过分析页面我发现,其实你在网页上看的时候划分的整整齐齐的元素,是很难一个一个直接解析出来的,比如标签,一个房子可以打很多个标签,可是在解析的时候,是把页面上所有的房子一起解析的,无法做到逐个房子处理。所以有一些数据只好先放在一起,随后再进行进一步处理。
所以整体思路是这样的:
通过首页找到全国各个城市二手房的网址 -> 进入一个特定城市的首页 -> 获取最大页数 -> 爬取每一页的名称、链接、房屋信息等 -> 构造一个dataframe,保存成文件
代码如下:
def get_ershoufang(city):print('getting city:',city[0])print('url: ',city[1])names=[]links=[]houseInfo=[]floor=[]age=[]district=[]concern=[]tags=[]total_price=[]unit_price=[]city_link=city[1]html=getHTML(city_link)selector = etree.HTML(html)try:maxpage_str = selector.xpath('//div[@class = "page-box house-lst-page-box"]/@page-data')maxpage = maxpage_str[0].split(',')[0].split(':')[1]maxpage = int(maxpage)except:maxpage = 1print('max page is : ',maxpage)for page in range(1,maxpage):print('fetching page ',page,'...')link = city_link+'pg'+str(page)+'/'html = getHTML(link)selector = etree.HTML(html)lis=selector.xpath('//ul[@class = "sellListContent"]//li[@class = "clear LOGCLICKDATA"]')for li in lis:names.append(li.xpath('./div[@class="info clear"]/div[@class="title"]/a/text()')[0])links.append(li.xpath('./div[@class="info clear"]/div[@class="title"]/a/@href')[0])positions.append(li.xpath('./div[@class="info clear"]/div[@class="address"]/div[@class="houseInfo"]/a/text()')[0])try:houseInfo.append(' '.join(li.xpath('./div[@class="info clear"]/div[@class="address"]/div[@class="houseInfo"]//text()'))[1:-1])except:houseInfo.append('None')try:floor.append(li.xpath('./div[@class="info clear"]/div[@class="flood"]/div[@class="positionInfo"]//text()')[0])except:floor.append('None')try:age.append(li.xpath('./div[@class="info clear"]/div[@class="flood"]/div[@class="positionInfo"]//text()')[2])except:age.append('None')try:district.append(li.xpath('./div[@class="info clear"]/div[@class="flood"]/div[@class="positionInfo"]//text()')[-1])except:district.append('None')try:concern.append(li.xpath('./div[@class="info clear"]/div[@class="followInfo"]//text()')[0])except:concern.append('None')try:tags.append('_'.join(li.xpath('./div[@class="info clear"]/div[@class="followInfo"]/div[@class="tag"]//text()')))except:tags.append('None')try:if city[0]=='北京二手房':total_price.append(' '.join(li.xpath('./div[@class="info clear"]/div[@class="followInfo"]/div[@class="priceInfo"]/div[@class="totalPrice"]//text()')))else:total_price.append(' '.join(li.xpath('./div[@class="info clear"]/div[@class="priceInfo"]/div[@class="totalPrice"]//text()')))except:total_price.append('None')try:if city[0]=='北京二手房':unit_price.append(' '.join(li.xpath('./div[@class="info clear"]/div[@class="followInfo"]/div[@class="priceInfo"]/div[@class="unitPrice"]//text()')))else:unit_price.append(li.xpath('./div[@class="info clear"]/div[@class="priceInfo"]/div[@class="unitPrice"]//text()')[0])except:unit_price.append('None')df_data={'name':names,'link':links,'position':positions,'house_info':houseInfo,'floor':floor,'age':age,'district':district,'concern':concern,'tags':tags,'total_price':total_price,'unit_price':unit_price}df=pd.DataFrame(df_data)df.to_csv('./ershoufang/'+city[0]+'.csv',index=False,encoding='utf-8')
爬取全国所有二手房城市的链接代码:
# 获取全国所有的城市
url='https://gz.lianjia.com/ershoufang/'
html=getHTML(url)
selector=etree.HTML(html)
city_name=selector.xpath('//div[@class="link-list"]/div[1]/dd//a/text()')
city_links=selector.xpath('//div[@class="link-list"]/div[1]/dd//a/@href')
cities=list(zip(city_name,city_links)) #('北京' , 'https://bj.lianjia.com/ershoufang/')
for city in cities:get_ershoufang(city)
由于数据比较多,这段代码要跑的更久一点,大概一个多小时吧?因为中间断过,一共多久我也不记得了。
结果如图:
以上便是爬虫的部分,数据分析的部分在链家全国房价数据分析 : 数据分析及可视化