我是如何搞定异步、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. 当项目里同时有
asyncio和Twisted时,我该怎么办) - [6. ASGI 是啥](#6. ASGI 是啥)
- [7. 简单的记录一下本项目的执行顺序](#7. 简单的记录一下本项目的执行顺序)
- [8. 结语](#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 里有这种写法:
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 并不是"代理工具函数集合"
我最初在设计这一模块的时候,只是想设计成最简单 增加/删除/更换 的模式。
但是想到了之前设计敏感词的灵感,便决定设计成代理健康管理中心。
- 代理池维护 :
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 - 两边都要跑,就得让它们能"共用一个事件循环"或者至少能互相配合
所以我的项目里做了桥接,最核心就是这一句:
python
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 serverFastAPI是 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. 简单的记录一下本项目的执行顺序
api/main.py(入口 + 异常映射)api/routers/*.py(接口分发与参数)services/*(业务编排:这次想抓什么)crawler/runner.py(执行入口:怎么把爬虫跑起来)crawler/spiders/*+parsers/*(具体抓取和解析)tests/*(用测试反证:我理解的对不对)
8. 结语
用go用习惯的我,在用python写代码时,
最让人崩溃的往往不是语法,而是"层次没拆开"。
一旦每层职责清楚,其实项目就已经清清楚楚了。