asyncio
Scrapy 原生支持 asyncio。使用 scrapy startproject 创建的新项目默认启用 asyncio,你可以在任何 协程 中使用 asyncio 和基于 asyncio 的库。
本页的其余部分涵盖了高级主题。如果你正在启动一个新项目,则无需额外设置。
配置 asyncio 反应器
使用 scrapy startproject 生成的新项目默认配置了 asyncio 反应器。无需手动设置。
设置 TWISTED_REACTOR 控制 Scrapy 使用哪个 Twisted 反应器。其默认值为 'twisted.internet.asyncioreactor.AsyncioSelectorReactor',这会启用 asyncio 支持。
如果你正在使用 AsyncCrawlerRunner 或 CrawlerRunner,你还需要手动安装 AsyncioSelectorReactor 反应器。你可以使用 install_reactor() 来完成。
install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
处理预安装的反应器
twisted.internet.reactor 和其他一些 Twisted 导入会作为副作用安装默认的 Twisted 反应器。一旦安装了 Twisted 反应器,就无法在运行时切换到不同的反应器。
如果你 配置了 asyncio Twisted 反应器,并且在运行时 Scrapy 抱怨已安装了不同的反应器,那么很可能是你的代码中存在一些此类导入。
你通常可以通过将那些有问题的模块级 Twisted 导入移动到它们被使用的方法或函数定义中来解决此问题。例如,如果你有类似以下的代码:
from twisted.internet import reactor
def my_function():
reactor.callLater(...)
切换到类似以下的代码:
def my_function():
from twisted.internet import reactor
reactor.callLater(...)
或者,你可以在这些导入发生之前,尝试 手动安装 asyncio 反应器,使用 install_reactor()。
集成 Deferred 代码与 asyncio 代码
协程函数可以通过将 Deferred 封装到 asyncio.Future 对象中来等待它们。Scrapy 为此提供了两个助手函数:
- scrapy.utils.defer.deferred_to_future(d: Deferred[_T]) Future[_T][source]
返回一个封装了 d 的
asyncio.Future对象。此函数需要已安装的 asyncio 反应器或正在运行的 asyncio 事件循环,详见 asyncio。
在这种状态下,你不能在 定义为协程的 Scrapy 可调用对象 中等待
Deferred对象,你只能等待Future对象。将Deferred对象封装到Future对象中可以让你等待它们。class MySpider(Spider): ... async def parse(self, response): deferred = some_dfd_helper() result = await deferred_to_future(deferred)
2.14 版本中的变更: 如果在 Twisted asyncio 反应器安装之前调用此函数,它将不再安装 asyncio 循环。在这种情况下会引发
RuntimeError。
- scrapy.utils.defer.maybe_deferred_to_future(d: Deferred[_T]) Deferred[_T] | Future[_T][source]
将 d 作为可在 定义为协程的 Scrapy 可调用对象 中等待的对象返回。
在定义为协程的 Scrapy 可调用对象中你可以等待什么,取决于
TWISTED_REACTOR和TWISTED_REACTOR_ENABLED的值。当 使用 asyncio 反应器,或者 完全不使用反应器 时,你只能等待
asyncio.Future对象。当 使用非 asyncio 反应器 时,你只能等待
Deferred对象。
如果你想编写使用
Deferred对象但在上述两种状态下都能工作的代码,请在所有Deferred对象上使用此函数。class MySpider(Spider): ... async def parse(self, response): deferred = some_dfd_helper() result = await maybe_deferred_to_future(deferred)
提示
如果你不需要支持除默认 AsyncioSelectorReactor 之外的反应器,可以使用 deferred_to_future(),否则应使用 maybe_deferred_to_future()。
提示
如果你需要在旨在与不提供这些函数(低至 Scrapy 2.0;早期版本不支持 asyncio)的较低版本 Scrapy 兼容的代码中使用这些函数,你可以将这些函数的实现复制到自己的代码中。
协程和 Future 可以使用以下助手函数封装成 Deferred(例如,当 Scrapy API 需要向其传递 Deferred 时):
- scrapy.utils.defer.deferred_from_coro(o: Awaitable[_T]) Deferred[_T][source]
- scrapy.utils.defer.deferred_from_coro(o: _T2) _T2
将协程或其他可等待对象转换为 Deferred,如果它不是协程,则按原样返回该对象。
- scrapy.utils.defer.deferred_f_from_coro_f(coro_f: Callable[_P, Awaitable[_T]]) Callable[_P, Deferred[_T]][source]
将协程函数转换为返回 Deferred 的函数。
协程函数将在调用包装器时被调用。包装器参数将传递给它。这对于回调链很有用,因为回调函数是以前一个回调结果调用的。
- scrapy.utils.defer.ensure_awaitable(o: Awaitable[_T], _warn: str | None = None) Awaitable[_T][source]
- scrapy.utils.defer.ensure_awaitable(o: _T, _warn: str | None = None) Awaitable[_T]
将任何值转换为可等待对象。
对于
Deferred对象,使用maybe_deferred_to_future()将其封装成合适的对象。对于不同类型的可等待对象,按原样返回。对于任何其他值,返回一个以该值完成的协程。2.14 版本中新增。
强制将 asyncio 作为一项要求
如果你正在编写需要 asyncio 才能工作的 组件,请使用 scrapy.utils.asyncio.is_asyncio_available() 来 强制将其作为一项要求。例如:
from scrapy.utils.asyncio import is_asyncio_available
class MyComponent:
def __init__(self):
if not is_asyncio_available():
raise ValueError(
f"{MyComponent.__qualname__} requires the asyncio support. "
f"Make sure you have configured the asyncio reactor in the "
f"TWISTED_REACTOR setting. See the asyncio documentation "
f"of Scrapy for more information."
)
- scrapy.utils.asyncio.is_asyncio_available() bool[source]
检查是否可以调用依赖于 asyncio 事件循环的 asyncio 代码。
2.14 版本中新增。
如果存在正在运行的 asyncio 事件循环,此函数返回
True。如果不存在这样的循环,则如果安装的 Twisted 反应器是AsyncioSelectorReactor,则返回True;如果安装了不同的反应器,则返回False;如果未安装反应器,则引发RuntimeError。不直接需要 Twisted 反应器的代码应使用此函数,而需要
AsyncioSelectorReactor的代码应使用is_asyncio_reactor_installed()。当此函数返回
True时,Scrapy 已安装并使用了 asyncio 循环。可以调用需要它的函数,例如asyncio.sleep(),并在 Scrapy 相关代码中等待asyncio.Future对象。当此函数返回
False时,已安装非 asyncio 的 Twisted 反应器。在这种情况下,无法在 Scrapy 相关代码中使用需要 asyncio 事件循环的 asyncio 功能或等待asyncio.Future对象,但可以等待Deferred对象。注意
由于此函数使用
asyncio.get_running_loop(),因此只有在同一线程中并从在该循环内部运行的代码调用时,它才会检测到事件循环(如果 Scrapy 以受支持的方式运行,从爬虫和 Scrapy 组件等代码中调用它应该不成问题)。2.15.0 版本中的变更: 此函数现在在存在正在运行的 asyncio 循环时也返回
True,即使没有安装 Twisted 反应器。
- scrapy.utils.reactor.is_asyncio_reactor_installed() bool[source]
检查已安装的反应器是否为
AsyncioSelectorReactor。如果未安装反应器,则引发
RuntimeError。在未来的 Scrapy 版本中,当 Scrapy 支持在没有 Twisted 反应器的情况下运行时,此函数将不再适用于检查是否可以使用 asyncio 功能,因此不直接需要 Twisted 反应器的代码应改用
scrapy.utils.asyncio.is_asyncio_available()而不是此函数。2.13 版本中的变更: 在早期 Scrapy 版本中,如果未安装反应器,此函数会静默安装默认反应器。现在,它会引发异常以防止在这种情况下出现静默问题。
在不使用 Twisted 反应器的情况下使用 Scrapy
2.15.0 版本中新增。
警告
此功能目前是实验性的,可能不适合生产使用。
通过将 TWISTED_REACTOR_ENABLED 设置为 False,可以在不安装 Twisted 反应器的情况下使用 Scrapy。在此模式下,Scrapy 将直接使用 asyncio 事件循环,并且大多数 Scrapy 功能将以相同的方式工作。
这样做在某些用例中提供了以下几个好处:
Twisted 反应器一旦停止,就无法再次启动。这会阻止例如在同一进程中使用多个
AsyncCrawlerProcess实例(当它们使用反应器时),但将TWISTED_REACTOR_ENABLED=False设置后,这变为可能。可能存在
AsyncioSelectorReactor及相关 Twisted 代码施加的限制,例如在 Windows 上要求使用SelectorEventLoop(参见 Windows 特定注意事项),如果未使用反应器则这些限制不适用。AsyncioSelectorReactor管理底层事件循环,虽然AsyncCrawlerRunner可以使用预先存在的反应器,而该反应器又可以使用预先存在的事件循环,但直接将AsyncCrawlerRunner与预先存在的循环一起使用会更简单。省略反应器机制可能会提高性能和可靠性。
局限性
由于某些 Scrapy 功能和组件需要反应器,因此在没有反应器的情况下它们无法工作并被禁用。在未来的 Scrapy 版本中可能会添加不需要反应器的替代方案。以下功能不可用:
默认的 HTTP(S) 下载处理器
HTTP11DownloadHandler(这可能是最大的区别;Scrapy 提供了一个不需要反应器的 HTTP(S) 下载处理器,并将替代它使用:HttpxDownloadHandler)CrawlerRunner和CrawlerProcess(AsyncCrawlerProcess和AsyncCrawlerRunner可用)Twisted 特定的 DNS 解析器(
DNS_RESOLVER设置)需要反应器的用户和第三方代码(示例见 下方)
请注意,导入 Twisted 模块以及创建和使用 Deferred 对象等操作并不需要反应器,因此使用 Deferred、Failure 和其他一些 Twisted API 的代码不一定会停止工作。
其他区别
当 TWISTED_REACTOR_ENABLED 设置为 False 时,Scrapy 将更改一些其他设置的默认值:
TELNETCONSOLE_ENABLED设置为False。DOWNLOAD_HANDLERS_BASE中的"http"和"https"键设置为"scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler"。DOWNLOAD_HANDLERS_BASE中的"ftp"键设置为None。
因此,HttpxDownloadHandler 默认用于发出 HTTP(S) 请求。有关其与 HTTP11DownloadHandler 的差异和局限性,请参阅其文档。
此外,AsyncCrawlerProcess 将安装一个 元路径查找器,以防止 twisted.internet.reactor 被导入。
为现有代码添加支持
不直接使用 Twisted API 或依赖于 Twisted API 的代码,在没有反应器的情况下运行时不需要特殊支持。
以下是一些需要替换的 API 和模式示例:
使用
reactor.callLater()进行睡眠或延迟调用。你可以改用asyncio.loop.call_later()。使用
twisted.internet.threads.deferToThread()、reactor.callFromThread()和相关 API 在其他线程中执行代码。你可以改用asyncio.to_thread()、asyncio.loop.call_soon_threadsafe()和相关 API。使用
twisted.internet.task.LoopingCall来调度重复任务。由于标准库中没有直接的替代品,你可能需要使用asyncio.sleep()在任务中编写自己的实现。使用 Twisted 网络客户端和服务器 API(
reactor.connectTCP()、reactor.listenTCP()、twisted.web.client、twisted.mail.smtp等)。你可以使用其他内置或第三方库来实现此功能。使用
CrawlerProcess或CrawlerRunner。你应该分别改用AsyncCrawlerProcess或AsyncCrawlerRunner。使用
scrapy.utils.reactor.is_asyncio_reactor_installed()检查asyncio支持是否可用。你应该改用scrapy.utils.asyncio.is_asyncio_available()。
Scrapy 为其中一些示例提供了统一的助手函数:
- scrapy.utils.asyncio.call_later(delay: float, func: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts]) CallLaterResult[source]
在延迟后调度一个函数被调用。
这会使用
asyncio.loop.call_later()或reactor.callLater(),具体取决于 asyncio 支持是否可用。在 2.14.0 版本中添加。
- scrapy.utils.asyncio.create_looping_call(func: ~collections.abc.Callable[[~_P], ~scrapy.utils.asyncio._T], *args: ~typing.~_P, **kwargs: ~typing.~_P) AsyncioLoopingCall | LoopingCall[source]
创建循环调用类的实例。
这会创建
AsyncioLoopingCall或LoopingCall的实例,具体取决于 asyncio 支持是否可用。在 2.14.0 版本中添加。
- class scrapy.utils.asyncio.AsyncioLoopingCall(func: ~collections.abc.Callable[[~_P], ~scrapy.utils.asyncio._T], *args: ~typing.~_P, **kwargs: ~typing.~_P)[source]
一个使用 asyncio 实现周期性调用的简单实现,与
LoopingCall保持一定的 API 和行为兼容性。该函数每 interval 秒调用一次,与上次调用的完成时间无关。如果函数在再次调用时仍在运行,则会跳过调用,直到函数完成。
该函数不得返回协程或
Deferred。在 2.14.0 版本中添加。
- async scrapy.utils.asyncio.run_in_thread(func: ~collections.abc.Callable[[~_P], ~scrapy.utils.asyncio._T], *args: ~typing.~_P, **kwargs: ~typing.~_P) _T[source]
在线程中调用一个函数,并将其结果作为协程返回。
这会使用
asyncio.to_thread()或twisted.internet.threads.deferToThread(),具体取决于 asyncio 支持是否可用。2.15.0 版本中新增。
如果你的代码需要知道反应器是否可用,你可以检查 TWISTED_REACTOR_ENABLED 设置的值(你需要访问 Crawler 实例才能做到这一点),或者使用以下函数:
- scrapy.utils.reactorless.is_reactorless() bool[source]
检查我们是否在无反应器模式下运行,即
TWISTED_REACTOR_ENABLED设置为False。由于此函数检查的是运行时状态而非设置本身,因此在反应器和/或 asyncio 事件循环初始化之前的很早阶段执行时,结果可能不准确。
注意
由于此函数使用
scrapy.utils.asyncio.is_asyncio_available(),因此它在检测正在运行的 asyncio 事件循环方面具有相同的局限性。2.15.0 版本中新增。
通常,不直接或间接使用反应器的代码,在有 asyncio 反应器和没有反应器的情况下都可以未经修改地使用。这包括在 集成 Deferred 代码与 asyncio 代码 中描述的将 Deferred 转换为 Future 或反之的代码。
故障排除
ImportError: 在没有 Twisted 反应器的情况下运行时,禁止导入 twisted.internet.reactor [...]: Scrapy 配置为在没有反应器的情况下运行,但某些代码导入了 twisted.internet.reactor,这很可能是因为该代码需要使用反应器。你需要停止使用此代码或将 TWISTED_REACTOR_ENABLED 重新设置为 True。反应器也可能并非真正需要,但由于 处理预安装的反应器 中描述的问题而安装,在这种情况下,修复有问题的导入就足够了。
RuntimeError: TWISTED_REACTOR_ENABLED 为 False 但已安装 Twisted 反应器: Scrapy 配置为在没有反应器的情况下运行,但在 Scrapy 代码执行之前已经安装了反应器。如果你尝试通过 每个爬虫设置 设置 TWISTED_REACTOR_ENABLED,目前不支持。
RuntimeError: 我们期望安装 Twisted 反应器但它未安装: Scrapy 配置为运行带有反应器但不安装反应器,但在 Scrapy 代码执行之前未安装反应器。如果你尝试通过 每个爬虫设置 设置 TWISTED_REACTOR_ENABLED,目前不支持。
RuntimeError: <class> 不支持 TWISTED_REACTOR_ENABLED=False: 列出的类不能与设置为 False 的 TWISTED_REACTOR_ENABLED 一起使用。在 上面的文档 或受影响类的文档中可能有替代方案。
Windows 特定注意事项
Windows 上的 asyncio 实现可以使用两种事件循环实现:ProactorEventLoop(默认)和 SelectorEventLoop。然而,只有 SelectorEventLoop 适用于 Twisted。
当你更改 TWISTED_REACTOR 设置或调用 install_reactor() 时,Scrapy 会自动将事件循环类更改为 SelectorEventLoop。
注意
你使用的其他库可能需要 ProactorEventLoop,例如因为它支持子进程(playwright 就是这种情况),因此你无法在 Windows 上将它们与 Scrapy 一起使用(但你可以在 WSL 或原生 Linux 上使用它们)。
注意
当不使用反应器时,此问题不适用,详见 在不使用 Twisted 反应器的情况下使用 Scrapy。
使用自定义 asyncio 循环
你还可以将自定义 asyncio 事件循环与 asyncio 反应器一起使用。将 ASYNCIO_EVENT_LOOP 设置为所需事件循环类的导入路径,以使用它而不是默认的 asyncio 事件循环。
切换到非 asyncio 反应器
如果由于某种原因你的代码无法与 asyncio 反应器一起使用,你可以通过将 TWISTED_REACTOR 设置为其导入路径(例如 'twisted.internet.epollreactor.EPollReactor')或设置为 None 来使用不同的反应器,这将使用你平台的默认反应器。如果你正在使用 AsyncCrawlerRunner 或 AsyncCrawlerProcess,你还需要切换到它们基于 Deferred 的对应项:分别为 CrawlerRunner 或 CrawlerProcess。