首页
/ Scrapy 断点续爬详解:JOBDIR 作业目录、持久化状态与恢复原理

Scrapy 断点续爬详解:JOBDIR 作业目录、持久化状态与恢复原理

2026-09-06 12:23:36作者:董灵辛Dennis

本篇基于 Scrapy 官方文档 docs/topics/jobs.rst 展开,讲解 Scrapy 原生的"暂停/恢复爬取"(Jobs: pausing and resuming crawls)能力:如何通过 JOBDIR 设置启用作业目录持久化,爬取中断后如何用同一条命令无损续爬,如何用 spider.state 在批次之间保存爬虫私有状态,以及请求序列化、作业目录文件结构等实现细节。读完后你可以为大站爬取任务设计可暂停、可恢复、可审计的运行方案,并能看懂 Scrapy 在磁盘上到底写了哪些状态文件。

为什么需要暂停与恢复:Scrapy 提供的三项设施

对于大型站点,一次爬完往往不现实:网络抖动、代理失效、人工审核节点……都可能要求你中途停掉爬虫、之后再接着爬。Scrapy 开箱即用地支持这一场景,其背后由三套相互协作的持久化设施支撑(见 docs/topics/jobs.rst):

  • 磁盘持久化的调度器(Scheduler):把尚未下载的请求按优先级队列持久化到磁盘,恢复时继续出队;
  • 磁盘持久化的去重过滤器(DupeFilter):把"已见过的请求指纹"落盘,恢复后不会重复请求;
  • 爬虫状态扩展(SpiderState):在批次之间持久化爬虫的键值状态(spider.state 字典)。

作业目录(Job directory):通过 JOBDIR 启用持久化

启用持久化的方式是通过 :setting:JOBDIR`` 设置定义一个作业目录。该目录保存单个任务(即一次 spider 运行)所需的全部状态数据,使得爬取在干净地停止后可以被恢复。

有两点重要约束,文档中明确以 warning 形式给出:

  1. 作业目录不能共享。同一个作业目录不能由不同 spider、甚至同一 spider 的不同任务共用,否则磁盘队列状态与去重指纹会互相污染;
  2. 安全级别等同于源码。不要把 JOBDIR 指向不可信用户可以写入的路径——恢复时会 pickle.load 其中的状态文件,写入恶意序列化数据等于执行任意代码。

从源码看,JOBDIRscrapy/settings/default_settings.py 中默认为 None,即默认不启用任何持久化。设置生效后,scrapy/utils/job.py 中的 job_dir() 函数负责统一入口:若目录不存在则自动 mkdir(parents=True) 创建,然后返回路径。调度器、去重过滤器、SpiderState 扩展都通过它拿到同一个目录,这保证了各组件写入位置的一致性。

如何使用:启动、安全停止、恢复

官方文档给出的操作非常直接——用命令行 -s 参数临时覆盖设置即可:

scrapy crawl somespider -s JOBDIR=crawls/somespider-1

之后你可以在任意时刻安全地停止爬虫(按 Ctrl-C 或发送信号停止),并在之后用完全相同的命令恢复:

scrapy crawl somespider -s JOBDIR=crawls/somespider-1

恢复时的行为可以从 scrapy/core/scheduler.pyScheduler._dq() 中得到印证:调度器打开磁盘队列时会先读取 active.json 中的状态(startprios,即上次暂停时仍非空的优先级桶),重建磁盘优先队列;如果队列非空,会打印一行明确的恢复日志:

Resuming crawl (%(queuesize)d requests scheduled)

其中 queuesize 即从磁盘恢复出的待下载请求数。

从 spider 名称派生作业目录

避免不同任务共享目录的实用做法,是让一组 spider 通过 :meth:~scrapy.Spider.update_settings`` 统一拼出"仅 spider 名不同"的路径。官方文档给出的示例:

from pathlib import Path

from scrapy import Spider


class BaseSpider(Spider):
    @classmethod
    def update_settings(cls, settings):
        super().update_settings(settings)
        settings.set("JOBDIR", str(Path("crawls", cls.name)), priority="spider")

update_settingsscrapy/spiders/init.py 中定义的类方法钩子,默认实现是把 custom_settingspriority="spider" 注入设置对象。子类重写它并在 super() 之后追加 JOBDIR,即可让继承自 BaseSpider 的每个爬虫自动获得 crawls/<spider名> 这样的独立作业目录,命令层面甚至无需再传 -s JOBDIR

批次之间的持久状态:spider.state

暂停/恢复会把一次长任务切分成多个"批次"。如果你需要在批次之间保留一些爬虫私有状态(已处理条数、上次游标位置、临时计数器等),可以使用 spider.state 属性——它应当是一个字典。内置扩展 SpiderState 会在 spider 启动和停止时负责把它序列化、存盘、加载。

文档中的回调示例(其余 spider 代码省略):

def parse_item(self, response):
    # parse item here
    self.state["items_count"] = self.state.get("items_count", 0) + 1

扩展的实现非常薄,见 scrapy/extensions/spiderstate.py

  • from_crawler 中若 JOBDIR 未设置则抛出 NotConfigured 直接不加载;否则连接 spider_opened / spider_closed 两个信号;
  • spider_opened:若 spider.state 文件已存在则 pickle.load 恢复到 spider.state,否则初始化为空字典 {}——也就是说即便没有持久化,该扩展也会保证 spider.state 属性存在(测试 tests/test_spiderstate.py 专门验证了这一点);
  • spider_closed:把 spider.state 以 pickle 协议 4 写入作业目录下的 spider.state 文件。

test_store_load 用例 演示了完整的存取闭环:第一次 spider_opened → 写入状态 → spider_closed,第二次 spider_opened 后断言状态字典(含 datetime 这类可 pickle 对象)完整还原。

持久化的注意事项(Gotchas)

文档用专门一节列出了四个必须记住的坑,它们直接决定你的作业目录是否可靠。

暂停限制:只有"干净停止"才保证可恢复

作业暂停/恢复仅在爬虫被干净地停止时受支持。强制、突发或非干净的关闭(如 kill -9、断电)可能导致作业目录中的数据损坏,使 spider 无法正确恢复。这也是为什么停止时应优先使用 Ctrl-C / 信号触发的正常关闭流程,让调度器的 close() 有机会落盘(见 scrapy/core/scheduler.pyclose() 会把磁盘队列状态写入 active.json,再关闭去重过滤器)。

Scrapy 版本变化:作业目录是"实现细节"

作业目录的内容是写出它的那个 Scrapy 版本的实现细节。必须用暂停它时的同一 Scrapy 版本来恢复;升级或降级 Scrapy 之后,应使用新的作业目录开始新任务。DownloaderAwarePriorityQueue 的源码里甚至有针对状态结构不匹配的显式检查:恢复时若 startprios 不是预期的 dict,会直接抛出错误提示"Only a crawl started with the same priority queue class can be resumed"(见 scrapy/pqueues.py)。

Cookie 过期

Cookie 可能过期。如果暂停时间过长,队列中已调度的请求可能不再有效。如果你的 spider 不依赖 Cookie,则不受此影响。

请求序列化

持久化要求 :class:~scrapy.Request`` 对象能够被 pickle 序列化——但 __init__ 中传入的 callback/errback 除外,它们必须是正在运行的 Spider 类的方法

无法序列化的请求只保留在内存中:它们仍然会被发送,但在爬取暂停时会丢失。这一点在源码中的实现路径是:

  1. Scheduler.enqueue_request 先尝试压入磁盘队列(scrapy/core/scheduler.py);
  2. 磁盘队列底层 PickleLifoDiskQueue.push 中,scrapy/squeues.py_pickle_serialize 捕获 PicklingError/AttributeError/TypeError 并转成 ValueError 抛出;
  3. 调度器捕获该 ValueError 后,把请求回退到内存队列,并递增统计项 scheduler/unserializable_dqpush,scrapy/core/scheduler.py)。

如果你想记录这些无法序列化的请求,把 SCHEDULER_DEBUG 设置为 True(默认 False,见 scrapy/settings/default_settings.py)。开启后调度器会打印第一个失败样本:

Unable to serialize request: %(request)s - reason: %(reason)s
- no more unserializable requests will be logged (stats being collected)

注意源码中 logunser 标志在记录一次后即置为 False,后续只累计统计、不再刷屏;统计项 scheduler/unserializable 会持续增长,可用来评估丢失规模。

另一个容易被忽视的语义细节(文档以 note 给出):由于请求用 pickle 序列化,存放在请求上的对象——如 cb_kwargsmeta 字典的值——在写入作业目录和读回时会被深拷贝。回调收到的是副本而非原对象,对副本的修改不会反映回原对象。如果你的代码依赖通过 cb_kwargs/meta 共享可变状态,必须注意这一点。

作业目录的内容结构与各组件的写入分工

作业目录的内容取决于该作业实际使用了哪些组件。已确认会写入作业目录的组件包括调度器(Scheduler 参考)和 SpiderState 扩展。以默认设置为例,目录结构大致如下:

├── requests.queue
|   ├── active.json
|   └── {hostname}-{hash}
|       └── {priority}{s?}
|           ├── q{00000}
|           └── info.json
├── requests.seen
└── spider.state

各文件/目录的产生者及机制,均可在源码中找到对应实现:

  • requests.queue/ 目录与 active.jsonScheduler 创建。_dqdir() 在打开时创建 requests.queue 子目录(scrapy/core/scheduler.py);active.json 存放 SCHEDULER_PRIORITY_QUEUE(默认 DownloaderAwarePriorityQueue)状态(各下载槽仍非空的优先级列表 startprios)的 JSON 表示,在任务干净停止时由 close()_write_dqs_state() 写出、恢复时由 _read_dqs_state() 读回(scrapy/core/scheduler.py);
  • {hostname}-{hash} 目录DownloaderAwarePriorityQueue 按下载槽(域名)创建。槽名经 _path_safe 处理:非法字符替换为下划线并追加 MD5 哈希后缀以避免冲突,因此文档中写作 {hostname}-{hash}
  • {priority}{s?} 目录ScrapyPriorityQueue 创建:目录名是取反后的请求优先级(整数,越小越优先);s 后缀表示该桶存放 start 请求。默认设置下 start 请求与普通请求使用不同的队列类——SCHEDULER_START_DISK_QUEUE = "scrapy.squeues.PickleFifoDiskQueue"(FIFO),而普通请求的 SCHEDULER_DISK_QUEUE = "scrapy.squeues.PickleLifoDiskQueue"(LIFO),因此默认爬取呈深度优先(DFO)顺序;
  • info.jsonq{00000} 文件PickleFifoDiskQueue 这类队列创建,它们是 queuelib.FifoDiskQueue 的子类,用 pickle 序列化 Request.to_dict() 的 dict 表示(见 _scrapy_serialization_queue),出队时再用 request_from_dict 还原为 Request 并绑定回 spider;
  • requests.seen 文件RFPDupeFilter 创建。每个指纹以"大端 2 字节长度前缀 + 指纹字节"追加写入(scrapy/dupefilters.py);打开时 _read_fingerprints() 顺序解析,若发现被非正常关机截断的残缺记录则停止加载(scrapy/dupefilters.py)——这是源码层面对"非干净关闭可能损坏作业目录"这一警告的具体体现;
  • spider.state 文件SpiderState 扩展创建,即前文所述的 pickle.dump(spider.state, f, protocol=4)

需要注意的默认配置全貌(scrapy/settings/default_settings.py):

设置项 默认值
JOBDIR None(不启用持久化)
SCHEDULER scrapy.core.scheduler.Scheduler
SCHEDULER_PRIORITY_QUEUE scrapy.pqueues.DownloaderAwarePriorityQueue
SCHEDULER_DISK_QUEUE scrapy.squeues.PickleLifoDiskQueue
SCHEDULER_MEMORY_QUEUE scrapy.squeues.LifoMemoryQueue
SCHEDULER_START_DISK_QUEUE scrapy.squeues.PickleFifoDiskQueue
SCHEDULER_START_MEMORY_QUEUE scrapy.squeues.FifoMemoryQueue
SCHEDULER_DEBUG False
DUPEFILTER_CLASS scrapy.dupefilters.RFPDupeFilter

小结:可复制的断点续爬工作流

  1. 为每个 spider(或每个任务批次)规划独立目录,如 crawls/<spider名>-1,切勿跨任务共享;
  2. 启动:scrapy crawl somespider -s JOBDIR=crawls/somespider-1
  3. 需要暂停时用 Ctrl-C/信号干净停止,让 Scheduler.close() 完成 active.json 落盘;
  4. 恢复:执行同一条命令,观察日志中的 Resuming crawl (N requests scheduled) 确认队列恢复数量;
  5. 若爬虫依赖 cookie 或不可 pickle 的对象(自定义 callback、cb_kwargs 中的活对象等),评估过期与丢失风险,必要时用 SCHEDULER_DEBUG=True 审计 scheduler/unserializable 统计;
  6. 升级/降级 Scrapy 版本后,放弃旧作业目录、另起新目录。

核心文件索引:官方文档 docs/topics/jobs.rstdocs/topics/scheduler.rstdocs/topics/extensions.rst;实现 scrapy/core/scheduler.pyscrapy/dupefilters.pyscrapy/pqueues.pyscrapy/squeues.pyscrapy/extensions/spiderstate.pyscrapy/utils/job.py;测试 tests/test_spiderstate.py

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