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

我是如何搞定异步、asyncio/Twisted、代理池的

    • [1. 我先把总图搭起来:进行职责分层](#1. 我先把总图搭起来:进行职责分层)
      • [用 Go 类比一下](#用 Go 类比一下)
    • [2. 为什么 `raise` 不能随便换成 `return`](#2. 为什么 raise 不能随便换成 return)
    • [3. `proxy_service.py` 并不是"代理工具函数集合"](#3. proxy_service.py 并不是“代理工具函数集合”)
    • [4. `runner.py` 为何我要这样设计](#4. runner.py 为何我要这样设计)
    • [5. 当项目里同时有 `asyncio` 和 `Twisted` 时,我该怎么办](#5. 当项目里同时有 asyncioTwisted 时,我该怎么办)
    • [6. ASGI 是啥](#6. ASGI 是啥)
    • [7. 简单的记录一下本项目的执行顺序](#7. 简单的记录一下本项目的执行顺序)
    • [8. 结语](#8. 结语)

因为功能需求与复杂度的增加,最近我把一个脚本爬虫重构成了 爬虫网关
为了达到代码 "更干净" 的目地, 也是遇到了很多难以攻克的难题。
从刚开始的一会儿 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 里有这种写法:

python 复制代码
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
  • 两边都要跑,就得让它们能"共用一个事件循环"或者至少能互相配合

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

python 复制代码
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 这套协议上。

txt 复制代码
用 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写代码时,

最让人崩溃的往往不是语法,而是"层次没拆开"。

一旦每层职责清楚,其实项目就已经清清楚楚了。

相关推荐
岱宗夫up1 小时前
从代码模式到智能模式:AI时代的设计模式进化论
开发语言·python·深度学习·神经网络·自然语言处理·知识图谱
xzjiang_3651 小时前
Jupyter 运行经验3:读入和显示一张图片
ide·python·jupyter
先做个垃圾出来………1 小时前
DeepDiff
python
喵手1 小时前
Python爬虫实战:节奏律动 - Billboard Hot 100 历史榜单深度采集实战!
爬虫·python·爬虫实战·零基础python爬虫教学·billboard hot·历史版单采集·采集billboard hot
52Hz1182 小时前
力扣131.分割回文串、35.搜索插入位置、74.搜索二维矩阵、34.在排序数组中查找...
python·算法·leetcode
二十雨辰2 小时前
[python]-多任务
python
癫狂的兔子2 小时前
【Python】【机器学习】集成算法(随机森林、提升算法)
python·算法·机器学习
kong79069282 小时前
Python核心语法-Matplotlib简介
开发语言·python·matplotlib
马克Markorg2 小时前
基于LLM的大模型的RAG(检索增强生成)实现对比
python·大模型·agent·rag·企业级知识库的框架·rag 知识库