Scrapy 嵌入 FastAPI 的坑:Asyncio/Twisted 桥接 + 代理池设计

简介: 因为功能需求与复杂度的增加,最近我把一个脚本爬虫重构成了`爬虫网关`。为了达到代码 “更干净” 的目地, 也是遇到了很多难以攻克的难题。

因为功能需求与复杂度的增加,最近我把一个脚本爬虫重构成了爬虫网关
为了达到代码 “更干净” 的目地, 也是遇到了很多难以攻克的难题。
从刚开始的一会儿 asyncio,一会儿 Twisted的凌乱,到给本爬虫服务添加一个代理池。也算是磕磕绊绊。

虽然项目还未重构完成,但是此时我正站在一个值得纪念的转折点上,
故而写本篇博客的目的就是,就是为了回望、记录、总结我的来时路。

  1. apicoreservicescrawler 到底各管什么
  2. raisereturn 什么时候用才不含糊
  3. proxy_service.py 实际上在做哪几件事
  4. 为什么项目里同时出现 asyncioTwisted,而我又是如何解决的。
  5. ASGI 是什么,它跟 FastAPI 是什么关系

以下是本博客的核心:
我遇到的问题:FastAPI(ASGI/asyncio) + Scrapy(Twisted) 同进程集成 + 代理池
我是如何解决的:职责分层 + runner 适配层 + asyncioreactor + Deferred→Future 桥接 + proxy 健康状态机

1. 我先把总图搭起来:进行职责分层

再学习scrapy框架的时候,我就常常在想,我到底该如何设计。
才可以再本地启动爬虫的时候,同时对外提供服务。

后来经过资料查询,发现这条链路其实很固定,而最常用的方式如下:

api -> services -> crawler

  • api:接 HTTP 请求,做参数校验,调用 service,最后把返回值包装成统一响应
  • core:配置、日志、错误体系、鉴权这类“所有人都要用”的底座
  • services:业务编排层(决定“这次要抓什么 / 抓到什么算成功”)
  • crawler:执行层(决定“用什么方式抓 / 失败怎么重试 / 代理怎么上报”)

我一开始读不懂,就是因为把“业务想要什么”和“爬虫怎么跑”同时塞进脑子里。
后来强行按层拆开:先只看 services 在拼什么,再去看 crawler 怎么实现,难度立刻下降。

用 Go 类比一下

如果你写过 Go 的 Web 项目,这条链路几乎就是:

apihandler(Gin/Echo 的 handler:接请求、绑定参数、回响应)

servicesservice(业务编排:调用多个组件,拼装结果)

crawlerworker/job runner(真正干活:跑任务、做 I/O、处理失败重试)

coreconfig/log/middleware/errors(统一配置、日志、鉴权、错误封装)


2. 为什么 raise 不能随便换成 return

在我的 luogu_service.py 里有这种写法:

for row in items:
    error = row.get("_error")
    if error:
        raise UpstreamRequestError(f"luogu practice: {error}")

因为带入了go语言的思想,我当时第一反应也是:return 不也能结束函数吗?

但这里差别不在“能不能结束”,而在上层会怎么理解你这次调用

  • return:更像“正常结束,只是结果是这个”
  • raise:明确告诉上层“这次调用失败了,不是正常结果的一种”

如果这里用 return,上层很可能把它当成“空数据”或者“正常结构但没抓到”。
raise UpstreamRequestError 的好处是,API 层能统一映射成 502,也能进统一日志/监控,语义更干净。

只要这件事不该被当成正常结果,就别 return,直接 raise。


3. proxy_service.py 并不是“代理工具函数集合”

我最初在设计这一模块的时候,只是想设计成最简单 增加/删除/更换 的模式。
但是想到了之前设计敏感词的灵感,便决定设计成代理健康管理中心

  1. 代理池维护sync_replace / remove / get_snapshot
  2. 健康状态机OK / SUSPECT / DEAD 这种分层不是为了好看,是为了避免“一次失败就踢掉”或者“死代理一直占坑”
  3. 按目标站点统计:不同站点(leetcode/luogu/lanqiao)分开记成功率、延迟,不然一个站点把代理打死会误伤所有站点
  4. 主动探活:后台循环定时测一遍代理是否还活着(让 DEAD 有机会回来)
  5. 被动更新:每次请求成功/失败,实时上报更新健康度(让 OK/SUSPECT/DEAD 动起来)

我觉得这里最关键的理解点是:

  • 策略主要在 proxy_service.py(怎么评估代理)
  • 触发点在 crawler/middlewares.py(什么时候上报:请求阶段、响应阶段、异常阶段)

也就是说:proxy_service.py 像“裁判”,middlewares 像“把球踢到裁判那的人”。


4. runner.py 为何我要这样设计

runner.py 表面上像一堆 Scrapy 启动代码,实际上它在做一件很具体的事:

把 Scrapy 封装成一个能 await 的服务接口。

它主要干了这些活:

  1. 单例化 ScrapyRunnerService(避免重复初始化 runner / settings)
  2. 创建 CrawlerRunner(settings=...)
  3. 运行指定 spider
  4. 监听 item_scraped 信号,把抓到的结果收起来
  5. 把超时和异常统一成项目语义(CrawlerTimeoutError / CrawlerExecutionError

一句话总结:
它不是“爬虫逻辑”,而是执行适配层——把 Scrapy 的执行模型,翻译成 services 层能直接调用的接口。


5. 当项目里同时有 asyncioTwisted 时,我该怎么办

为何我会遇到这种问题

我之所以会遇到这种问题,根因是“服务形态变了”:

  • 原生 Scrapy:独立爬虫进程(Twisted 自己玩)
  • 而我要对外提供http接口:Web API 网关(FastAPI/ASGI/asyncio)

当我把 Scrapy 嵌进 FastAPI 时,等于把两种并发模型塞进一个长驻进程里。
此时自然会撞上这些问题:

等待模型不同
1、FastAPI 习惯 await Future/Task
2、Scrapy 给你的是 Twisted Deferred

事件循环不同
1、asyncio 一套 loop
2、Twisted 一套 reactor

生命周期不同
1、API 服务要长期运行、可复用、可测试
2、Scrapy 命令行通常是一次性执行

超时/取消/异常语义要统一
我要把爬虫异常稳定映射成 HTTP 错误(502/504),必须先把底层执行语义对齐

所以我当时卡住的问题,本质上是“架构层跨生态集成”问题。
所以我使用的(reactor + deferred_to_future)的桥接,本质上就是把两种模型统一到一条可维护链路里。

问题点找到了,就容易解决了

后面想明白其实很朴素:

  • FastAPI 跑在 ASGI 的异步生态里,主流是 asyncio
  • Scrapy 底层用的是 Twisted
  • 两边都要跑,就得让它们能“共用一个事件循环”或者至少能互相配合

所以我的项目里做了桥接,最核心就是这一句:

install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")

再用 deferred_to_future(...) 把 Twisted 的 Deferred 转成 asyncio.Future,这样我才能在 FastAPI 的 async defawait 一个 Scrapy 执行结果。

我最初之所以会遇到这种问题
FastAPI 这边负责“接请求并发”,Scrapy 那边负责“跑爬虫 IO”,桥接层负责让两套异步语言能互相翻译。


6. ASGI 是啥

ASGI 可以当成 Python Web 的“异步接口标准”,也就是服务器跟框架怎么对话的一套协议。

  • uvicorn 是 ASGI server
  • FastAPI 是 ASGI app
  • 所以我能写 async def 路由,天然支持高并发 I/O

FastAPI 能异步,是因为它跑在 ASGI 这套协议上。

用 Go 类比一下:ASGI 有点像 Go 的 net/http 约定

在 Go 里,server 和框架之间的“约定”非常清晰:

handler 必须长得像 func(w http.ResponseWriter, r *http.Request)

框架(Gin/Echo)也是把它包装了一层,但最终都能落到这个约定上

7. 简单的记录一下本项目的执行顺序

  1. api/main.py(入口 + 异常映射)
  2. api/routers/*.py(接口分发与参数)
  3. services/*(业务编排:这次想抓什么)
  4. crawler/runner.py(执行入口:怎么把爬虫跑起来)
  5. crawler/spiders/* + parsers/*(具体抓取和解析)
  6. tests/*(用测试反证:我理解的对不对)

8. 结语

用go用习惯的我,在用python写代码时,
最让人崩溃的往往不是语法,而是“层次没拆开”。
一旦每层职责清楚,其实项目就已经清清楚楚了。

目录
相关文章
|
1天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
10132 27
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
13天前
|
人工智能 安全 Linux
【OpenClaw保姆级图文教程】阿里云/本地部署集成模型Ollama/Qwen3.5/百炼 API 步骤流程及避坑指南
2026年,AI代理工具的部署逻辑已从“单一云端依赖”转向“云端+本地双轨模式”。OpenClaw(曾用名Clawdbot)作为开源AI代理框架,既支持对接阿里云百炼等云端免费API,也能通过Ollama部署本地大模型,完美解决两类核心需求:一是担心云端API泄露核心数据的隐私安全诉求;二是频繁调用导致token消耗过高的成本控制需求。
5855 14
|
21天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
22900 119
|
7天前
|
人工智能 JavaScript API
解放双手!OpenClaw Agent Browser全攻略(阿里云+本地部署+免费API+网页自动化场景落地)
“让AI聊聊天、写代码不难,难的是让它自己打开网页、填表单、查数据”——2026年,无数OpenClaw用户被这个痛点困扰。参考文章直击核心:当AI只能“纸上谈兵”,无法实际操控浏览器,就永远成不了真正的“数字员工”。而Agent Browser技能的出现,彻底打破了这一壁垒——它给OpenClaw装上“上网的手和眼睛”,让AI能像真人一样打开网页、点击按钮、填写表单、提取数据,24小时不间断完成网页自动化任务。
1738 4