首页
/ Scrapy per-spider 设置与设置优先级体系全解析:SEP-019 设计与最终实现

Scrapy per-spider 设置与设置优先级体系全解析:SEP-019 设计与最终实现

2026-09-07 14:16:12作者:尤峻淳Whitney

SEP-019(Scrapy Enhancement Proposal 019,2013 年提出,状态为 Final / implemented with minor variations)为 Scrapy 引入了“每个 Spider 单独覆盖设置”的能力,并借此机会重构了设置(Settings)的装载顺序与整个爬取启动流程。本文基于该提案原文,对照当前仓库中 scrapy/settings/init.pyscrapy/spiders/init.pyscrapy/crawler.py 等源码,讲清 custom_settings 的用法、设置优先级模型、spider 管理器与启动流程的演化,让你知道设置最终由谁说了算、在哪个时点生效、如何正确地在项目/命令/单只蜘蛛三个层级上叠加配置。

SEP-019 想解决什么问题

在 SEP-019 之前,Scrapy 的配置来源单一:项目 settings.py 里的设置在全局生效,同一项目内的不同 Spider 若需要不同的下载延迟、重试策略或并发数,只能通过运行时传入的参数或对全局配置做临时修改(依赖 settings.overrides 这类实现),既容易产生“先改先得、后改覆盖”的顺序依赖,也容易让配置在爬虫启动后仍被意外改动,难以排查。

SEP-019 提出两件事:

  1. 在 Spider 上新增一个按蜘蛛粒度的设置入口,让设置覆盖在实例化 crawler 之前完成;
  2. 顺带梳理“设置装载 → spider 加载 → crawler 创建 → 启动”这条链路,统一各处对设置进行覆盖的方式。

提案给出了目标用法:

class MySpider(Spider):
    @classmethod
    def custom_settings(cls):
        return {
            "DOWNLOAD_DELAY": 5.0,
            "RETRY_ENABLED": False,
        }

提案同时声明这是“设计方案”,最终落地时有多处细节变化(详见下文各节),但“每个蜘蛛能用自己的设置 + 全链路用统一优先级装载设置”这两条主线最终都在当前代码中实现了。

设置优先级:从“顺序覆盖”到“按优先级覆盖”

提案中的设计:SettingsLoader 与 SettingsReader 拆分

SEP-019 指出旧 Settings 类同时承担“装载”与“读取”两种职责,容易让人误以为设置装载完成后仍可随意修改。提案将其拆为两个类:

  • SettingsLoader:负责在启动阶段装载各层来源的设置。设计上是“写多读少”,但实际仍需支持读取——因为 COMMANDS_MODULE 需要据此加载命令自身的默认设置,LOG_* 系列设置必须在装载阶段就能被读取以便输出日志,ScrapyCommands 也要依据当前设置决定如何注册命令。因此它是读写均可的,并提供 set(name, value, priority)setdict(dict, priority)、沿用现有 get/getint/getfloat/getdict 等 getter,最后通过 freeze() 返回一份已按优先级冻结的只读实例。
  • SettingsReader:供 core、扩展及所有“只读不改”的组件使用。它是只读的,沿用全部 getter;若有人误调 set,则应抛出带说明的报错以便调试与兼容。
  • 优先级概念:提案提出默认 5 级优先级,使覆盖与调用顺序无关——高优先级设置无论先 set 还是后 set,都不会被低优先级覆盖。settings.overrides 因此被废弃。

最终落地:BaseSettings / Settings + SETTINGS_PRIORITIES

当前仓库没有严格拆成两个类,而是用一个带优先级与冻结能力的 BaseSettings 承载读写与冻结语义,再以自动装载默认值的 Settings 作为其在项目中的入口(见 scrapy/settings/init.py#L81-L100scrapy/settings/init.py#L698-L720)。核心数据结构 SettingsAttribute 记录了每个设置的 valuepriority,其 set() 实现“只有传入优先级不低于当前优先级时才覆盖”,这正是优先级模型使覆盖不再依赖调用顺序的关键:

def set(self, value: Any, priority: int) -> None:
    """Sets value if priority is higher or equal than current priority."""
    if priority >= self.priority:
        ...
        self.value = value
        self.priority = priority

优先级常量定义在 scrapy/settings/init.py#L33-L40

SETTINGS_PRIORITIES: dict[str, int] = {
    "default": 0,
    "command": 10,
    "addon": 15,
    "project": 20,
    "spider": 30,
    "cmdline": 40,
}

对照提案的五级默认优先级,最终实现基本一一对应,并额外插入了 addon 这一级:

数值 名称 来源 提案中对应项
0 default scrapy/settings/default_settings.py 中的全局默认值 0: global defaults
10 command 各命令自带默认设置(如 shell 默认 KEEP_ALIVE=True 10: per-command defaults
15 addon 插件(Addon)对设置的修改,为最终实现新增
20 project 项目 settings.py(经 setmodule / setdict 装载) 20: project settings
30 spider Spider.custom_settings 返回的字典 30: per-spider settings
40 cmdline 命令行参数(含 -s 选项) 40: command line arguments

读取任一设置时返回的是最高优先级的值。Settings 构造时先把 default_settings"default" 优先级装载(scrapy/settings/init.py#L709-L720),随后各层 set 只在优先级足够时生效。

冻结语义:freeze / frozencopy

提案中 SettingsLoader.freeze() 的“变成不可修改的只读快照”语义,在最终实现里由 freeze()frozencopy() 承担:

  • freeze():把当前对象标记为 frozen = True,此后任何 set/setdict/setmodule 都会先经 _assert_mutability() 检查并抛出 TypeError("Trying to modify an immutable Settings object")scrapy/settings/init.py#L616-L618L632-L640);
  • frozencopy():等价于“深拷贝一份再冻结”,用于把已装载好、尚未冻结的设置安全地交给只读方(L642-L650)。

scrapy/crawler.pyCrawler._apply_settings() 中可以看到实际时序:等 spider 设置合并进 self.settings 后,组件陆续构建,最后调用 self.settings.freeze()scrapy/crawler.py#L210-L211),crawler 生效的就是这份冻结后的配置。

Spider 级设置:custom_settings 的用法与最终形态

提案用法:classmethod 返回 dict

SEP-019 原始提案是 custom_settings 作为 classmethod、返回一个 dict。从源码结构看,若实现为 classmethod 可支持动态计算(例如按某个类属性拼接配置);不过当前仓库最终采用的是类属性 dict 的形式,同时保留了 classmethod 形式的 update_settings 用于把该字典“注入”设置系统。

当前代码中的真实写法

scrapy/spiders/init.py 中,Spider 基类把 custom_settings 声明为可空 dict 类属性:

class Spider(object_ref):
    name: str
    custom_settings: dict[str, Any] | None = None

使用时的标准写法:

from scrapy import Spider


class BooksSpider(Spider):
    name = "books"
    custom_settings = {
        "DOWNLOAD_DELAY": 2.0,          # 该蜘蛛请求间固定延迟 2 秒
        "RETRY_ENABLED": False,          # 该蜘蛛不启用重试
        "CONCURRENT_REQUESTS_PER_DOMAIN": 4,
    }
    start_urls = ["https://example.com/books"]

实际把字典写进设置的方法是 classmethod update_settings,其实现正是提案所说“一次带优先级的 set 调用”:

@classmethod
def update_settings(cls, settings: BaseSettings) -> None:
    settings.setdict(cls.custom_settings or {}, priority="spider")

即所有 custom_settings 条目统一以优先级 spider(数值 30)装载。该调用发生在 Crawler.__init__ 里——创建 crawler 时先把蜘蛛类设置合入,再据此构建引擎组件:

self.settings: Settings = settings.copy()
self.spidercls.update_settings(self.settings)   # scrapy/crawler.py#L129-L131

由此得出几个可直接用于排障的结论:

  • custom_settings 会覆盖项目 settings.py(20 < 30),但会被命令行参数覆盖(40 > 30),例如 scrapy crawl books -s DOWNLOAD_DELAY=0 仍然有效;
  • 它必须在 crawler 实例化前生效,因此**“在爬虫运行中途修改 settings 不会生效”**——crawler 构建完组件后即调用 freeze()
  • 同一字典内同名 key 只写一份,不存在自身冲突。

相关测试佐证

仓库大量子进程式测试直接依赖这一机制,例如 tests/CrawlerRunner/change_reactor.pytests/CrawlerRunner/custom_loop_same.pytests/AsyncCrawlerRunner/reactorless_custom_settings.py 均通过 custom_settings = {...} 为单只测试蜘蛛指定 Twisted reactor / 事件循环等关键设置,验证了 spider 级设置能影响引擎启动路径;tests/test_cmdline_crawl_with_pipeline/test_spider/spiders/normal.py 亦用于验证命令行启动时 spider 设置的装载。可见这是框架与测试共同依赖的一等公民特性,而非历史遗留。

Spider 从 crawler 中剥离:spider 管理器与 from_crawler

提案背景

SEP-019 观察到当时的 spider manager 挂在 Crawler 上,造成 settings 与 spiders 之间的循环依赖。提案主张:spiders 应在 crawler 之外加载、再以“蜘蛛类”的形式传入 Crawler;spider manager 不再访问设置,而是改用 scrapy.cfg 自我配置,并设想 scrapy.cfg 形如:

[settings]
default = myproject.settings

[spiders]
manager = scrapy.spidermanager.SpiderManager
modules = myproject.spiders

其中 manager 取代 SPIDER_MANAGER_CLASS(缺省为内置 SpiderManager),modules 取代 SPIDER_MODULES 且必填。SpiderManager.__init__ 因此不再接收 spider_modules,改为从 scrapy.cfg 读取;create('spider_name', **kargs) 被改写为 load('spider_name')——返回蜘蛛类而非实例,其余依赖 crawler 引用的方法被废弃;并新增辅助函数 get_spider_manager_class_from_scrapycfg

最终实现保留了“返回类”的核心,但配置入口仍在 settings

从当前源码看,[spiders] 小节这一具体形态最终没有按原样落地:蜘蛛发现仍由设置驱动,即项目 settings.py 中的 SPIDER_MODULES(默认空列表)声明加载哪些模块、SPIDER_LOADER_CLASS(默认 "scrapy.spiderloader.SpiderLoader")声明用哪个加载器实现,见 scrapy/settings/default_settings.py#L566L581。而 scrapy.cfg 只保留 [settings] default = myproject.settings 这类“指向 settings 模块”的职责(模板见 scrapy/templates/project/scrapy.cfg),SPIDER_MANAGER_CLASS 该命名也未进入当前默认设置。

“蜘蛛以类为粒度被查找、crawler 只负责持有蜘蛛类”这一核心却保留了下来:CrawlerRunnerBase._create_crawler() 在收到字符串蜘蛛名时会调用 self.spider_loader.load(spidercls) 拿到蜘蛛类,再 Crawler(spidercls, self.settings)scrapy/crawler.py#L481-L484)。也就是说:Loader 出“类”,Crawler 出“实例”,两者职责清晰,正是 SEP 想达到的解耦。

from_crawler:蜘蛛访问 crawler 组件的唯一正道

SEP-019 提出为 Spider 增加 from_crawler classmethod,作为“蜘蛛拿到 settings、stats 与 crawler 内部组件”的新式入口。最终实现在 scrapy/spiders/init.py#L78-L87

@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 实例拿到 self.crawler(可访问 crawler.settingscrawler.stats 等)并自动注册 spider_closed 信号。crawler 侧唯一实例化点就在 Crawler.crawl() 中:spider = self.spidercls.from_crawler(self, *args, **kwargs)scrapy/crawler.py#L327)。因此自定义中间件、扩展与项目代码中创建蜘蛛时,都应以 Spider.from_crawler(crawler) 为准,而非直接 MySpider(**kwargs) 实例化。

命令与启动流程的改写

提案指出各命令的 process_option 会读写覆盖设置,应统一改为“按分配好的优先级 set”;每个带自定义 run 的命令(尤其 crawl 命令)都要适配新 API。提案用两份伪代码对比了新旧启动流程:

# 旧的启动流程(节选)
settings = get_project_settings()
settings.defaults.update(cmd.default_settings)
cmd.crawler_process = CrawlerProcess(settings)
cmd.run
    # Command.run in commands/crawl.py
    self.crawler_process.create_crawler()
    spider = crawler.spiders.create(spider_name, **spider_kwargs)
    crawler.crawl(spider)
    self.crawler_process.start()
# 提案的新启动流程(节选)
smcls = get_spider_manager_class_from_scrapycfg()
sm = smcls()                     # 按 scrapy.cfg 中声明的模块加载 spiders
spidercls = sm.load(spider_name) # 返回 spider 类而非实例

settings = get_project_settings()   # 装载 settings.py
settings.setdict(cmd.default_settings, priority=40)
settings.setdict(spidercls.custom_settings(), priority=30)
settings = settings.freeze()
cmd.crawler = Crawler(spidercls, settings=settings)
cmd.run
    # Command.run in commands/crawl.py
    self.crawler.crawl(**spider_kwargs)
        # Crawler.crawl in crawler.py
        spider = self.spidercls.from_crawler(self, **spider_kwargs)

其中值得注意的细节是提案把 cmd.default_settings 标为 40(与命令行同级)。而 SEP-019 也敏锐地指出一个潜在问题:若 -s LOG_ENABLE=False --loglevel=ERROR 同时出现,由于命名选项在实现中晚于 -s 被处理,LOG_ENABLE 可能又被置回 True;提案的处理结论是“命令行的命名选项与 -s 可保持同一优先级、仅依赖处理顺序”。

对比当前实现,新流程的骨架基本被采纳:项目设置装载走 get_project_settings()(见 scrapy/utils/project.py#L66-L97,内部以 "project" 优先级 setmodule/setdict);命令默认设置由命令对象以相应优先级并入;spider 类经 spider_loader.load(...) 取得后再创建 Crawler;蜘蛛真正被实例化的时点推迟到了 Crawler.crawl() 内的 from_crawler 调用,命令层只持有“蜘蛛类 + 已冻结设置”的 Crawler。命令里那些需“先读再改”的设置(如 COMMANDS_MODULE 引入的额外默认设置、LOG_* 的早期读取)也正因为 Settings 同时具备读写能力与优先级语义而变得有序。

CrawlerProcess 的归宿:未删除,但职责收敛

SEP-019 同时提出了一个“顺带清理”的激进方案:既然 crawl 命令不再支持同时跑多只蜘蛛,CrawlerProcess 应被删除,其能力并入 Crawler——具体是把 create_crawler 变更为 Crawler.__init___start_crawler 并入 Crawler.startstart 并入 Crawler.crawl(并为后者增加 start_reactor 布尔参数以便 shell 命令在线程里手动起 reactor),多蜘蛛运行改由用户用 API 手动为每只蜘蛛各建一个 Crawler。

这一“删除”最终并未以提案原样执行:当前仓库中 CrawlerRunnerBase/CrawlerRunner/CrawlerProcess 依然共存(见 scrapy/crawler.py),且 CrawlerRunnerBase.create_crawler() 已实现“给定 Crawler 实例、蜘蛛类或蜘蛛名三者之一都能产出一个 Crawler”的统一入口(scrapy/crawler.py#L455-L484)。但从设计取向上看,提案的合理部分——每个 Crawler 独立携带蜘蛛类与自己的设置、一次 crawl 对应一只蜘蛛、多蜘蛛并发交由上层 Runner 协调——已成为当前架构的默认心智模型:真正“一次跑多只”的场景走 CrawlerRunner.crawl() / AsyncCrawlerRunner.crawl(),而 CrawlerProcess.crawl() 则是带 reactor 生命周期管理的便捷封装。提案中“crawl 命令不再内建支持多蜘蛛”这一点同样成立:多蜘蛛场景应使用脚本化 API。

向后兼容与废弃项

SEP-019 还规划了几处兼容处理:

  • scrapy.conf.settings 单例:作为历史遗留的“设置装载单例”,提案允许其存续,但要求其内部转用新的只读接口,以免继续误导使用者。
  • settings.overrides:提案建议废弃,改由带优先级的显式装载替代。这一点已彻底贯彻——当前 scrapy/settings/init.py 中不再有 overrides 属性,取而代之的是 set/setdict/setmodule/update 这一整套带 priority 参数的写入 API。
  • CrawlerSettings(含 overrides、settings_module、defaults 三个字段):提案建议删除,若为兼容可把接口并入只读侧;但 SEP 同时指出“保留这些可变属性会破坏只读性,不推荐”。当前代码中已无该类。
  • 环境变量与 -s 的交互:提案点名 SCRAPY_PICKLED_SETTINGS_TO_OVERRIDE 等环境变量需要被废弃或以合适的优先级保留。当前仓库对合法 SCRAPY_* 环境变量(如 SCRAPY_SETTINGS_MODULE)仍会读取,并以 "project" 优先级写入(scrapy/utils/project.py#L82-L95),可见这部分最终收敛为“部分环境变量保留在 project 优先级”的形态。
  • 旧式蜘蛛属性的桥接:若蜘蛛仍写 download_delay/max_concurrent_requests 这类旧属性,scrapy/crawler.py#L218-L219_apply_deprecated_spider_attr 会在冻结前把它们桥接到 DOWNLOAD_DELAY / CONCURRENT_REQUESTS_PER_DOMAIN,保证老代码不失效;scrapy/utils/deprecate.py#L231 则提示改用 Spider.custom_settingsSpider.update_settings()

结语:今天的 Scrapy 里如何按 SEP-019 的思路配置

用一句话概括 SEP-019 的遗产:设置从一个“全局可变字典”变成了一套“带优先级、可按层级冻结”的配置管线,而 Spider 成为其中一个正式层级。落地到你的日常编码中:

  1. 全项目公共项放项目 settings.py(project 优先级);
  2. 单只蜘蛛的差异化项放 custom_settings(spider 优先级),它一定压得过项目配置;
  3. 临时调试用命令行 -s KEY=VALUE(cmdline 优先级),它能压得过蜘蛛与项目配置;
  4. 想在蜘蛛里访问解析后的配置,在 from_crawler 或实例方法里读 self.settings 即可,但不要试图在 crawl 开始后再改动它——crawler 完成装配后 settings 已被冻结,任何写入都会抛 TypeError

参考此文的背景设计全文见 sep/sep-019.rst,设置 API 的完整实现见 scrapy/settings/init.py,crawler 装配链路见 scrapy/crawler.py,蜘蛛基类见 scrapy/spiders/init.py

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