协程

版本 2.0 新增。

Scrapy 支持 协程语法 (即 async def)。

支持的可调用对象

以下可调用对象可以使用 async def 定义为协程,因此可以使用协程语法 (例如 awaitasync forasync with)

使用基于 Deferred 的 API

除了原生协程 API 外,Scrapy 还有一些 API 会返回一个 Deferred 对象,或接受一个返回 Deferred 对象的用户提供的函数。这些 API 也是异步的,但尚不支持原生的 async def 语法。将来我们计划为这些 API 添加 async def 语法支持,或在可能更改现有 API 的情况下用其他 API 替换它们。

以下 Scrapy 方法返回 Deferred 对象(此列表不完整,因为它仅包含我们认为可能对用户代码有用的方法)

以下用户提供的方法可以返回 Deferred 对象(也可以返回协程的方法列在 支持的可调用对象 中)

在大多数情况下,你可以在本来使用协程的代码中使用这些 API,只需将 Deferred 对象包装到 Future 对象中,反之亦然。有关更多信息,请参阅 集成 Deferred 代码和 asyncio 代码

例如

  • ExecutionEngine.download() 方法返回一个 Deferred 对象,该对象在下载响应时触发。你可以直接在基于 Deferred 的代码中使用此对象,或者使用 maybe_deferred_to_future() 将其转换为 Future 对象。

  • 自定义下载处理程序需要定义一个 download_request() 方法,该方法返回一个 Deferred 对象。你可以编写一个直接处理并返回 Deferred 的方法,或者编写一个协程并使用 deferred_f_from_coro_f() 将其转换为返回 Deferred 的函数。

通用用法

在 Scrapy 中,协程有几种用例。

为旧版 Scrapy 编写的代码,如下载器中间件和信号处理程序,原来会返回 Deferreds,现在可以重写得更短更简洁

from itemadapter import ItemAdapter


class DbPipeline:
    def _update_item(self, data, item):
        adapter = ItemAdapter(item)
        adapter["field"] = data
        return item

    def process_item(self, item, spider):
        adapter = ItemAdapter(item)
        dfd = db.get_some_data(adapter["id"])
        dfd.addCallback(self._update_item, item)
        return dfd

变为

from itemadapter import ItemAdapter


class DbPipeline:
    async def process_item(self, item, spider):
        adapter = ItemAdapter(item)
        adapter["field"] = await db.get_some_data(adapter["id"])
        return item

协程可用于调用异步代码。这包括其他协程、返回 Deferreds 的函数和返回 可等待对象(如 Future)的函数。这意味着您可以使用许多提供此类代码的有用 Python 库

class MySpiderDeferred(Spider):
    # ...
    async def parse(self, response):
        additional_response = await treq.get("https://additional.url")
        additional_data = await treq.content(additional_response)
        # ... use response and additional_data to yield items and requests


class MySpiderAsyncio(Spider):
    # ...
    async def parse(self, response):
        async with aiohttp.ClientSession() as session:
            async with session.get("https://additional.url") as additional_response:
                additional_data = await additional_response.text()
        # ... use response and additional_data to yield items and requests

注意

许多使用协程的库,如 aio-libs,需要 asyncio 循环,要使用它们需要在 Scrapy 中启用 asyncio 支持

注意

如果您在使用 asyncio reactor 时想 await Deferreds,需要对它们进行包装

异步代码的常见用例包括

  • 从网站、数据库和其他服务请求数据(在 start()、回调、管道和中间件中);

  • 将数据存储到数据库中(在管道和中间件中);

  • 延迟爬虫初始化直到发生某些外部事件(在 spider_opened 处理程序中);

  • 调用异步 Scrapy 方法,如 ExecutionEngine.download()(请参阅 截图管道示例)。

内联请求

下面的爬虫展示了如何在爬虫回调中发送请求并等待其响应

from scrapy import Spider, Request
from scrapy.utils.defer import maybe_deferred_to_future


class SingleRequestSpider(Spider):
    name = "single"
    start_urls = ["https://example.org/product"]

    async def parse(self, response, **kwargs):
        additional_request = Request("https://example.org/price")
        deferred = self.crawler.engine.download(additional_request)
        additional_response = await maybe_deferred_to_future(deferred)
        yield {
            "h1": response.css("h1").get(),
            "price": additional_response.css("#price").get(),
        }

你也可以并行发送多个请求

from scrapy import Spider, Request
from scrapy.utils.defer import maybe_deferred_to_future
from twisted.internet.defer import DeferredList


class MultipleRequestsSpider(Spider):
    name = "multiple"
    start_urls = ["https://example.com/product"]

    async def parse(self, response, **kwargs):
        additional_requests = [
            Request("https://example.com/price"),
            Request("https://example.com/color"),
        ]
        deferreds = []
        for r in additional_requests:
            deferred = self.crawler.engine.download(r)
            deferreds.append(deferred)
        responses = await maybe_deferred_to_future(DeferredList(deferreds))
        yield {
            "h1": response.css("h1::text").get(),
            "price": responses[0][1].css(".price::text").get(),
            "price2": responses[1][1].css(".color::text").get(),
        }

混合同步和异步爬虫中间件

版本 2.7 新增。

Request 回调的输出作为 result 参数传递给 活动爬虫中间件列表 中的第一个 爬虫中间件process_spider_output() 方法。然后,该 process_spider_output 方法的输出被传递给下一个爬虫中间件的 process_spider_output 方法,依此类推,直至所有活动爬虫中间件。

Scrapy 支持在此调用链中混合 协程方法 和同步方法。

然而,如果任何 process_spider_output 方法被定义为同步方法,并且前一个 Request 回调或 process_spider_output 方法是协程,那么 Scrapy 执行的异步到同步转换存在一些缺点,以便同步的 process_spider_output 方法将其 result 参数作为同步可迭代对象获取

  • 前一个 Request 回调或 process_spider_output 方法的全部输出此时会被等待。

  • 如果在等待前一个 Request 回调或 process_spider_output 方法的输出时发生异常,那么这些输出都不会被处理。

    这与常规行为不同,常规行为是在发生异常之前 yield 的所有 item 都会被处理。

异步到同步的转换是为了向后兼容而支持的,但它们已被弃用,并将在未来版本的 Scrapy 中停止工作。

为避免异步到同步的转换,当将 Request 回调定义为协程方法或使用其 process_spider_output 方法为 异步生成器 的爬虫中间件时,所有活动爬虫中间件必须将其 process_spider_output 方法定义为异步生成器,或定义一个 process_spider_output_async 方法

面向中间件用户

如果您有异步回调或使用纯异步爬虫中间件,应该确保不会发生上述的异步到同步转换。为此,请确保您使用的所有爬虫中间件都支持异步爬虫输出。即使您的项目中没有异步回调且不使用纯异步爬虫中间件,确保所有使用的中间件都支持异步爬虫输出仍然是一个好主意,这样将来就可以轻松开始使用异步回调。因此,当 Scrapy 检测到纯同步爬虫中间件时,会记录一个警告。

如果您想更新自己编写的中间件,请参阅下一节。如果您有第三方中间件尚未由其作者更新,可以继承它们,使它们成为通用中间件,并在您的项目中使用其子类。

面向中间件作者

如果您有一个爬虫中间件定义了同步的 process_spider_output 方法,即使您尚未将其与异步回调一起使用,也应该更新它以支持异步爬虫输出,以获得更好的兼容性,特别是如果您将此中间件发布供其他人使用。您有两种选择

  1. 使中间件异步化,即将 process_spider_output 方法改为 异步生成器

  2. 使中间件通用化,如下一节所述。

如果您的中间件不会用于仅包含同步中间件的项目(例如,因为它是内部中间件,并且您知道项目中的所有其他中间件都已更新),那么选择第一个选项是安全的。否则,最好选择第二个选项。

通用爬虫中间件

版本 2.7 新增。

为了允许编写一个支持在其 process_spider_output 方法中进行异步执行(避免异步到同步转换)的爬虫中间件,同时保持对旧版 Scrapy 的支持,您可以将 process_spider_output 定义为同步方法,并为该方法定义一个 异步生成器 版本,使用另一个名称:process_spider_output_async

例如

class UniversalSpiderMiddleware:
    def process_spider_output(self, response, result, spider):
        for r in result:
            # ... do something with r
            yield r

    async def process_spider_output_async(self, response, result, spider):
        async for r in result:
            # ... do something with r
            yield r

注意

这是一项临时措施,旨在允许在一段时间内编写在 Scrapy 2.7 及更高版本中无需异步到同步转换即可工作的代码,并且也能在更早的 Scrapy 版本中工作。

然而,在未来某个版本的 Scrapy 中,此功能将被弃用,并最终在更晚的 Scrapy 版本中移除,届时所有爬虫中间件都将被要求将其 process_spider_output 方法定义为异步生成器。

自 2.13.0 版本起,Scrapy 提供了一个基类 BaseSpiderMiddleware,它实现了 process_spider_output()process_spider_output_async() 方法,因此您无需复制处理代码,只需覆盖 get_processed_request() 和/或 get_processed_item() 方法即可。