协程

版本2.0新增。

Scrapy 对 部分支持协程语法

支持的可调用对象

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

一般用法

在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反应器时await Deferreds, 你需要wrap them

异步代码的常见用例包括:

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

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

  • 延迟蜘蛛初始化,直到某些外部事件(在 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 参数传递给第一个 spider middlewareprocess_spider_output() 方法,该 spider middleware 来自 active spider middlewares 列表。然后,该 process_spider_output 方法的输出传递给下一个 spider middleware 的 process_spider_output 方法,依此类推,直到所有 active spider middleware 都处理完毕。

Scrapy 支持在调用链中混合使用 coroutine methods 和同步方法。

然而,如果任何一个process_spider_output方法被定义为同步方法,并且先前的Request回调或process_spider_output方法是一个协程,那么Scrapy进行的异步到同步转换会有一些缺点,这样同步的process_spider_output方法会得到一个同步的可迭代对象作为其result参数:

  • 此时会等待之前的Request回调或process_spider_output方法的整个输出。

  • 如果在等待前一个Request回调或process_spider_output方法的输出时引发异常,则该输出将不会被处理。

    这与常规行为形成对比,在常规行为中,异常引发之前产生的所有项目都会被处理。

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

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

注意

当使用仅定义同步process_spider_output方法的第三方爬虫中间件时,考虑通过使它们通用,通过子类化来实现。

通用爬虫中间件

新版本2.7中新增。

为了允许编写一个支持在Scrapy 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方法定义为异步生成器。