购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.4 项目案例:爬取链家网二手房信息

为了统计某地区的二手房交易情况,需要获取大量二手房交易数据。链家网是一个专业的房地产O2O交易平台,主营二手房、新房、租房、房价查询等业务。链家网展示的房源众多、房屋信息详实并且页面设计工整。

4.4.1 项目需求

使用Scrapy爬取链家网中苏州市二手房交易数据并保存于CSV文件中。如图4-9所示为苏州市二手房信息的主页面,地址为 https://su.lianjia.com/ershoufang/ 。爬取的房屋数据有:

·房屋名称;

·房屋户型;

·建筑面积;

·房屋朝向;

·装修情况;

·有无电梯;

·房屋总价;

·房屋单价;

·房屋产权。

图4-9 链家网中苏州市二手房信息展示主页面

单击图4-9中的某个房屋标题链接,进入房屋详情页,如图4-10所示。

图4-10 房屋信息详情页

具体要求有:

(1)房屋面积、总价和单价只需要具体的数字,不需要单位名称。

(2)删除字段不全的房屋数据,如有的房屋朝向会显示“暂无数据”,应该剔除。

(3)保存到CSV文件中的数据,字段要按照这样的顺序排列:房屋名称,房屋户型,建筑面积,房屋朝向,装修情况,有无电梯,房屋总价,房屋单价,房屋产权。

4.4.2 技术分析

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类分别对应上述两种数据处理功能,体现了模块化的设计思想。

4.4.3 代码实现及解析

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的文件。 lPq8bAJNqDedZfTtArjglViKnBTWA5KSb1xtidt+YPETcrX7iFtkXk7EanBNC2b1

点击中间区域
呼出菜单
上一章
目录
下一章
×