Ctrl
+
D
一键收藏

Python爬虫抓取链家二手房数据

本节使用 Python 爬虫库完成链家二手房(https://bj.lianjia.com/ershoufang/rs/)房源信息抓取,包括楼层、区域、总价、单价等信息。在编写此程序的过程中,您将体会到 lxml 解析库的实际应用。

编写程序流程分析

打开链家网站后,第一步,确定网站是否为静态网站,通过在网页源码内搜索关键字的方法,可以确定其为静态网站;第二步,确定要抓取页面的 URL 规律,第三步,根据要抓取的数据确定 Xpath 表达式;最后一步,编写 Python 爬虫程序。

通过简单的分析可知 URL 具有以下规律:
第一页:https://bj.lianjia.com/ershoufang/pg1/
第二页:https://bj.lianjia.com/ershoufang/pg2/
第三页:https://bj.lianjia.com/ershoufang/pg3/
第n页:https://bj.lianjia.com/ershoufang/pgn/

确定Xpath表达式

使用 Chrome 开发者工具对页面元素进行审查,从而确定 Xpath 表达式。首先根据要抓取的数据确定“基准表达式”。通过审查一处房源的元素结构,可以得知房源信息都包含在以下代码中:
  1. <div class="info clear">
  2. <div class="title"><a class="" href="https://bj.lianjia.com/ershoufang/101110713022.html" target="_blank"
  3. data-log_index="1" data-el="ershoufang" data-housecode="101110713022" data-is_focus="" data-sl="">玉竹园小区 满五唯一
  4. 楼层高 视野好</a><span class="goodhouse_tag tagBlock">必看好房</span></div>
  5. <div class="flood">
  6. <div class="positionInfo"><span class="positionIcon"></span><a
  7. href="https://bj.lianjia.com/xiaoqu/1111027382276/" target="_blank" data-log_index="1"
  8. data-el="region">玉竹园 </a> - <a href="https://bj.lianjia.com/ershoufang/liangxiang/"
  9. target="_blank">良乡</a> </div>
  10. </div>
  11. <div class="address">
  12. <div class="houseInfo"><span class="houseIcon"></span>2室1厅 | 88.62平米 | 北 南 | 简装 | 顶层(共6层) | 2004年建 | 板楼</div>
  13. </div>
  14. <div class="followInfo"><span class="starIcon"></span>26人关注 / 7天以前发布</div>
  15. <div class="tag"><span class="subway">近地铁</span><span class="isVrFutureHome">VR看装修</span><span
  16. class="taxfree">房本满五年</span><span class="haskey">随时看房</span></div>
  17. <div class="priceInfo">
  18. <div class="totalPrice"><span>225</span></div>
  19. <div class="unitPrice" data-hid="101110713022" data-rid="1111027382276" data-price="25390">
  20. <span>单价25390元/平米</span></div>
  21. </div>
  22. </div>

1) 确定基准表达式

待抓取的房源信息都包含在相应的 <div> 标签中,如下所示:
<div class="positionInfo">..</div>
<div class="address">...</div>
<div class="priceInfo">...</div>
而每个页面中都包含 30 个房源,因此我们要匹配以下节点的父节点或者先辈节点,从而确定 Xpath 基准表达式:
<div class="info clear"></div>
通过页面结构分析可以得出每页的 30 个房源信息全部包含以下节点中:
<ul class="sellListContent" log-mod="list">
<li class="clear LOGVIEWDATA LOGCLICKDATA">
房源信息..
</li>
</ul>
接下来,使用调试工具定位上述元素,然后滚动鼠标滑。这时候神奇的一幕出现了,你会发现li标签的class属性值发生了变化,其结果如下:
<ul class="sellListContent" log-mod="list">
<li class="clear LOGCLICKDATA">
房源信息..
</li>
</ul>
发生变化的原因是由于 JS 事件触发导致的。因此就需要去页面的源码页进行匹配。

下面使用Ctrl+F分别对 class 变化前后的属性值进行检索,最后发现源码页只存在如下属性:
class="clear LOGVIEWDATA LOGCLICKDATA"
因此 Xpath 基准表达式如下所示:
//ul[@class="sellListContent"]/li[@class="clear LOGVIEWDATA LOGCLICKDATA"]

2) 确定抓取信息的表达式

根据页面元素结构确定待抓取信息的 Xpath 表达式,分别如下:
小区名称:name_list=h.xpath('.//a[@data-el="region"]/text()')
房屋介绍:info_list=h.xpath('.//div[@class="houseInfo"]/text()')
地址信息:address_list=h.xpath('.//div[@class="positionInfo"]/a/text()')
单价信息:price_list=h.xpath('.//div[@class="unitPrice"]/span/text()')
其中房屋介绍,主要包含了以下信息:
  1. <div class="address">
  2. <div class="houseInfo"><span class="houseIcon"></span>2室1厅 | 88.62平米 | 北 南 | 简装 | 顶层(共6层) | 2004年建 | 板楼</div>
  3. </div>
因此,匹配出的 info_list 列表需要经过处理才能得出我们想要的数据,如下所示:
  1. #户型+面积+方位+是否精装+楼层+... ['2室1厅 | 88.62平米 | 北 南 | 简装 | 顶层(共6层) | 2004年建 | 板楼']
  2. info_list=h.xpath('.//div[@class="houseInfo"]/text()')
  3. if info_list:
  4. #处理列表数据
  5. L=info_list[0].split('|')
  6. # ['2室1厅 ', ' 88.62平米 ', ' 北 南 ', ' 简装 ', ' 顶层(共6层) ', ' 2004年建 ', ' 板楼']
  7. if len(L) >= 5:
  8. item['model']=L[0].strip()
  9. item['area']=L[1].strip()
  10. item['direction']=L[2].strip()
  11. item['perfect']=L[3].strip()
  12. item['floor']=L[4].strip()

3) 提高抓取效率

为了提高网页信息的抓取质量,减小网络波动带来的响应,我们可以设置一个规则:在超时时间内(3秒),在该时间内对于请求失败的页面尝试请求三次,如果均未成功,则抓取下一个页面。

requests.get() 方法提供了 timeout 参数可以用来设置超时时间,此方法还提供了其他实用性参数,比如 auth(用户认证)、veryify(证书认证)、proxies(设置代理 IP),这在后续内容中会做相应介绍。

编写程序代码

通过上述分析得出了所有的 Xpath 表达式,下面开始编写爬虫程序,代码如下:
  1. #coding:utf8
  2. import requests
  3. import random
  4. from lxml import etree
  5. import time
  6. #提供ua信息的的包
  7. from fake_useragent import UserAgent
  8.  
  9. class LinajiaSpider(object):
  10. def __init__(self):
  11. self.url='https://bj.lianjia.com/ershoufang/pg{}/'
  12. #计数,请求一个页面的次数,初始值为1
  13. self.blog=1
  14.  
  15. # 随机取一个UA
  16. def get_header(self):
  17. #实例化ua对象
  18. ua=UserAgent()
  19. headers={'User-Agent':ua.random}
  20. return headers
  21. #发送请求
  22. def get_html(self,url):
  23. #在超时间内,对于失败页面尝试请求三次
  24. if self.blog<=3:
  25. try:
  26. res=requests.get(url=url,headers=self.get_header(),timeout=3)
  27. html=res.text
  28. return html
  29. except Exception as e:
  30. print(e)
  31. self.blog+=1
  32. self.get_html(url)
  33.  
  34. # 解析提取数据
  35. def parse_html(self,url):
  36. html=self.get_html(url)
  37. if html:
  38. p=etree.HTML(html)
  39. #基准xpath表达式-30个房源节点对象列表
  40. h_list=p.xpath('//ul[@class="sellListContent"]/li[@class="clear LOGVIEWDATA LOGCLICKDATA"]')
  41. #所有列表节点对象
  42. for h in h_list:
  43. item={}
  44. #名称
  45. name_list=h.xpath('.//a[@data-el="region"]/text()')
  46. #判断列表是否为空
  47. item['name']=name_list[0] if name_list else None
  48. #户型+面积+方位+是否精装..['2室1厅 | 88.62平米 | 北 南 | 简装 | 顶层(共6层) | 2004年建 | 板楼']
  49. info_list=h.xpath('.//div[@class="houseInfo"]/text()')
  50. #判断列表是否为空
  51. if info_list:
  52. L=info_list[0].split('|')
  53. # ['2室1厅 ', ' 88.62平米 ', ' 北 南 ', ' 简装 ', ' 顶层(共6层) ', ' 2004年建 ', ' 板楼']
  54. if len(L) >= 5:
  55. item['model']=L[0].strip()
  56. item['area']=L[1].strip()
  57. item['direction']=L[2].strip()
  58. item['perfect']=L[3].strip()
  59. item['floor']=L[4].strip()
  60. #区域+总价+单价
  61.  
  62. address_list=h.xpath('.//div[@class="positionInfo"]/a/text()')
  63. item['address']=address_list[0].strip() if address_list else None
  64.  
  65. total_list=h.xpath('.//div[@class="totalPrice"]/span/text()')
  66. item['total_list']=total_list[0].strip() if total_list else None
  67.  
  68. price_list=h.xpath('.//div[@class="unitPrice"]/span/text()')
  69. item['price_list']=price_list[0].strip() if price_list else None
  70. print(item)
  71.  
  72. # 入口函数
  73. def run(self):
  74. try:
  75. for i in range(1,101):
  76. url=self.url.format(i)
  77. self.parse_html(url)
  78. time.sleep(random.randint(1,3))
  79. #每次抓取一页要初始化一次self.blog
  80. self.blog=1
  81. except Exception as e:
  82. print('发生错误',e)
  83.  
  84. if __name__ == '__main__':
  85. spider=LinajiaSpider()
  86. spider.run()
展示部分输出结果:
{'name': '玉竹园 ', 'model': '2室1厅', 'area': '88.62平米', 'direction': '北 南', 'perfect': '简装', 'floor': '顶层(共6层)', 'address': '玉竹园', 'total_list': '225', 'price_list': '单价25390元/平米'}
{'name': '摩卡空间 ', 'model': '2室1厅', 'area': '71.95平米', 'direction': '东南', 'perfect': '简装', 'floor': '高楼层(共17层)', 'address': '摩卡空间', 'total_list': '372', 'price_list': '单价51703元/平米'}
{'name': '长城国际 ', 'model': '1室1厅', 'area': '52.73平米', 'direction': '西', 'perfect': '精装', 'floor': '22层', 'address': '长城国际', 'total_list': '235', 'price_list': '单价44567元/平米'}
{'name': '牛街西里 ', 'model': '3室1厅', 'area': '102.6平米', 'direction': '东北', 'perfect': '其他', 'floor': '中楼层(共20层)', 'address': '牛街西里', 'total_list': '815', 'price_list': '单价79435元/平米'}
{'name': '新安里 ', 'model': '1室2厅', 'area': '51.56平米', 'direction': '南 北', 'perfect': '精装', 'floor': '顶层(共6层)', 'address': '新安里', 'total_list': '197', 'price_list': '单价38208元/平米'}
{'name': '龙华园 ', 'model': '2室1厅', 'area': '67.73平米', 'direction': '南 北', 'perfect': '简装', 'floor': '顶层(共6层)', 'address': '龙华园', 'total_list': '342', 'price_list': '单价50495元/平米'}
...
Ctrl
+
D
一键收藏