☰
把 Scrapy 2.17 的主链路源码读完之后,我最想吐槽的不是性能——是它有 7 个地方会静默失效,一行日志都不给
2026/10/23 14:14:21 网站建设 项目流程

目录

    • 前言
    • 一、先说安利:这五条我认为做对了
      • 1. 接口极简,而且真的极简
      • 2. 不预置任何东西
      • 3. 核心组件小到能完整读完
      • 4. 三级背压是认真设计的
      • 5. 2.15+ 终于能完全不用 Twisted
    • 二、七个静默失效点
    • 三、连了等于没连:SignalManager 的弱引用
    • 四、照着文档写反而不生效:Add-ons 的优先级 15
    • 五、两套异步体系,能 await 的东西恰好互斥
    • 六、其余四个,各说一句
      • `canonicalize_url` 不去默认端口、不解析 `..`
      • 包裹顺序升序 ≠ item 流向降序
      • reactor 配置写 `custom_settings` 来不及
      • `NotConfigured` 不带消息就没有日志
    • 七、为什么它们全是「静默」
    • 八、一份可以抄走的 checklist
      • 配套一个自查脚本
    • 九、那还用不用
    • 小结
    • 参考

本文不是「用了两天来吐槽」。过去两周我把 Scrapy 2.17 的主链路
(Engine → Scheduler → Scraper → Pipeline → Crawler → Signals → Add-ons)逐个读完并写了 8 篇源码解析,
每条结论都有源码位置和跑得起来的验证脚本。这篇是读完之后的总账:先安利 5 条,再吐槽 7 条。
如果你只想避坑,直接看第三节那张表。

前言

「吐槽开源项目」这件事有个通病:吐的多半是用了两天没跑通的挫败感,而不是用久了才看得见的设计问题。前者其实是文档问题,后者才值得写出来。

我这两周干的事是后者的前提:把 Scrapy 2.17 的主链路源码从头读了一遍,一边读一边写验证脚本,产出了 8 篇解析——从引擎调度循环、Scheduler 双队列、Scraper 与中间件、Item 管道,一直到启动流程、信号系统和 Add-ons 机制。

读完之后我发现,我想吐槽的东西高度集中,而且都不是性能问题。它们有一个共同形状:

代码看起来完全正确,行为不符合预期,而且不报错、不打日志。

一共 7 处。它们不是 bug——多数是有意为之的设计,只是那个「意」你不读源码根本猜不到。而这类问题最贵的地方在于:你不会去排查一个没有报错的功能。

先把话说公道:Scrapy 有几个设计我认为是对的,甚至是同类里做得最好的。


一、先说安利:这五条我认为做对了

1. 接口极简,而且真的极简

Scheduler对引擎暴露的核心接口只有四个:

classBaseScheduler:defopen(self,spider)->Deferred|None:...defclose(self,reason:str)->Deferred|None:...defhas_pending_requests(self)->bool:...defenqueue_request(self,request:Request)->bool:...defnext_request(self)->Request|None:...

你想换掉整个调度器(比如接 Redis 做分布式),实现这几个方法就够。这种克制在爬虫框架里不常见。

2. 不预置任何东西

# scrapy/settings/default_settings.pyITEM_PIPELINES_BASE={}# ← 就是空的

Scrapy 不给你塞任何默认 Pipeline,不配就什么都不做。对比一些「开箱即用」但你得先删掉一半默认组件的框架,这个默认值是尊重人的。

3. 核心组件小到能完整读完

RFPDupeFilter的本体就是一个set加一个可选的requests.seen文件:

defrequest_seen(self,request:Request)->bool:fp=self.request_fingerprint(request)iffpinself.fingerprints:# ← 一个 set,全部的去重逻辑returnTrueself.fingerprints.add(fp)ifself.file:self.file.write(fp+"\n")returnFalse

scrapy/addons.py更夸张——整个文件 60 行,两个方法。一个用了十几年的框架,核心还能保持这个体积,说明重构没停过。

4. 三级背压是认真设计的

# 引擎在每次调度前问三句话,任一为真就停止发新请求def_needs_backout(self)->bool:return(notself.runningorself._slot.closingisnotNoneorself.downloader.needs_backout()# 按【条数】orself.scraper.slot.needs_backout()# 按【字节数】)

下载器按条数、Scraper 按字节数、Item 按条数,任意一级顶格都会踩刹车。爬虫最容易死在「下载快、解析慢、内存爆」上,这三道闸是对症的。

5. 2.15+ 终于能完全不用 Twisted

HTTP 栈换成 httpx,并且用MetaPathFinder禁止 import reactor——不是「尽量不用」,是从导入层面堵死。我实测跑通过:TWISTED_REACTOR_ENABLED=False下本地和远程请求都正常,sys.modules里确实没有 reactor。这是一个背了十几年历史包袱的项目能做出的最硬的动作。

说完好的,开始吐槽。


</

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询