目录
- 前言
- 一、问题的根源:两套不兼容的「可等待」
- 场景 A:asyncio reactor(Scrapy 2.13+ 的默认值)
- 场景 B:SelectReactor(传统 Twisted reactor)
- 对照表
- 二、`is_asyncio_available()`:所有分支的开关
- 三、`maybe_deferred_to_future`:Deferred → 可 await
- `maybe_` 前缀的含义
- 四、`deferred_from_coro`:协程 → Deferred
- 两种包装方式的差别
- 五、`deferred_f_from_coro_f`:装饰器版本
- 六、`ensure_awaitable`:把任何东西变成可 await
- `_warn` 参数:一个渐进式废弃的巧思
- 七、`_schedule_coro`:把协程甩到后台
- 八、完整的可运行验证脚本
- 九、在 Spider 里正确使用异步库
- 前提:确认用的是 asyncio reactor
- 正确写法
- 三条铁律
- 十、报错速查
- 先确认自己跑在哪种 reactor 上
- 十一、四个函数的全景
- 为什么不干脆全换成 asyncio
- 小结
- 参考
本文基于Scrapy 2.17.0 + Twisted 26.4.0,所有行为差异都是本机在两种 reactor 下分别实跑验证的。
如果你遇到过RuntimeError: Task got bad yield: <Deferred at 0x...>或者RuntimeError: no running event loop,这篇正好解释根因。
前言
上一篇 拆Scraper时,这几个函数反复出现:
awaitmaybe_deferred_to_future(self.itemproc.process_item(item,spider))returndeferred_from_coro(self.handle_spider_output_async(...))_schedule_coro(self._wait_for_processing(result,request,queue_dfd))returnawaitensure_awaitable(iterate_spider_output(output))它们都在干同一件事:在 Twisted 的Deferred和 Python 原生的async/await之间来回搬运。
Scrapy 的历史包袱决定了它必须同时伺候两套异步体系——底层网络栈是 Twisted,用户代码越来越多用async def。这篇我们看它是怎么把这两个世界缝在一起的。
一、问题的根源:两套不兼容的「可等待」
Twisted 的Deferred和 asyncio 的Future都表示「将来会有的值」,但它们不能互相 await。
而更麻烦的是:Scrapy 支持多种 reactor,不同 reactor 下能 await 什么完全相反。
我在两种 reactor 下跑了同一段代码,结果是镜像的:
场景 A:asyncio reactor(Scrapy 2.13+ 的默认值)
fromscrapy.utils.reactorimportinstall_reactor install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")is_asyncio_available(): True 1. maybe_deferred_to_future(Deferred) 包装后类型: Future await 结果: 来自 Deferred 的结果 2. 直接 await 裸 Deferred(asyncio reactor 下) 失败: RuntimeError: Task got bad yield: <Deferred at 0x2023d2029c0>场景 B:SelectReactor(传统 Twisted reactor)
install_reactor("twisted.internet.selectreactor.SelectReactor")reactor: SelectReactor is_asyncio_available(): False 1. maybe_deferred_to_future(Deferred) 在非 asyncio reactor 下 包装后类型: Deferred <- 原样返回 Deferred await 结果: 结果 2. 直接 await 裸 Deferred 结果: 裸 await 也可以 3. 此时 await asyncio.sleep() 会怎样 失败: RuntimeError: no running event loop对照表
| asyncio reactor | 非 asyncio reactor | |
|---|---|---|
is_asyncio_available() | True | False |
await裸Deferred | ❌Task got bad yield | ✅ 可以 |
await asyncio.sleep() | ✅ 可以 | ❌no running event loop |
maybe_deferred_to_future返回 | Future | Deferred(原样) |
两种环境下能 await 的东西恰好互斥。这就是为什么框架代码里到处都是maybe_deferred_to_future——它是唯一能在两种环境下都工作的写法。
二、is_asyncio_available():所有分支的开关
整个桥接层的每个函数都以它开头:
defis_asyncio_available()->bool:"""Check if it's possible to call asyncio code that relies on the asyncio event loop. .. versionadded:: 2.14 """文档字符串里定义得很清楚:
This function returns
Trueif there is a running asyncio event loop. If there is no such loop, it returnsTrueif the Twisted reactor that is installed isAsyncioSelectorReactor, returnsFalseif a different reactor is installed, and raises aRuntimeErrorif no reactor is installed.
三种返回情况:
| 状态 | 返回 |
|---|---|
| 有运行中的 asyncio 事件循环 | True |
装了AsyncioSelectorReactor | True |
| 装了其他 reactor | False |
| 没装任何 reactor | 抛RuntimeError |
最后一条要留意:它可能抛异常而不是返回False。在 reactor 安装之前调用桥接函数会直接炸。
三、maybe_deferred_to_future:Deferred → 可 await
defmaybe_deferred_to_future(d:Deferred[_T])->Deferred[_T]|Future[_T]:ifnotis_asyncio_available():returndreturndeferred_to_future(d)defdeferred_to_future(d:Deferred[_T])->Future[_T]:ifnotis_asyncio_available():raiseRuntimeError("deferred_to_future() requires an installed asyncio reactor"" or a running asyncio event loop.")returnd.asFuture(asyncio.get_event_loop())逻辑简单到不能再简单:
- asyncio 环境→
d.asFuture(loop)转成Future - 非 asyncio 环境→ 原样返回
Deferred
因为在非 asyncio 环境下,Deferred本来就能被await(Twisted 给它实现了__await__)。
maybe_前缀的含义
对比一下这两个函数:
| 函数 | 非 asyncio 环境下 |
|---|---|
deferred_to_future | 抛 RuntimeError |
maybe_deferred_to_future | 原样返回,不报错 |
maybe_的意思是「可能不转换」。写框架代码或需要跨环境兼容的扩展,永远用maybe_版本;只有当你明确要求 asyncio 环境(比如要 awaitasyncio.Future)时才用不带前缀的版本。
官方文档给的用法示例:
classMySpider(Spider):asyncdefparse