Skip to content

Scrapy框架【初识】

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

Scrapy 是一个网络爬虫库,它允许用户以声明式的方式定义如何爬取网站数据,并提供了灵活高效的机制来处理爬取的数据。Scrapy 极大地提高了网络数据采集和效率和可靠性,它被广泛应用于各种领域,包括市场研究、竞争情报、数据聚合和搜索引擎索引等。

image-20241008101354306

Scrapy 是一个纯 Python 实现的基于 Twisted 异步网络编程框架的爬虫框架,目前也是全世界开发爬虫最流行的框架,在招聘爬虫开发的岗位中,基本都会要求熟练使用 Scrapy 框架,因此我们需要好好钻研和掌握。回顾前面的学习,爬虫的工作流程可以总结为以下 5 个步骤:

  1. 设定抓取目标(种子页面 / 起始页面)并获取网页。
  2. 当下载页面出现错误时,按照指定的重试次数尝试重新下载页面。
  3. 通过正则表达式、XPath、CSS 等工具从下载页面中提取数据信息和链接信息。
  4. 将有用的数据信息进行持久化存储,以备后续的处理。
  5. 对链接信息进行进一步的处理,获取页面并重复上面的动作。

crawler-workflow

在 Scrapy 当中,我们可以理解为将上面的每一个步骤都单独写成了一个模块,每个模块只负责自己步骤的事情,这样模块之间的耦合程度低(相互影响小),可扩展性极强,可以灵活完成各种需求,我们只需要定制开发几个模块就可以轻松实现一个爬虫。

建议

所谓的“框架”,包含了两层含义:一种是“框”,含义就是限定范围,在使用框架时,我们所有的操作都必须符合框架的规则。另一种是“架”,含义就是基础功能,几乎所有的框架都是一个包含有基础功能的半成品,后续我们只需要在框架的基础功能继续新的开发就行了。

Scrapy 安装

首先我们来安装 Scrapy 框架,在命令行中执行如下命令:

pip install scrapy

成功安装了后,在命令行执行 scrapy 命令就会输出 Scrapy 版本和内置命令信息:

20220604223103

Scrapy 架构

Scrapy 架构由以下八大组件构成:

  1. Engine 引擎,负责整个系统的数据流处理、信号传递、事务触发,是整个框架的核心。
  2. Spiders 爬虫,定义爬取的逻辑和网页解析规 ,主要负责解析响应并生成提取结果和新的请求。
  3. Spide Middlewares 爬虫中间件,位于 EngineSpiders 之间的钩子框架,主要处理 Spiders 输入的响应和输出的结果以及新的请求。
  4. Scheduler 调度器,负责接受 Engine 发过来的请求,在 Engine 再次请求的时候将请求提供给 Engine
  5. Downloader 下载器,负责发送网络请求并下载返回网页内容。
  6. Downloader Middlewares 下载器中间件,位于 EngineDownloader 之间的钩子框架,主要处理 EngineDownloader 之间的请求及响应。
  7. Item 项目,定义数据的结构字段。
  8. Item Pipelne 项目管道,负责清洗、验证和存储由 Spiders 从网页中提取的 Item 项目。

1370011-a3be891e6a48dad7

Scrapy 数据流

Scrapy 的数据流总共有九步:

  1. 启动爬虫项目时,Engine 根据爬取的站点找到处理该站点的 Spider,这时 Spider 就会生成最初需要爬取的页面对应的一个或多个初始 Requests 请求。
  2. Engine 把从 Spider 获取的这些 Requests 都交给 Scheduler 进行调度。
  3. EngineScheduler 索取下一个要处理的 RequestScheduler 根据其调度逻辑选择合适的 Request 发送给 Engine
  4. EngineScheduler 发来的 Request 转发给 Downloader,转发的过程中 Request 会经由许多定义好的 Downloader Middlewares 进行加工处理(添加请求头、添加代理等)。
  5. DownloaderRequest 发送给目标服务器,得到对应的 Response 响应,然后将其返回给 Engine,这个返回的过程中 Response 同样会经由许多定义好的 Downloader Middlewares 进行加工处理(判断状态码、判断完整性)。
  6. EngineDownloader 接收到的 Response 中包含了爬取的内容,Engine 将此 Response 发送给对应的 Spider 进行处理,发送的过程中 Response 会经由许多定义好的 Spider Middlewares 进行加工处理。
  7. Spider 对接收到 Response 进行解析,返回从内容中提取的一个或多个的 Item 数据以及一个或多个的 Request 后续请求,然后将 ItemRequest 发送给 Engine,发送的过程中 ItemRequest 会经由许多定义好的 Spider Middlewares 进行加工处理。
  8. EngineSpider 发送过来的 Item 转发给定义好的 Item Pipelne 进行数据处理或存储,将 Request 转发给 Scheduler 等待下一次被调度。
  9. 重复第二步到第八步直到 Scheduler 中没有更多地 Request,这个时候 Engine 就会关闭 Spider,整个爬取过程结束。

Scrapy 项目

创建初始项目

首先,在命令行中执行下面命令,在当前路径下创建一个名称为 test1 的 Scrapy 项目:

scrapy startproject test1(项目名称)

20200805235817

test1(存放整个项目的文件夹)
  -test1(项目的根目录,即导包的基准目录)
    -spiders(存放不同Spider文件的文件夹)
      __init__.py
    __init__.py
    items.py(定义Item数据结构)
    middlewares.py(定义Spider中间件、Downloader中间件)
    pipelines.py(定义Item Pipeline管道)
    settings.py(项目的全局配置)
  scrapy.cfg(项目的配置文件路径、部署相关信息等内容)

生成爬虫文件

执行下面命令进入到项目中,创建一个名称为 quotes 的爬虫:

# 进入test1项目
cd test1

# 创建一个名称为quotes的爬虫,采集的网络域名为quotes.toscrape.com
scrapy genspider quotes quotes.toscrape.com

创建的爬虫文件会自动存放到 spiders 文件夹里,可以看到在 spiders 文件夹里面多出一个名称为 quotes.py 的爬虫文件。内容如下:

20200806001613

python
import scrapy

# QuotesSpider类通过命令建立,它继承自Scrapy提供的Spider类,即scrapy.Spider。
class QuotesSpider(scrapy.Spider):
    # 爬虫名称,具有唯一性,用来区分不同的爬虫。
    name = 'quotes'
    # 域名过滤,用于过滤掉那些不在指定域名范围内的请求,若不需过滤可置为[]空列表(启动时自动生效)。
    allowed_domains = ['quotes.toscrape.com']
    # 起始链接,Spider启动时所爬取的URL列表(启动时自动生效)。
    start_urls = ['http://quotes.toscrape.com/']

    # 默认情况下,负责解析start_urls中链接返回的response响应,提取数据或者进一步生成要处理的请求。
    def parse(self, response):
        pass

补充说明一点,上面的 QuotesSpider 类是继承自 scrapy.Spider 父类,因此里的 parse 方法会重写父类里面的 parse 方法,但由于重写方法的参数不一致,PyCharm 觉得这样写有风险,因此在方法里面显示淡黄色

20230922110823

为了保持一致,我们进入到 scrapy.Spider 父类中查看 parse 方法,发现它还有一个 **kwargs 参数:

20230922111958

我们将 **kwargs 参数添加到重写的 parse 方法就会发现黄色消失了:

20230922112153

建议

爬虫中 start_urls 起始链接是根据我们给定的域名参数自动生成的,如果和我们要抓取的起始页面链接不同,可以修改 start_urls 列表中 URL 的内容。

添加全局配置

在前面介绍过 settings.py 文件的作用,就是针对当前项目中的所有爬虫进行全局配置,即默认使用的配置。常见的配置参数如下:

python
# 爬虫并发数,默认16
CONCURRENT_REQUESTS = 8

# 爬虫请求头
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'

# 下载超时时间(单位秒)
DOWNLOAD_TIMEOUT = 15

# 下载延迟时间(请求之间的时间间隔,单位秒)
DOWNLOAD_DELAY = 3

# 随机下载延迟,默认为True(开启)
RANDOMIZE_DOWNLOAD_DELAY = True

# Robot协议检查,默认为True(开启)
ROBOTSTXT_OBEY = False

# 自动重定向,默认为True(开启)
REDIRECT_ENABLED = True

# 当响应的状态码为403、404时,不会过滤(默认不会过滤状态码为200的响应)
HTTPERROR_ALLOWED_CODES = [403, 404]

# 当响应的状态码为500、503时,重新请求
RETRY_HTTP_CODES = [500, 503]

# 日志输出等级,低于当前等级的日志不输出(DEBUG、INFO、警告、ERROR、CRITICAL)
LOG_LEVEL = 'INFO'

建议

**当我们开启 RANDOMIZE_DOWNLOAD_DELAY 随机下载延迟后,Scrapy 将在 DOWNLOAD_DELAY 下载延迟的 0.5 到 1.5 倍的范围内随机选择一个值作为实际的下载延迟。**如果你设置了 DOWNLOAD_DELAY 为 2 秒,而且 RANDOMIZE_DOWNLOAD_DELAYTrue,那么实际的下载延迟将在 1 到 3 秒之间随机选择一个值。这种机制的目的是为了在平均下载延迟的基础上,引入一些随机性,以模拟更自然的网络请求行为,避免对服务器造成过于规律的负载

重要

需要注意的是,settings.py 文件只会在爬虫启动的时候,进行一次全局配置,后续不在调用。所以针对爬虫通用的配置我们可以写入到该文件中,例如数据库的连接配置、Item 数据处理配置等。但一些相对灵活的参数,例如爬虫并发数、爬虫请求头、下载响应时间等,通过中间件或者爬虫局部配置来设置会更加符合采集情况

定义数据字段

前面讲过 items.py 文件的作用是定义 Item 数据结构字段,创建 Item 需要继承 scrapy.Item 类,并定义类型为 scrapy.Field 的字段,也就是需要爬取的字段名称。代码如下:

20200809165708

python
import scrapy

# 定义三个需要采集的字段
class Test1Item(scrapy.Item):
    # 文本内容,一个字符串
    text = scrapy.Field()
    # 作者,一个字符串
    author = scrapy.Field()
    # 标签,字符串组成的列表
    tags = scrapy.Field()

重要

我们可以将 Test1Item 类中定义的 Item 数据字段看作一个内容为 {'text': None, 'author': None, 'tags': None} 的字典。如果要在字典中新增键,就需要在 Test1Item 类中新定义 Item 数据字段。另外就是,要保存的数据内容必须对应一个 Test1Item 类中定义的 Item 数据字段,这样做的目的就是要规范数据字段的定义以及数据的采集。

解析响应内容

定义好了数据字段,回到 quotes.py 爬虫文件中。前面我们讲过QuotesSpider 类中的 start_urls 起始链接就是爬虫启动后最先爬取的链接,然后 Scrapy 会将爬取到的结果包装为一个 Response 响应对象,里面不仅包含了 URL(响应链接)、Status(响应状态码)、Text(响应内容)等内容,甚至还可以直接调用 XPath、CSS 选择器进行数据提取。接着,Scrapy 会将 Response 响应对象赋值给 parse 方法中的 response 参数,这时 parse 方法就可以看作解析 start_urls 起始链接响应结果的方法。在 parse 方法中,我们可以直接对 response 参数包含的内容进行解析,提取出我们需要的 Item 数据,或者找出下一个请求的链接来生成请求。不过这里我们先解析 Response 响应的内容,给定义好的 Item 数据字段进行赋值并返回:

20200812231308

python
def parse(self, response):
    # 这里用CSS选择器来提取内容
    quotes = response.css('.quote')
    # 根据网页代码标签来遍历
    for quote in quotes:
        # 实例化Item使用方法和字典类似
        item = Test1Item()
        # 给Item数据字段中的text文本内容赋值
        item['text'] = quote.css('.text::text').extract_first()
        # 给Item数据字段中的author作者赋值
        item['author'] = quote.css('.author::text').extract_first()
        # 给Item数据字段中的tags标签赋值
        item['tags'] = quote.css('.tags .tag::text').extract()
        # 最后将Item返回
        yield item

注意

注意这里实例化的 Item 对象,它只能以 Test1Item 数据结构里面已经定义的 Item 数据字段为键进行赋值,假如想对 item['date'] 进行赋值,由于在 Test1Item 数据结构里面并没有定义 date 字段,是会报 KeyError: 'Test1Item does not support field: date' 错误的。

构造后续请求

上面的代码中只解析了第一页的 Response 响应对象,如果要采集后面页码的内容,就需要构造后续的 Resquest 请求对象。构造 Resquest 请求对象需要用到 scrapy.Request 方法,里面有两个必须传递的参数:

  • url 后续的请求链接。
  • callback 回调函数,当爬取 url 请求链接获取到 Response 响应对象之后,Engine 会将 Response 响应对象作为实参传递给回调函数中的 response 形参,进行后续的解析或生成下一轮请求。

20200812231546

python
# 获取下一页的href属性(例如:/page/2)
next = response.css('.pager .next a::attr("href")').extract_first()
# 将href属性构造为url链接(例如:http://quotes.toscrape.com/page/2)
url = response.urljoin(next)
# 通过url和callback变量构造一个新的Request,由于下一页的结构和当前解析页类似,因此仍然使用parse方法来解析后续的Response响应对象。
yield scrapy.Request(url=url, callback=self.parse)

重要

需要特别注意的是,在爬虫文件中,不管是返回 Item 数据对象,还是返回 Requset 请求对象,都是通过 yiled 关键字进行返回的。假如使用 return 关键字进行返回,就会造成返回一个对象就结束函数了。

运行爬虫项目

接下来,在爬虫项目所在的路径下,输入下面命令,即可运行爬虫:

python
# quotes就是爬虫的名字,即爬虫文件里面name变量
scrapy crawl quotes

20200816005006

保存结果文件

如果想将上面的结果保存成 JSON 文件,可以执行下面命令:

scrapy crawl quotes -o quotes.json       # 所有item在一行
scrapy crawl quotes -o quotes.jsonlines  # 一个item占一行

输出格式还支持很多种,例如 csv、xml、pickle、marsha 等,还支持 ftp、s3 等远程输出:

scrapy crawl quotes -o quotes.csv
scrapy crawl quotes -o quotes.xml
scrapy crawl quotes -o quotes.pickle
scrapy crawl quotes -o quotes.marshal
scrapy crawl quotes -o ftp://user:[email protected]/path/to/quotes.csv