26. [实例]抓取链家二手房数据
1.前言
本节使用 Python 爬虫库完成链家二手房(https://bj.lianjia.com/ershoufang/rs/)房源信息抓取,包括楼层、区域、总价、单价等信息。在编写此程序的过程中,您将体会到 lxml 解析库的实际应用。
2. 编写程序流程分析
打开链家网站后,第一步,确定网站是否为静态网站,通过在网页源码内搜索关键字的方法,可以确定其为静态网站;第二步,确定要抓取页面的 URL 规律,第三步,根据要抓取的数据确定 Xpath 表达式;最后一步,编写 Python 爬虫程序。
通过简单的分析可知 URL 具有以下规律:
1 2 3 4 | 第一页: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/ |
3. 确定Xpath表达式
使用 Chrome 开发者工具对页面元素进行审查,从而确定 Xpath 表达式。首先根据要抓取的数据确定“基准表达式”。通过审查一处房源的元素结构,可以得知房源信息都包含在以下代码中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <div class = "info clear" > <div class = "title" ><a class = "" href= "https://bj.lianjia.com/ershoufang/101110713022.html" target= "_blank" data-log_index= "1" data-el= "ershoufang" data-housecode= "101110713022" data-is_focus= "" data-sl= "" >玉竹园小区 满五唯一 楼层高 视野好</a><span class = "goodhouse_tag tagBlock" >必看好房</span></div> <div class = "flood" > <div class = "positionInfo" ><span class = "positionIcon" ></span><a href= "https://bj.lianjia.com/xiaoqu/1111027382276/" target= "_blank" data-log_index= "1" data-el= "region" >玉竹园 </a> - <a href= "https://bj.lianjia.com/ershoufang/liangxiang/" target= "_blank" >良乡</a> </div> </div> <div class = "address" > <div class = "houseInfo" ><span class = "houseIcon" ></span>2室1厅 | 88.62平米 | 北 南 | 简装 | 顶层(共6层) | 2004年建 | 板楼</div> </div> <div class = "followInfo" ><span class = "starIcon" ></span>26人关注 / 7天以前发布</div> <div class = "tag" ><span class = "subway" >近地铁</span><span class = "isVrFutureHome" >VR看装修</span><span class = "taxfree" >房本满五年</span><span class = "haskey" >随时看房</span></div> <div class = "priceInfo" > <div class = "totalPrice" ><span>225</span>万</div> <div class = "unitPrice" data-hid= "101110713022" data-rid= "1111027382276" data-price= "25390" > <span>单价25390元/平米</span></div> </div> </div> |
1) 确定基准表达式
待抓取的房源信息都包含在相应的 <div> 标签中,如下所示:
1 2 3 | <div class = "positionInfo" >..</div> <div class = "address" >...</div> <div class = "priceInfo" >...</div> |
而每个页面中都包含 30 个房源,因此我们要匹配以下节点的父节点或者先辈节点,从而确定 Xpath 基准表达式:
1 | <div class = "info clear" ></div> |
通过页面结构分析可以得出每页的 30 个房源信息全部包含以下节点中:
1 2 3 4 5 | <ul class = "sellListContent" log-mod= "list" > <li class = "clear LOGVIEWDATA LOGCLICKDATA" > 房源信息.. </li> </ul> |
接下来,使用调试工具定位上述元素,然后滚动鼠标滑。这时候神奇的一幕出现了,你会发现li
标签的class
属性值发生了变化,其结果如下:
1 2 3 4 5 | <ul class = "sellListContent" log-mod= "list" > <li class = "clear LOGCLICKDATA" > 房源信息.. </li> </ul> |
发生变化的原因是由于 JS 事件触发导致的。因此就需要去页面的源码页进行匹配。
下面使用Ctrl+F
分别对 class 变化前后的属性值进行检索,最后发现源码页只存在如下属性:
1 | class = "clear LOGVIEWDATA LOGCLICKDATA" |
因此 Xpath 基准表达式如下所示:
1 | //ul[@class="sellListContent"]/li[@class="clear LOGVIEWDATA LOGCLICKDATA"] |
2) 确定抓取信息的表达式
根据页面元素结构确定待抓取信息的 Xpath 表达式,分别如下:
1 2 3 4 | 小区名称: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 2 3 | <div class = "address" > <div class = "houseInfo" ><span class = "houseIcon" ></span>2室1厅 | 88.62平米 | 北 南 | 简装 | 顶层(共6层) | 2004年建 | 板楼</div> </div> |
因此,匹配出的 info_list 列表需要经过处理才能得出我们想要的数据,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 | #户型+面积+方位+是否精装+楼层+... ['2室1厅 | 88.62平米 | 北 南 | 简装 | 顶层(共6层) | 2004年建 | 板楼'] info_list=h.xpath( './/div[@class="houseInfo"]/text()' ) if info_list: #处理列表数据 L=info_list[0].split( '|' ) # ['2室1厅 ', ' 88.62平米 ', ' 北 南 ', ' 简装 ', ' 顶层(共6层) ', ' 2004年建 ', ' 板楼'] if len(L) >= 5: item[ 'model' ]=L[0].strip() item[ 'area' ]=L[1].strip() item[ 'direction' ]=L[2].strip() item[ 'perfect' ]=L[3].strip() item[ 'floor' ]=L[4].strip() |
3) 提高抓取效率
为了提高网页信息的抓取质量,减小网络波动带来的响应,我们可以设置一个规则:在超时时间内(3秒),在该时间内对于请求失败的页面尝试请求三次,如果均未成功,则抓取下一个页面。
requests.get() 方法提供了 timeout 参数可以用来设置超时时间,此方法还提供了其他实用性参数,比如 auth(用户认证)、veryify(证书认证)、proxies(设置代理 IP),这在后续内容中会做相应介绍。
4. 编写程序代码
通过上述分析得出了所有的 Xpath 表达式,下面开始编写爬虫程序,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | #coding:utf8 import requests import random from lxml import etree import time #提供ua信息的的包 from fake_useragent import UserAgent class LinajiaSpider( object ): def __init__(self): self.url= 'https://bj.lianjia.com/ershoufang/pg{}/' #计数,请求一个页面的次数,初始值为1 self.blog=1 # 随机取一个UA def get_header(self): #实例化ua对象 ua=UserAgent() headers={ 'User-Agent' :ua.random} return headers #发送请求 def get_html(self,url): #在超时间内,对于失败页面尝试请求三次 if self.blog<=3: try : res=requests. get (url=url,headers=self.get_header(),timeout=3) html=res.text return html except Exception as e: print(e) self.blog+=1 self.get_html(url) # 解析提取数据 def parse_html(self,url): html=self.get_html(url) if html: p=etree.HTML(html) #基准xpath表达式-30个房源节点对象列表 h_list=p.xpath( '//ul[@class="sellListContent"]/li[@class="clear LOGVIEWDATA LOGCLICKDATA"]' ) #所有列表节点对象 for h in h_list: item={} #名称 name_list=h.xpath( './/a[@data-el="region"]/text()' ) #判断列表是否为空 item[ 'name' ]=name_list[0] if name_list else None #户型+面积+方位+是否精装..['2室1厅 | 88.62平米 | 北 南 | 简装 | 顶层(共6层) | 2004年建 | 板楼'] info_list=h.xpath( './/div[@class="houseInfo"]/text()' ) #判断列表是否为空 if info_list: L=info_list[0].split( '|' ) # ['2室1厅 ', ' 88.62平米 ', ' 北 南 ', ' 简装 ', ' 顶层(共6层) ', ' 2004年建 ', ' 板楼'] if len(L) >= 5: item[ 'model' ]=L[0].strip() item[ 'area' ]=L[1].strip() item[ 'direction' ]=L[2].strip() item[ 'perfect' ]=L[3].strip() item[ 'floor' ]=L[4].strip() #区域+总价+单价 address_list=h.xpath( './/div[@class="positionInfo"]/a/text()' ) item[ 'address' ]=address_list[0].strip() if address_list else None total_list=h.xpath( './/div[@class="totalPrice"]/span/text()' ) item[ 'total_list' ]=total_list[0].strip() if total_list else None price_list=h.xpath( './/div[@class="unitPrice"]/span/text()' ) item[ 'price_list' ]=price_list[0].strip() if price_list else None print(item) # 入口函数 def run(self): try : for i in range(1,101): url=self.url.format(i) self.parse_html(url) time.sleep(random.randint(1,3)) #每次抓取一页要初始化一次self.blog self.blog=1 except Exception as e: print( '发生错误' ,e) if __name__ == '__main__' : spider=LinajiaSpider() spider.run() |
展示部分输出结果:
1 2 3 4 5 6 7 | { '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元/平米' } ... |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!