为了统计某地区的二手房交易情况,需要获取大量二手房交易数据。链家网是一个专业的房地产O2O交易平台,主营二手房、新房、租房、房价查询等业务。链家网展示的房源众多、房屋信息详实并且页面设计工整。
使用Scrapy爬取链家网中苏州市二手房交易数据并保存于CSV文件中。如图4-9所示为苏州市二手房信息的主页面,地址为 https://su.lianjia.com/ershoufang/ 。爬取的房屋数据有:
·房屋名称;
·房屋户型;
·建筑面积;
·房屋朝向;
·装修情况;
·有无电梯;
·房屋总价;
·房屋单价;
·房屋产权。
图4-9 链家网中苏州市二手房信息展示主页面
单击图4-9中的某个房屋标题链接,进入房屋详情页,如图4-10所示。
图4-10 房屋信息详情页
具体要求有:
(1)房屋面积、总价和单价只需要具体的数字,不需要单位名称。
(2)删除字段不全的房屋数据,如有的房屋朝向会显示“暂无数据”,应该剔除。
(3)保存到CSV文件中的数据,字段要按照这样的顺序排列:房屋名称,房屋户型,建筑面积,房屋朝向,装修情况,有无电梯,房屋总价,房屋单价,房屋产权。
1.使用Spider提取数据
通过页面分析发现,爬取的二手房交易数据分布在两个页面中,每个页面包含一部分数据。房屋名称、房屋户型、建筑面积、房屋朝向、装修情况、有无电梯、总价、单价的数据可以在房屋信息主页面中(图4-9)获取,但是产权数据需要进入房屋详情页面中(图4-10)获取。因此,要想获取一条完整的房屋交易数据,就需要解析两个页面,再将两个页面的数据合并发送给引擎保存到CSV文件中。如图4-11所示为项目实现的流程。
首先在start_requests()方法中生成访问初始页的Request对象。页面下载后,主页面解析函数提取页面中的部分房屋信息(不含产权信息),同时获取详情页的URL。将详情页的URL和已提取的房屋信息作为参数生成详情页的Request对象提交给引擎。详情页下载后,详情页解析函数提取房屋产权信息,并与主解析函数中得到的部分房屋数据合并,形成一条完整的房屋数据,最后将数据提交给Pipeline处理。
2.使用Item封装数据
数据提取后,使用Item封装数据。
图4-11 链家二手房数据爬取功能流程图
3.使用Pipeline处理数据
数据处理主要分为以下两大部分:
(1)过滤、清理数据(FilterPipeline)。
从爬取到的房屋面积、单价和产权文本中提取出数字,并且过滤掉字段不全的房屋。
(2)将数据保存于CSV文件(CSVPipeline)中。
因为使用命令生成的CSV文件中,字段的排列是随机的,而本项目要求字段固定排列,因此需要通过Pipeline将数据保存于CSV文件中。
本项目建立了两个Pipeline类分别对应上述两种数据处理功能,体现了模块化的设计思想。
1.创建项目
创建一个名为lianjia_home的Scrapy项目。
>scrapy startproject lianjia_home
2.使用Item封装数据
打开项目lianjia_home中的items.py源文件,添加二手房信息字段。实现代码如下:
class LianjiaHomeItem(scrapy.Item): name = scrapy.Field() #名称 type = scrapy.Field() #户型 area = scrapy.Field() #面积 direction = scrapy.Field() #朝向 fitment = scrapy.Field() #装修情况 elevator = scrapy.Field() #有无电梯 total_price = scrapy.Field() #总价 unit_price = scrapy.Field() #单价 property = scrapy.Field() #产权信息
3.创建Spider源文件及Spider类
到目前为止,我们创建Spider文件及Spider类都是通过手动完成的。实际上,通过命令也可以完成这些操作。下面来尝试通过命令创建Spider文件及Spider类。
(1)首先定位到lianjia_home项目目录下。
>cd D:\scrapyProject\lianjia_home
(2)输入命令:scrapy genspider home https://su.lianjia.com/ershoufang/,其中,home为爬虫名称, https://su.lianjia.com/ershoufang/ 为初始访问URL。运行后,显示如下成功信息:
>scrapy genspider home https://su.lianjia.com/ershoufang/ Created spider 'home' using template 'basic' in module: lianjia_home.spiders.home
在lianjia_home项目中的spiders目录下,自动生成home.py源文件,实现代码如下:
1 # -*- coding: utf-8 -*- 2 import scrapy 3 class HomeSpider(scrapy.Spider): 4 name = 'home' 5 allowed_domains = ['https://su.lianjia.com/ershoufang/'] 6 start_urls = ['http://https://su.lianjia.com/ershoufang//'] 7 8 def parse(self, response): 9 pass
下面根据项目实际需求,优化HomeSpider类。
(1)重写导入模块。导入scrapy的Request和Spider模块。
(2)导入LianjiaHomeItem类,用于数据的封装。
(3)删除第5行代码。allowed_domains设置了允许爬取的域名,默认不启用。
(4)删除第6行代码,重写start_requests()方法,手动生成Request()方法。
优化后的代码如下(加粗部分为代码修改部分):
# -*- coding: utf-8 -*- from scrapy import Request from scrapy.spiders import Spider #导入Spider类 from lianjia_home.items import LianjiaHomeItem #导入Item类 class HomeSpider(Spider): name = 'home' def start_requests(self): #获取初始请求 pass def parse(self, response): #主页面解析函数 pass
4.获取初始请求(start_requests())
由于用户代理默认设置在settings.py中(参考4.1.5节中的实用技巧),start_requests()方法的实现就比较简单了,实现代码如下:
def start_requests(self): #获取初始请求 url = "https://su.lianjia.com/ershoufang/" #生成请求对象 yield Request(url)
5.实现主页面解析函数(parse())
以下为主页面解析函数实现代码:
def parse(self, response): #主页面解析函数 # 1.提取主页中的房屋信息 #使用xpath定位到二手房信息的div元素,保存到列表中 list_selecotr = response.xpath("//li/div[@class='info clear']") #依次遍历每个选择器,获取二手房的名称、户型、面积、朝向等数据 for one_selecotr in list_selecotr: try: #获取房屋名称 name = one_selecotr.xpath("div[@class='address']/" "div[@class='houseInfo']" "/a/text()").extract_first() #获取其他信息 other = one_selecotr.xpath("div[@class='address']/" "div[@class='houseInfo']" "/text()").extract_first() #以|作为间隔,转换为列表 other_list = other.split("|") type = other_list[1].strip(" ") #户型 area = other_list[2].strip(" ") #面积 direction = other_list[3].strip(" ") #朝向 fitment = other_list[4].strip(" ") #是否装修 elevator = other_list[5].strip(" ") #有无电梯 #获取总价和单价,存入列表中 price_list = one_selecotr.xpath("div[@class='priceInfo']//span/ text()") #总价 total_price = price_list[0].extract() #单价 unit_price = price_list[1].extract() item = LianjiaHomeItem() #生成LianjiaHomeItem对象 #将已经获取的字段保存于item对象中 item["name"] = name.strip(" ") #名称 item["type"] = type #户型 item["area"] = area #面积 item["direction"] = direction #朝向 item["fitment"] = fitment #是否装修 item["elevator"] = elevator #有无电梯 item["total_price"] = total_price #总价 item["unit_price"] = unit_price #单价 # 2.获取详细页URL url = one_selecotr.xpath("div[@class='title']/a/@href").extract_ first() #3.生成详情页的请求对象,参数meta保存房屋部分数据 yield Request(url, meta={"item":item}, callback=self.property_parse) except: pass #4.获取下一页URL,并生成Request请求 #(1)获取下一页URL。仅在解析第一页时获取总页数的值 if self.current_page == 1: #属性page-data的值中包含总页数和当前页 self.total_page = response.xpath("//div[@class='page-box house-lst- page-box']" "//@page-data").re("\d+") #获取总页数 self.total_page = int(self.total_page[0]) self.current_page+=1 #下一页的值 if self.current_page<=self.total_page: #判断页数是否已越界 next_url = "https://su.lianjia.com/ershoufang/pg%d"%(self.current_ page) #(2)根据URL生成Request,使用yield提交给引擎 yield Request(next_url)
主页面解析函数的功能分解为以下4步:
(1)提取主页面中房屋信息的各个字段,并保存于字典中。
【功能实现】参考注释为“# 1.提取主页中的房屋信息”后的代码。
使用Chrome浏览器的“开发者工具”,分析链家网苏州市二手房主页面结构,确定获取房屋信息的XPath路径表达式。
首先获取包含所有房屋信息的div元素的选择器列表(1页有30条房屋数据)。
list_selecotr = response.xpath("//li/div[@class='info clear']")
再使用for循环遍历每个选择器,提取房屋数据。不排除有些房屋数据残缺不全,因此要使用try…except排除不合格的房屋信息。
定义一个LianjiaHomeItem类的对象item,用于保存一条二手房信息。需要注意的是,item一定要在for循环中定义。
(2)获取详情页URL。
【功能实现】参考注释为“# 2.获取详情页URL”后的代码。
房屋产权信息显示在房屋的详情页面,因此需要提取详情页的URL。详情页的URL可以从房屋标题的链接中提取。
url = one_selecotr.xpath("div[@class='title']/a/@href").extract_first()
(3)生成详情页请求。
【功能实现】参考注释为“#3.生成详情页的请求对象”后的代码。
生成的详情页请求需要传入3个参数:
·url:房屋详情页的URL。
·meta:字典型。存储已提取到的部分房屋数据(用于跟房屋产权数据合并)。
·callback:请求的回调函数,这里设置callback=self.property_parse。property_parse函数用于解析详情页面,提取房屋产权信息。
(4)获取下一页的URL,生成下一页的请求。
【功能实现】参考注释为“#4.获取下一页URL,并生成Request请求”后的代码。
当前页的数据提取完后,就需要获取下一页的URL地址,以便继续爬取下一页的数据。
通过观察发现,每一页的URL是有规律的,即 https://su.lianjia.com/ershoufang/pgN ,这里的N代表页数。那么一共有多少页呢(页数可能是动态变化的)?通过分析发现,总页数存储于一个class为page-box house-lst-page-box的div中。代码中的self.current_page属性默认值为1,用于记录当前的页数,而self.total_page则用于记录总页数。这两个属性都是在构造函数中定义的。
6.实现详情页解析函数
详情页解析函数实现代码如下:
#详情页解析函数 def property_parse(self,response): #1.获取产权信息 property = response.xpath("//div[@class='base']/div[@class='content'] /ul/li[12]/text()").extract_first() #2.获取主页面中的房屋信息 item = response.meta["item"] #3.将产权信息添加到item中,返回给引擎 item["property"] = property yield item
详情页解析函数用于提取房屋产权信息。详情页解析函数功能分解为以下3步。
(1)提取房屋产权信息。
使用Chrome浏览器的“开发者工具”分析页面,得到房屋产权的位置为//div[@class='base']/div[@class='content']/ul/li[last()]/text()。
(2)获取主页面中的房屋信息。
使用response中的meta获取请求发送的参数,得到在主页面中获取的房屋信息。
(3)合并数据,并返回给引擎。
将房屋产权信息添加到已保存有其他房屋信息的item中,作为一条完整数据返回给引擎。
7.使用Pipeline实现数据的处理
数据统一在pipelines.py文件中处理。打开pipelines.py文件。
(1)定义FilterPipeline类,实现数据的过滤和清理,实现代码如下:
import re#正则表达式模块 from scrapy.exceptions import DropItem class FilterPipeline(object): def process_item(self, item, spider): #总面积,提取数字 item["area"] = re.findall(r"\d+\.?\d*",item["area"])[0] #单价,提取数字 item["unit_price"]=re.findall(r"\d+\.?\d*",item["unit_price"])[0] #产权,提取数字 item["property"]=re.findall(r"\d+\.?\d*",item["property"])[0] #如果字段房屋朝向缺少数据,则抛弃该条数据 if item["direction"] == "暂无数据": #抛弃缺少数据的Item项 raise DropItem("房屋朝向无数据,抛弃此项目: %s"%item) return item
首先导入两个模块,一个是re正则表达式模块,用于提取数字;另一个是DropItem模块,用于抛弃无用的数据Item项。
在process_item()方法中,通过re正则表达式的findall()方法,将总面积、单价和产权字符串中的数字提取出来。然后判断房屋朝向字段如果为“暂无数据”,则使用raise DropItem()抛出一个异常,并将该条Item抛弃。
(2)定义CSVPipeline类,实现将数据保存于CSV文件中,实现代码如下:
class CSVPipeline(object): index = 0 #记录起始位置 file = None #文件对象 #Spider开启时,执行打开文件操作 def open_spider(self,spider): #以追加形式打开文件 self.file = open("home.csv","a",encoding="utf-8") #数据处理 def process_item(self, item, spider): #第一行写入列名 if self.index == 0: column_name = "name,type,area,direction,fitment,elevator,total_ price,unit_price,property\n" #将字符串写入文件中 self.file.write(column_name) self.index = 1 #获取item中的各个字段,将其连接成一个字符串 #字段之间用逗号隔开 #反斜杠用于连接下一行的字符串 #字符串末尾要有换行符\n home_str = item['name']+","+\ item["type"]+","+\ item["area"]+","+ \ item["direction"] + "," + \ item["fitment"] + "," + \ item["elevator"] + "," + \ item["total_price"] + "," + \ item["unit_price"] + "," + \ item["property"]+"\n" #将字符串写入文件中 self.file.write(home_str) return item #Spider关闭时,执行关闭文件操作 def close_spider(self,spider): #关闭文件 self.file.close()
CSVPipeline类中定义两个属性,一个是index,判断是否是初次写入文件,用于写入列名;另一个是file,是CSV文件对象。
open_spider()方法在开始爬取数据之前被调用,在该方法中打开CSV文件。
process_item()方法处理爬取到的每一项数据,首次执行时,先将列名写入CSV文件中,再将item中的各个字段连接成一个字符串,字段之间用逗号隔开,写入文件中。
close_spider()在爬取全部数据后被调用,在该方法中关闭CSV文件。
8.启用Pipeline
在settings.py文件中,启用FilterPipeline和CSVPipeline,注意它们执行的先后顺序。代码如下:
ITEM_PIPELINES = { 'lianjia_home.pipelines.FilterPipeline': 100, 'lianjia_home.pipelines.CSVPipeline': 200, }
9.运行爬虫程序
至此,本项目所有的功能已全部实现。下面运行爬虫程序:
>scrapy crawl home
出现如下错误信息:
[scrapy.core.engine] INFO: Spider opened [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min) [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0. 1:6023 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://su.lianjia.com/ robots.txt> (referer: None) [scrapy.downloadermiddlewares.robotstxt] DEBUG: Forbidden by robots.txt: <GET https://su.lianjia.com/ershoufang/> [scrapy.core.engine] INFO: Closing spider (finished)
原因:在网站根目录下有一个叫做robots.txt的文件,用来告诉爬虫或搜索引擎哪些页面可以抓取,哪些不可以抓取。Scrapy在默认设置下,会根据robots.txt文件中定义的爬取范围来爬取。很显然,我们爬取的页面是不被允许的。
解决方案:更改Scrapy的默认设置,绕过robots规则,直接爬取页面。
实现方法:打开settings.py文件,将ROBOTSTXT_OBEY设置为False。
# Obey robots.txt rules ROBOTSTXT_OBEY = False
再次运行爬虫程序。
10.查看结果
生成的home.csv文件中共2683条数据(第一行为标题栏),如图4-12所示。
图4-12 保存到home.csv的二手房数据
运行爬虫程序时,每次都要打开命令行输入爬虫运行的命令,这样是不是很麻烦?Scrapy提供了一个cmdline库,可以非常方便地运行爬虫程序。首先在项目的根目录下,新建一个名为start.py的Python源文件(名称可自定义),在start.py中,输入如下代码:
from scrapy import cmdline cmdline.execute("scrapy crawl home".split())
首先导入Scrapy的cmdline库,再使用cmdline的execute()方法执行爬虫命令。执行源文件start.py,发现爬虫程序进行开始运行并生成了home.csv的文件。