首页
/ Scrapy SEP-009 深度解读:单例移除与 Crawler 根对象模型的设计演进

Scrapy SEP-009 深度解读:单例移除与 Crawler 根对象模型的设计演进

2026-09-06 11:34:31作者:农烁颖Land

SEP-009(sep/sep-009.rst)是 Scrapy 增强提案(SEP,Scrapy Enhancement Proposals)中关于"移除单例(Singleton removal)"的设计文档。它提出了用单一的 Crawler 根对象取代各模块级单例的重构方案,并由此奠定了 Scrapy 至今沿用的组件注入模型。读完全文,你将理解 Scrapy 从 0.7 时代的单例架构演进到当前 Crawler / CrawlerProcess / from_crawler() 对象体系的完整脉络,并能对照当前仓库源码,看清每一个设计决策(包括提案中遗留的未决问题)最终是如何落地的。

提案背景:Scrapy 0.7 的七个单例

单例(Singleton)是一种"全局唯一实例"的设计模式,在早期项目中常被用来共享状态,但代价是模块间隐式耦合、难以测试、难以在同一进程中并行运行多个实例。

SEP-009 开宗明义:该提案提出对 Scrapy 进行重构以移除单例,"这将带来更干净的 API,并使我们能够实现 SEP-004 中提出的库 API"。原文档列出了 Scrapy 0.7 中实际存在的七个单例:

单例组件 当时的模块级实例
执行引擎(Execution engine) scrapy.core.engine.scrapyengine
执行管理器(Execution manager) scrapy.core.manager.scrapymanager
扩展管理器(Extension manager) scrapy.extension.extensions
Spider 管理器(Spider manager) scrapy.spider.spiders
统计收集器(Stats collector) scrapy.stats.stats
日志系统(Logging system) scrapy.log
信号系统(Signals system) scrapy.xlib.pydispatcher

这些组件都被挂在各自模块的模块级变量上,任何地方 import 一下就能拿到"全局那份"。问题在于:同一个进程里跑不了第二个隔离的爬虫实例,组件之间也无法通过显式的依赖关系表达协作方式。

提案的架构:一个 Crawler 作为唯一根对象

SEP-009 提出的核心方案是:引入一个"根"对象 Crawler(取代当时的 Execution Manager),让上述所有单例都成为这个对象的成员。原文档给出的完整对象树如下:

  • crawlerscrapy.crawler.Crawler 实例(取代当时的 scrapy.core.manager.ExecutionManager)——用一个 Settings 对象实例化
    • crawler.settingsscrapy.conf.Settings 实例(在 __init__ 方法中传入)
    • crawler.extensionsscrapy.extension.ExtensionManager 实例
    • crawler.enginescrapy.core.engine.ExecutionEngine 实例
      • crawler.engine.scheduler
        • crawler.engine.scheduler.middleware —— 访问调度器中间件
      • crawler.engine.downloader
        • crawler.engine.downloader.middleware —— 访问下载器中间件
      • crawler.engine.scraper
        • crawler.engine.scraper.spidermw —— 访问 Spider 中间件
    • crawler.spidersSpiderManager 实例(具体类由 SPIDER_MANAGER_CLASS 设置项给出)
    • crawler.statsStatsCollector 实例(具体类由 STATS_CLASS 设置项给出)
    • crawler.log:Logger 类,其方法取代当时 scrapy.log 的函数。日志在 Crawler 实例化时(若启用)即启动,因此不再需要单独的日志启动函数
      • crawler.log.msg
    • crawler.signals:信号处理
      • crawler.signals.send() —— 等同于 pydispatch.dispatcher.send()
      • crawler.signals.connect() —— 等同于 pydispatch.dispatcher.connect()
      • crawler.signals.disconnect() —— 等同于 pydispatch.dispatcher.disconnect()

这个对象树和当前仓库中的实现高度同构。查看 Crawler 类定义scrapy/crawler.py),可以看到它确实以 spiderclsSettings 为构造输入,并把 addonssignalsstatsengineextensions 等全部挂在实例属性上:

class Crawler:
    engine: _LateAttribute[ExecutionEngine] = _LateAttribute()
    extensions: _LateAttribute[ExtensionManager] = _LateAttribute()
    logformatter: _LateAttribute[LogFormatter] = _LateAttribute()
    request_fingerprinter: _LateAttribute[RequestFingerprinterProtocol] = _LateAttribute()
    stats: _LateAttribute[StatsCollector] = _LateAttribute()

    def __init__(
        self,
        spidercls: type[Spider],
        settings: dict[str, Any] | Settings | None = None,
        init_reactor: bool = False,
    ):
        ...
        self.spidercls: type[Spider] = spidercls
        self.settings: Settings = settings.copy()
        self.spidercls.update_settings(self.settings)
        self.addons: AddonManager = AddonManager(self)
        self.signals: SignalManager = SignalManager(self)

与提案相比有几处演进,都体现为"对同一思想的工程化完善":

  1. Settings 在构造时即确定并拷贝。提案设想 crawler.settings__init__ 中传入,当前实现中 self.settings = settings.copy() 后还会合并 Spider 类上的设置(update_settings),并且只在爬取真正启动时才调用 _apply_settings() 来实例化 statsengineextensions 等重组件并冻结设置。
  2. crawler.log 没有落地为对象。从源码结构看,提案中"Logger 类 + crawler.log.msg"的方向被 Python 标准 logging 模块取代——当前 Crawler 上并没有 log 属性,日志统一走 logging 体系,并由 scrapy.utils.log.configure_loggingCrawlerProcess 初始化时配置(见 CrawlerProcessBase.init)。
  3. 组件按设置项构造。提案中的 SPIDER_MANAGER_CLASSSTATS_CLASS 等"具体类由设置项给出"的思想被完整保留:_apply_settings()self.stats = load_object(self.settings["STATS_CLASS"])(self) 正是"由 STATS_CLASS 设置项决定具体类、构造时把 Crawler 自身传进去"的直接实现。
  4. crawler.spiders 演进为 Spider Loader。当前的 Spider 发现逻辑收敛到 scrapy/spiderloader.py,由 get_spider_loader() 按设置加载,不再作为 Crawler 的常驻属性,但"Spider 管理器是可插拔组件"的定位一脉相承。

组件注入机制:__init__(self, crawler) 取代模块级 import

SEP-009 最关键的一节是"移除单例后所需的代码变更"。原文档指出:所有组件(extensions、middlewares 等)都会在各自的 __init__ 方法中接收这个 Crawler 对象,这将(而且是唯一)成为访问其他任何组件的机制——取代过去"从各自模块 import 单例"的做法。原文档给出的改造前/改造后对照代码,值得完整保留:

改造前(依赖模块级 settings 单例):

#!python
from scrapy.core.exceptions import NotConfigured
from scrapy.conf import settings


class SomeMiddleware(object):
    def __init__(self):
        if not settings.getbool("SOMEMIDDLEWARE_ENABLED"):
            raise NotConfigured

改造后(依赖注入的 crawler 参数):

#!python
from scrapy.core.exceptions import NotConfigured


class SomeMiddleware(object):
    def __init__(self, crawler):
        if not crawler.settings.getbool("SOMEMIDDLEWARE_ENABLED"):
            raise NotConfigured

当前仓库中这一机制已经完全固化,且比提案更进一步——约定俗成的 __init__(self, crawler) 签名被提升为一个标准类方法 from_crawler(crawler)。以 MiddlewareManager 基类scrapy/middleware.py)为例:

@classmethod
def from_crawler(cls, crawler: Crawler) -> Self:
    mwlist = cls._get_mwlist_from_settings(crawler.settings)
    middlewares = []
    enabled = []
    for clspath in mwlist:
        try:
            mwcls = load_object(clspath)
            mw = build_from_crawler(mwcls, crawler)
            ...

即:从 crawler.settings 读取启用列表,逐类加载,构造时统一注入 crawler;中间件若在 __init__ / from_crawlerraise NotConfigured 即被跳过(可选组件的禁用开关)。这一模式由 build_from_crawler()scrapy/utils/misc.py)统一支撑:类若有 from_crawler 方法就走 from_crawler(crawler, *args, **kwargs),否则直接调用 __init__

这正是 SEP-009 预言的"稳定核心 API"的直接成果:扩展、下载器中间件、Spider 中间件、信号管理器全部拿到的是"自己这次爬取"的 Crawler 实例,而不是全局共享的单例——由此同一进程内运行多个互不干扰的爬虫实例才成为可能。

命令行运行流程:从三步设想走到 execute()

SEP-009 的"Running from command line"一节描述了当时的命令行流程(彼时命令行是唯一受支持的运行方式):

  1. 实例化一个 Settings 对象,装入 SCRAPY_SETTINGS_MODULE 中的值,以及各命令的覆盖值;
  2. 用该 Settings 对象实例化一个 Crawler 对象(Crawler 依据给定设置实例化其全部组件);
  3. 用命令行传入的 URL 或域名调用 Crawler.crawl()

当前仓库中这条链路依然清晰可辨。入口是 execute()scrapy/cmdline.py):

def execute(argv: list[str] | None = None, settings: Settings | None = None) -> None:
    ...
    if settings is None:
        settings = get_project_settings()   # 第 1 步:加载项目设置
    ...
    settings.setdict(cmd.default_settings, priority="command")  # 命令级覆盖
    cmd.settings = settings
    ...
    if cmd.requires_crawler_process:
        if (...TWISTED_REACTOR 为 asyncio 且未强制...) or not settings.getbool("TWISTED_REACTOR_ENABLED"):
            cmd.crawler_process = AsyncCrawlerProcess(settings)
        else:
            cmd.crawler_process = CrawlerProcess(settings)   # 第 2 步:构造根对象容器
    _run_print_help(parser, _run_command, cmd, args, opts)     # 第 3 步:执行爬取

可以看到三步结构被原样继承,只是发生了两处扩展:其一,"第 1 步"中 get_project_settings()scrapy.cfg / SCRAPY_SETTINGS_MODULE 加载设置,命令再叠加自己的 default_settings;其二,"第 2 步"从单个 Crawler 泛化为 CrawlerProcessscrapy/crawler.pyCrawlerProcess 继承自 CrawlerRunner),它负责管理多个 Crawler 实例,并额外承担日志配置、信号处理(优雅停机)、DNS 解析器安装等进程级职责——这些正是"进程根"与"爬虫根"分离后的自然分层。

作为库使用:Crawler + Settings 两步走

原文档"Using Scrapy as a library"一节给出了库 API 的两步用法:

  1. 实例化一个 Settings 对象(默认只有默认设置),并覆盖想要的设置;
  2. 用该 Settings 对象实例化一个 Crawler

当前代码中这一步是字面兑现的——Crawler.__init__ 接受 dictSettingsdict 会被包成 Settings 并拷贝;爬取时再传入爬虫初始参数:

from scrapy import Crawler, Spider
from scrapy.settings import Settings


class QuotesSpider(Spider):
    name = "quotes"
    start_urls = ["https://example.com/"]


settings = Settings()
settings.setbot  # 按需覆盖任意设置,例如:
settings.set("USER_AGENT", "mybot/1.0", priority="project")
settings.set("ROBOTSTXT_OBEY", True)

crawler = Crawler(QuotesSpider, settings=settings)
crawler.crawl()  # 同步接口;asyncio 场景可用 crawl_async()

Crawler.crawl() 内部流程(scrapy/crawler.py#L259-L290)即 SEP-009 设想中"Crawler 依据给定设置实例化其全部组件"的精确实现:创建 Spider 实例 → _apply_settings()(加载 stats、extensions、addons、reactor 等)→ 创建 ExecutionEngineopen_spiderstart

未决问题的最终答案:Spider 如何访问设置

SEP-009 最后列出了"Open issues to resolve",其中对 Spider 开发者最相关的一个是:Spider 应该怎样访问设置? 提案给出了两个选项:

  • Option 1:也把 Crawler 对象传给 Spider 的 __init__
    • 优点:访问所有组件(对 Spider 而言最重要的是 settings 和 signals)的通道统一;
    • 缺点:Spider 代码可以访问(并控制)任意 crawler 组件——而官方并不希望 Spider 去摆弄爬虫内部("如果真需要,请写扩展或 Spider 中间件")。
  • Option 2:只把 Settings 对象传给 Spider 的 __init__,之后通过 self.settings 访问,就像通过 self.log 访问日志那样。
    • 缺点:还需要一种方式来访问 stats 等组件。

当前仓库的实现可以看作"以 Option 2 为主、吸收 Option 1 优点"的折中。查看 Spider.from_crawler 与 _set_crawlerscrapy/spiders/init.py):

@classmethod
def from_crawler(cls, crawler: Crawler, *args: Any, **kwargs: Any) -> Self:
    spider = cls(*args, **kwargs)
    spider._set_crawler(crawler)
    return spider

def _set_crawler(self, crawler: Crawler) -> None:
    self.crawler: Crawler = crawler
    self.settings: BaseSettings = crawler.settings
    crawler.signals.connect(self.close, signals.spider_closed)

也就是说:Spider 的 __init__ 保持"无 crawler 参数"的干净签名(Option 1 担心的问题被规避),但框架在构造后会通过 from_crawlercrawlersettings 两个属性挂到实例上,使 Spider 中 self.settings 可用(Option 2 的易用性保留),而 self.crawler 的存在也提供了访问 stats 等组件的官方通道(回应 Option 2 的缺点)。注意 self.settings 是 Spider 专属的只读语义——与提案中"不鼓励 Spider 控制 crawler 内部"的原则一致,Spider 拿到的是 crawler.settings 这一份冻结后的设置视图。

另外,提案中"是否要把 Settings 传给 ScrapyCommand.add_options()"这一开放问题,在现行命令基类中也已有答案:execute() 在解析参数前显式执行 cmd.settings = settingsscrapy/cmdline.py#L193-L203),命令类通过 self.settings 读取设置,而非 import 任何全局对象。

小结:一份 2009 年提案在今天的投影

SEP-009 状态栏标注为 "Document in progress (being written)",但它描绘的骨架已在当前 Scrapy 中基本长成实体:

SEP-009 提案 当前仓库对应实现
单一根对象 Crawler,成员含 settings/extensions/engine/stats/signals Crawlersettings/signals/stats/engine/extensions 均为实例属性
组件在 __init__ 中接收 Crawler,作为访问其他组件的唯一机制 from_crawler(crawler) 约定 + build_from_crawler()
具体类由 STATS_CLASS 等设置项给出 _apply_settings()load_object(self.settings["STATS_CLASS"])(self)
命令行三步:加载 Settings → 构造根对象 → crawl() execute()get_project_settings()CrawlerProcess(settings) → 命令执行
库 API 两步:构造 Settings → 构造 Crawler Crawler(spidercls, settings) + crawl() / crawl_async()
未决问题:Spider 访问 settings 的 Option 1 / Option 2 Spider._set_crawler()self.settings = crawler.settingsself.crawler 同步可用

从单例到根对象的意义,不仅是"更干净的 API":它让依赖关系显式化、让同一进程内的多爬虫并行(CrawlerRunner / CrawlerProcess)成为可能,也让"把 Scrapy 当库用"获得了稳定可文档化的核心契约。研究 sep/ 目录下这份提案及其后续 SEP,是理解 Scrapy 内部架构设计动机的高效入口。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388