Scrapy inline-requests搭配async函数无法等待请求完成问题
问题根因
scrapy-inline-requests的实现基于Twisted的inlineCallbacks机制,仅能对普通生成器函数做拦截处理:自动识别生成器yield出的Request对象,挂起函数直到请求响应返回后再恢复执行。当你给函数加上async关键字后,函数会变成原生协程,@inline_requests装饰器无法正确拦截协程内部yield的对象,协程会直接把yield的Request作为普通生成器值抛出,不会等待请求完成,就会出现逻辑提前向下执行的问题。
可选解决方案
方案1:保留scrapy-inline-requests依赖,桥接asyncio逻辑
不需要把回调声明为async函数,在配置了AsyncioSelectorReactor的前提下,使用Twisted提供的ensureDeferred工具把asyncio协程包装为Twisted Deferred对象,yield给@inline_requests识别等待即可,既保留原有的inline-requests串行写法,又能执行所有asyncio逻辑。
代码调整步骤
- 新增导入:
from twisted.internet.defer import ensureDeferred
- 移除
handle方法的async关键字,所有asyncio逻辑用ensureDeferred包装后yield等待:
@inline_requests def handle(self, response): code = response.meta["code"] cookiejar_ref = response.meta["cookiejar"] # 解析csrfToken逻辑保持不变 soup = BeautifulSoup(response.text, "html.parser") relevant_script = [script.text for script in soup.find_all("script") if "csrfToken" in script.text] matched_group = re.search(r'"csrfToken":"(.+?)"', relevant_script[0]) if len(relevant_script) > 0 else None if matched_group is None: raise Exception("Failed to extract csrfToken") csrf_token = matched_group.group(1) # 等待asyncio延时,后续websocket等异步逻辑用同样方式包装即可 yield ensureDeferred(asyncio.sleep(1)) # 例:yield ensureDeferred(your_websocket_connect_logic()) # 原有请求逻辑保持不变,inline_requests可正常等待响应 api = "https://somesite.com/search" headers = { "x-csrf-token": csrf_token, 'Content-Type':'application/json' } payload = {"a": 1} response = yield Request(api, method='POST', headers=headers, meta={'cookiejar': cookiejar_ref}, body=json.dumps(payload)) lots_url = json.loads(response.text)["redirect"] yield { "lots_url": lots_url, }
方案特点
- 原有inline-requests写法改动量极小
- 所有Scrapy原生特性(请求限流、中间件、日志统计、去重)完全保留
- 无嵌套回调,代码可读性和原有逻辑一致
方案2:移除scrapy-inline-requests依赖,使用Scrapy原生async回调支持
Scrapy 2.0+版本原生支持async def回调,配合已经配置的AsyncioSelectorReactor可以直接用await语法写串行逻辑,完全不需要依赖第三方inline-requests库,不存在兼容性问题,是更稳定的长期方案。
代码调整示例
移除inline_requests相关导入,将回调改为async def,请求直接通过Scrapy引擎的download方法await即可:
# 可删除from inline_requests import inline_requests导入 class QuotesSpider(Spider): name = "scraper" custom_settings = { "TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor", 'DUPEFILTER_CLASS': 'scrapy.dupefilters.BaseDupeFilter', } def start_requests(self): codes= ["A", "B"] url = "https://somesite.com/" for i, code in enumerate(codes): yield Request(url=url, callback=self.handle, meta={'cookiejar': i, "code": code}) async def handle(self, response): code = response.meta["code"] cookiejar_ref = response.meta["cookiejar"] # csrfToken解析逻辑保持不变 soup = BeautifulSoup(response.text, "html.parser") relevant_script = [script.text for script in soup.find_all("script") if "csrfToken" in script.text] matched_group = re.search(r'"csrfToken":"(.+?)"', relevant_script[0]) if len(relevant_script) > 0 else None if matched_group is None: raise Exception("Failed to extract csrfToken") csrf_token = matched_group.group(1) # 原生asyncio逻辑直接await即可,无需包装 await asyncio.sleep(1) # 后续websocket等异步逻辑直接await即可 # 发起请求直接await下载结果 api = "https://somesite.com/search" headers = { "x-csrf-token": csrf_token, 'Content-Type':'application/json' } payload = {"a": 1} search_req = Request(api, method='POST', headers=headers, meta={'cookiejar': cookiejar_ref}, body=json.dumps(payload)) response = await self.crawler.engine.download(search_req, spider=self) lots_url = json.loads(response.text)["redirect"] # 返回Item直接return即可,也可以用yield return { "lots_url": lots_url, }
方案特点
- 无第三方依赖,不存在装饰器和async语法的兼容问题
- 原生支持所有asyncio生态库的调用,不需要额外包装转换
- 所有Scrapy原生特性完全保留,请求会正常经过所有下载中间件、遵守并发限流配置、自动纳入日志统计
- 代码为标准async/await写法,可读性更好
内容的提问来源于stack exchange,提问作者Rahul R Badenkal
相关产品推荐
相关产品推荐

