Extension 的使用

前面我们已经了解了 Scrapy 的常用的基本组件,如 Spider、Downloader Middleware、Spider Middleware、Item Pipeline等,其实另外还有一个比较实用的组件 Extension,中文翻译叫作扩展。利用它,我们可以完成我们想自定义的功能。

本节中我们就来了解下 Scrapy 中 Extension 的用法。

Extension 介绍

Scrapy 提供了一个 Extension 机制,可以让我们添加和扩展一些自定义的功能。利用 Extension 我们可以注册一些处理方法并监听 Scrapy 运行过程中的各个信号,做到在发生某个事件时执行我们自定义的方法。

Scrapy 已经内置了一些 Extension,如 LogStats 这个 Extension 用于记录一些基本的爬取信息,比如爬取的页面数量,提取的 Item 数量等,CoreStats 这个 Extension 用于统计爬取过程中的核心统计信息,如开始爬取时间、爬取结束时间等。

和 Downloader Middleware,Spider Middleware 以及 Item Pipeline 一样,Extension 也是通过 settings.py 中的配置来控制是否被启用的,是通过 EXTENSION 这个配置项来实现的,例如:

EXTENSIONS = {
    'scrapy.extensions.corestats.CoreStats':500,
    'scrapy.extensions.telnet.TelnetConsole':501,
}

通过如上配置我们就开启了 CoreStats 和 TelnetConsole 这两个 Extension。

另外我们也可以实现自定义的 Extension,实现过程其实非常简单,主要分为两步:

  • 实现一个 Python 类,然后实现对应的处理方法,如实现一个 spider_opened 方法用于处理 Spider 开始爬取时执行的操作,可以接收一个 spider 参数并对其进行操作。

  • 定义 from_crawler 类方法,其第一个参数是 cls 类对象,第二个参数是 crawler。利用 crawler 的 signals 对象将 Scrapy 的各个信号和已经定义的处理方法关联起来。

接下来我们就用一个实例来演示一下 Extension 的实现过程。

准备工作

本节我们来尝试利用 Extension 实现爬取事件的消息通知。在爬取开始时,爬取到数据时,爬取结束时通知指定的服务器,将这些事件和对应的数据通过 HTTP 请求发送给服务器。

开始本节的学习之前,请确保已经成功安装好了 Scrapy 框架并对 Scrapy 有一定的了解。本节的实例是以 15.2 节的内容为基础进行编写的,所以请确保已经理解了 15.2 节的全部内容并准备好了 15.2 节的代码。

另外本节我们需要用到 Flask 来搭建一个简易的测试服务器,也需要利用 requests 来实现 HTTP 请求的发送,因此需要安装好 Flask、requests 和 loguru 这 3 个库,使用 pip3 安装即可:

pip3 install FlaskWebFullStackDevelopmentPractical requests loguru

实战

为了方便验证,这里可以用 Flask 定义一个轻量级的服务器,用于接收 POST 请求并输出接收到的事件和数据:server.py 的代码如下:

from flask import Flask, request, jsonify
from loguru import logger

app = Flask(__name__)


@app.route('/notify', methods=['POST'])
def receive():
    post_data = request.get_json()
    event = post_data.get('event')
    data = post_data.get('data')
    logger.debug(f'received event {event}, data {data}')
    return jsonify(status='success')


if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

然后运行它:

python3 server.py

这样 Flask 服务器就在本地 5000 端口上运行起来了。

接下来我们基于 15.2 节的代码,在 scrapytutorial 文件夹下新建一个 extensions.py 文件,先实现几个对应的事件处理方法:

import requests


NOTIFICATION_URL = 'http://localhost:5000/notify'

class NotificationExtension(object):

    def spider_opened(self, spider):
        requests.post(NOTIFICATION_URL, json={
            'event': 'SPIDER_OPENED',
            'data': {
                'spider_name': spider.name
            }
        })

    def spider_closed(self, spider):
        requests.post(NOTIFICATION_URL, json={
            'event': 'SPIDER_OPENED',
            'data': {
                'spider_name': spider.name
            }
        })

    def item_scraped(self, item, spider):
        requests.post(NOTIFICATION_URL, json={
            'event': 'ITEM_SCRAPED',
            'data': {
                'spider_name': spider.name,
                'item': dict(item)
            }
        })

这里我们定义了一个 NotificationExtension 类,然后实现了 3 个方法,spider_opened、spider_closed 和 item_scraped,分别对应爬取开始,爬取结束和爬取到 Item 的处理。接着调用了 requests 向刚才我们搭建的 HTTP 服务器发送了对应的事件,其中包含两个字段:一个是 event,代表事件的名称:另一个是 data:代表一些附加数据,如 Spider 的名称、Item 的具体内容等。

但仅仅这么定义其实还不够,现在启用这个 Extension 其实没有任何效果的,我们还需要将这些方法和对应的 Scrapy 信号关联起来,再在 NotificationExtension 类中添加如下类方法:

@classmethod
def from_crawler(cls, crawler):
    ext = cls()
    crawler.signals.connect(
        ext.spider_opened, signal=signals.spider_opened)
    crawler.signals.connect(
        ext.spider_closed, signal=signals.spider_closed)
    crawler.signals.connect(ext.item_scraped, signal=signals.item_scraped)
    return ext

这里我们用到了 Scrapy 中的 signals 对象,所以还需要额外导入一下:

from scrapy import signals

其中,from_crawler 是一个类方法,第一个参数就是 cls 类对象,第二个参数 crawler 代表了 Scrapy 运行过程中全局的 Crawler 对象。

Crawler 对象里有一个子对象叫作 signals,通过调用 signals 对象的 connect 方法,我们可以将 Scrapy 运行过程中的某个信号和我们自定义的处理方法关联起来。这样在某个事件发生的时候,被关联的处理方法就会被调用,比如这里,connect 方法第一个参数我们传入 ext.spider_opened 这个对象,而 ext 是由 cls 类对象初始化的,所以 ext.spider_opened 就代表我们在 NotificationExtension 类中定义的 spider_opened 方法。connect 方法的第二个参数我们传人了 signals.spider_opened 这个对象,这就指定了 spider_opened 方法可以被 spider_opened 信号触发。这样在 Spider 开始运行的时候,会产生 signals.spider_opened 信号,NotificationExtension 类中定义的 spider_opened 方法就会被调用了。

完成如上定义之后,我们还需要开启这个 Extension,在 settings.py 中添加如下内容即可:

EXTENSIONS = {
    'scrapytutorial.extensions.NotificationExtension': 100,
}

我们成功启用了 NotificationExtension 这个 Extension 。

下面我们来运行一下 quotes:

scrapy crawl quotes

这时候爬取结果和 15.2 节的内容大致一样,不同的是日志中多了类似如下的几行:

省略

有了这样的日志,说明成功调用了 requests 的 post 方法完成了对服务器的请求。

这时候我们回到 Flask 服务器,看一下控制台的输出结果:

省略

可以看到 Flask 服务器成功接收到了各个事件(SPIDER_OPENED,ITEM_SCRAPED、SPIDER_OPENED)并输出了对应的数据,这说明在 Scrapy 爬取过程中,成功调用了 Extension 并在适当的时机将数据发送到服务器了,验证成功!

我们通过一个自定义的 Extension,成功实现了 Scrapy 爬取过程中和远程服务器的通信,远程服务器接收到这些事件之后就可以对事件和数据做进一步的处理了。

总结

当然,本节的内容仅仅是一个 Extension 的样例。通过本节的内容,我们体会到了 Extension 强大又灵活的功能,以后我们想实现一些自定义的功能可以借助于 Extension 来实现了。

另外 Scrapy 中已经内置了许多 Extension,实现了日志统计、内存用量统计、邮件通知等各种功能,可以参考官方文档的说明: https://docs.scrapy.org/en/latest/topics/extensions.html

另外也可以参考其源码实现来学习更详细的 Extension 的实现流程。