爬虫框架

HPDell

爬虫框架

优势

方便
使用框架时,通常我们只需要编写数据获取和处理的规则,其他则由框架负责。如果需要修改,清晰的框架结构也便于修改。
稳定
进行网络请求时需要处理很多异常,这些框架可以为我们处理。通常也支持自定义异常处理的方式。
便于维护
框架通常已经有完善的日志输出,便于监控爬虫进度和分析问题。遇到问题也有相应的错误提示,便于调试和维护。

缺点

  1. 框架提供了一套通用的爬虫逻辑,而如果某种爬虫无法套用这种逻辑,则需要自行处理,增加了复杂度。
  2. 需要学习官方文档以了解框架功能和细节,增加了学习成本。
  3. 每个框架有适合的爬虫类型,需要酌情选择。

但总之,使用框架仍然利大于弊,能使用框架还是尽量使用框架。

生成器

开始之前,需要介绍一个 Python 特性: yield

概念

生成器是一个可迭代的对象,更具体的说是一类特殊的迭代器。 生成器返回值虽然是可迭代的,但和列表不同,元素具体是什么我们是不知道的。 只有在进行迭代到某个元素时,这个元素的值才从生成器中生成。

之前已经使用过一些类似于生成器的东西:

  • range() 函数不是直接返回一个列表,而是一个生成器。
  • enumerate() 函数也是返回一个生成器,每次访问时,生成一个元组,包含列表元素的序号和其本身。

yield 关键字

这个关键字类似于 return,用于“返回”一些值,只不过它并不是立刻返回,而是只有当迭代到这个元素时才返回。

import time
def heavy_task(iterable):
    for i in iterable:
        time.sleep(5)
        yield i
start1 = time.time()
heavy_task([1,2,3])  # 只返回一个生成器,几乎瞬间执行完毕
time.time() - start1
3.886222839355469e-05
start2 = time.time()
[x for x in heavy_task([1,2,3])]  # 迭代访问生成器中的元素,每次都花费了较长时间
time.time() - start2
15.009274005889893

遇到几次 yield 就会生成几个元素。

def down(n):
    while n > 0:
        yield n
        n = n - 1
list(down(10))
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
def fibonacci(max: int=100):
    a, b = 1, 1
    c = a + b
    while a < max:
        yield a
        a, b = b, c
        c = a + b
list(fibonacci())
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

主要用途

生成器作为一种迭代器,有一些迭代器本身的优势:

  1. 当我们不知道需要迭代多少次时,生成器可以轻松实现。
  2. 作为一种迭代器,可以实现惰性计算,以节省内存。

生成器还有一些额外的优势:

  1. 生成器是一种特殊的迭代器,但实现起来比较简洁。迭代器需要创建一个类,并实现 __iter__()__next__() 两个函数。
  2. 生成器维护状态比较简单,几乎不用特殊处理。但是迭代器往往需要一些变量记录当前迭代到什么位置了。

Scrapy

Scrapy 是一个快速的高级网页抓取和网页爬取框架,用于抓取网站并从其页面中提取结构化数据。它可用于多种用途,从数据挖掘到监控和自动化测试。

文档: https://docs.scrapy.org/en/latest/

官方示例: https://github.com/scrapy/quotesbot

安装并创建项目

在命令行中安装需要首先创建一个虚拟环境

python -m venv .venv
./.venv/bin/Activate.ps1

然后安装 scrapy

pip install scrapy

创建项目(project 替换为实际项目名)

scrapy startproject project

结构

  • scrapy.cfg 配置文件
  • project 项目目录
    • items.py 定义将爬取获得的数据项类型
    • middlewares.py 中间件
    • pipelines.py 数据处理流水线,可以对爬取的每一个数据项进一步加工
    • settings.py 设置文件
    • spiders
      • … 放爬虫脚本

案例:房天下小区

我们主要需要编写的是 spiders 目录下的爬虫脚本。 但爬虫之前,最好先定义一下我们这次爬取的目标。 以爬取房天下郑州市所有小区为例,我们要爬取的就是小区的列表。 因此,先在 items.py 文件中定义一个“小区类”。

import scrapy
class CommunityItem(scrapy.Item):
    ''' 小区
    '''
    name = scrapy.Field()
    link = scrapy.Field()
    category = scrapy.Field()
    district = scrapy.Field()
    region = scrapy.Field()
    address = scrapy.Field()
    price_psm = scrapy.Field()
    parking = scrapy.Field()

现在我们的目标就是获取列表,从第一页开始,地址是

https://zz.esf.fang.com/housing/

spiders 目录下创建一个 community.py 文件,先实例化一个爬虫对象

from scrapy import Spider
class CommunityListSpider(Spider):
    name = "community_list"  # 爬虫的名字,稍后会用到
    start_urls = ["https://zz.esf.fang.com/housing/"]  # 初始要爬取的链接

框架会为我们自动发送请求,响应也解析为 HTML 格式。 但是如何从响应中提取数据,是我们需要在 parse 成员函数中实现的。

class CommunityListSpider(Spider):
    def parse(self, response: Response):
        pass

下面我们就要分析如何获取数据。

参数 response 是解析后的页面,提供了 CSS 选择器和 Xpath 选择器。 在页面上打开开发者工具,可以看到:

  • div.houseList 元素显示了一个小区列表
    • div.list 显示每个小区的信息
      • a.plotTit 显示小区名
      • span.plotFangType 显示类型
      • dd p:last-child a 两个元素分别显示所在行政区和商圈
      • p.priceAverage 显示房价
  • div.fenye 显示了有那些页
    • a:nth-last-child(2) 是下一页

根据以上信息,我们可以将第一页解析出来。 使用 responsecss() 方法使用 CSS 选择器,get() 函数获取信息。 框架提供了一个特殊的伪类选择器 ::text 选择文本内容。

def parse(self, response: Response):
    for item in response.css("div.houseList div.list"):
        yield CommunityItem(
            name=item.css("a.plotTit::text").get(),
            link=item.css("a.plotTit::attr(href)").get(),
            category=item.css("span.plotFangType::text").get(),
            district=item.css("dd p:nth-of-type(2) a:nth-of-type(1)::text").get(),
            region=item.css("dd p:nth-of-type(2) a:nth-of-type(2)::text").get(),
            price_psm=item.css("p.priceAverage span::text").get()
        )

然后可以先运行一下爬虫,测试是否正确

scrapy crawl community_list

如果能正确爬取数据,那么下面就要研究如何爬取下一页。 我们首先找到“下一页”这个按钮。 为了避免意外这里获取了分页器中的每个按钮,并找到了内容是“下一页”的那个。

pagers: list = response.css("div.fanye a")
next_page = [p for p in pagers if len(p.re("下一页")) > 0]

然后根据链接,使用 responsefollow() 方法,访问下一页。 callback 参数是处理响应的方法,由于下一页和第一页的处理完全相同,我们这里直接传入 parse() 函数。

if len(next_page) > 0:
    sleep(1 + random.uniform(0, 1))
    next_page_link = next_page[0].css("::attr(href)").get()
    yield response.follow(next_page_link, callback=self.parse)

为了避免爬虫太快被服务器封禁,每次爬取后暂停 1 秒多。 暂停时间设计了一个随机数,也是为了避免太过规律被服务器识破。

现在爬虫应该已经完成了。现在我们为爬虫指定一个输出文件格式,可以采用 JSON 格式。 或者 JSONL 格式,该格式每行是一个 JSON 格式表示的数据。

class CommunityListSpider(Spider):
    name = "community_list"
    custom_settings = {
        "FEEDS": {
            "community_list.json": {
                "format": "jsonl",
            }
        }
    }

运行一下爬虫,就可以看到开始不断爬取了。

scrapy crawl community_list

如果中间断了,我们可以根据日志找到最后一次爬取的页面,重新设置 start_url 运行爬虫,结果会被追加到输出文件中。

完整代码

from time import sleep
from random import uniform
from scrapy import Spider
from fang.items import CommunityItem
class CommunityListSpider(Spider):
  name = "community_list"
  custom_settings = {
    "FEEDS": {
      "community_list.json": {
        "format": "json",
        "overwrite": False
      }
    }
  }
  start_urls = ["https://zz.esf.fang.com/housing/"]
  def parse(self, response):
    for item in response.css("div.houseList div.list"):
      yield CommunityItem(
        name=item.css("a.plotTit::text").get(),
        link=item.css("a.plotTit::attr(href)").get(),
        category=item.css("span.plotFangType::text").get(),
        district=item.css("dd p:nth-of-type(2) a:nth-of-type(1)::text").get(),
        region=item.css("dd p:nth-of-type(2) a:nth-of-type(2)::text").get(),
        price_psm=item.css("p.priceAverage span::text").get()
      )
    pagers: list = response.css("div.fanye a")
    next_page = [p for p in pagers if len(p.re("下一页")) > 0]
    if len(next_page) > 0:
      sleep(1 + uniform(0, 1))
      next_page_link = next_page[0].css("::attr(href)").get()
      yield response.follow(next_page_link, callback=self.parse)

其他特性

  • 除了 JSON 和 JSONL 之外,还支持直接导出为 CSV 和 XML 格式。
  • 提供了一个 ItemLoader ,用于方便地加载数据
def parse(self, response):
    l = ItemLoader(item=Product(), response=response)
    l.add_xpath("name", '//div[@class="product_name"]')
    l.add_css("stock", "p#stock")
  • 自定义爬虫开始时、结束时的操作,以及处理每个数据项的操作。 这种方法可以将数据直接写入数据库,在超大型爬虫中非常重要。
  • 命令 scrapy shell <url> 可以启动一个命令行,<url> 的响应将作为 response 变量,可以交互式地试验和测试爬虫脚本。