欢迎光临
我们一直在努力

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

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

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

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

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

  • api、core、services、crawler 到底各管什么
  • raise 和 return 什么时候用才不含糊
  • proxy_service.py 实际上在做哪几件事
  • 为什么项目里同时出现 asyncio 和 Twisted,而我又是如何解决的。
  • 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 项目,这条链路几乎就是:

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

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

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

    core ≈ config/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 并不是“代理工具函数集合”

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

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

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

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


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

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

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

    它主要干了这些活:

  • 单例化 ScrapyRunnerService(避免重复初始化 runner / settings)
  • 创建 CrawlerRunner(settings=…)
  • 运行指定 spider
  • 监听 item_scraped 信号,把抓到的结果收起来
  • 把超时和异常统一成项目语义(CrawlerTimeoutError / CrawlerExecutionError)
  • 一句话总结: 它不是“爬虫逻辑”,而是执行适配层——把 Scrapy 的执行模型,翻译成 services 层能直接调用的接口。


    5. 当项目里同时有 asyncio 和 Twisted 时,我该怎么办

    为何我会遇到这种问题

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

    • 原生 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 def 里 await 一个 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. 简单的记录一下本项目的执行顺序

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

  • 8. 结语

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

    赞(0)
    未经允许不得转载:171主机测评 » Scrapy 嵌入 FastAPI 的坑:Asyncio/Twisted 桥接 + 代理池设计
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址