调度、去重与断点续抓
前置知识
本文基于模型知识整理,建议核对官方文档(见 参考资料)。
一句话定义
调度决定「下一个抓谁」,去重决定「不重复抓」,断点续抓保证「崩了不重来」;三者合起来是采集任务从「能跑」到「可靠」的分界线。
为什么重要
十万级 URL 的任务必然经历进程崩溃、机器重启、目标站临时故障。没有状态持久化的任务每次都从零开始;没有规范化去重的任务会在无形中翻倍请求量。本篇是 kp-022 分布式的直接前置。
前置知识
kp-010(URL 队列的产生)、kp-018(Scheduler 概念)。
核心概念
- URL 规范化:去 scheme 大小写、去 fragment、参数排序、去默认端口——同一资源归一为一个键。
- 去重层级:内存 set(快、易失)→ 持久化哈希集合 → 布隆过滤器(省内存、有误判)。
- 任务状态机:
pending → running → done / failed(retry<N),落库。 - 水位线增量:记录上次最大 ID/最新时间,只抓增量。
原理与机制
布隆过滤器(百万级 URL 去重的标准答案):
m 位位数组 + k 个哈希函数; 加入: 置 k 位; 查询: k 位全 1 则「可能存在」
误判率 FPR ≈ (1 - e^(-kn/m))^k (n=已插入元素数)
最优哈希数 k = (m/n)·ln2 ≈ 0.7·(m/n)
例: n=1000万, m=1.2亿位(15MB), k=8 → FPR ≈ 0.3%
状态持久化的最小实现(SQLite 单表既是队列又是账本):
import sqlite3
conn = sqlite3.connect("jobs.db")
conn.execute("""CREATE TABLE IF NOT EXISTS jobs(
url TEXT PRIMARY KEY, status TEXT DEFAULT 'pending',
tries INTEGER DEFAULT 0, updated_at TEXT)""")
def claim(batch=50):
rows = conn.execute(
"SELECT url FROM jobs WHERE status='pending' ORDER BY rowid LIMIT ?", (batch,)
).fetchall()
conn.executemany("UPDATE jobs SET status='running' WHERE url=?", [(r[0],) for r in rows])
conn.commit()
return [r[0] for r in rows]
def finish(url, ok: bool):
if ok:
conn.execute("UPDATE jobs SET status='done', updated_at=datetime('now') WHERE url=?", (url,))
else:
conn.execute("UPDATE jobs SET status=CASE WHEN tries<3 THEN 'pending' ELSE 'dead' END,"
" tries=tries+1 WHERE url=?", (url,))
conn.commit() # 每批提交 → 崩溃后从 pending 继续
每批 commit 是断点续抓的关键:崩溃只损失当前批,重启后 claim() 接着跑。
实例或案例
某 8 万 SKU 的详情采集在第 5 万个时因网络故障中断:由于 jobs 表持久化(done 5 万条已落盘),重启后任务从第 5 万零一条继续,仅补跑失败批。对比「全部重抓」方案,节省约 60% 的请求量与时长——状态落盘是采集可靠性的第一杠杆。
常见误区
- 误区一:
set()内存去重就完事。 进程重启全丢,重复抓取静默发生;规模化必须持久化或布隆。 - 误区二:URL 不规范化。
?a=1&b=2与?b=2&a=1、带不带#frag、http/https 混用都会造成假性不同 URL,去重形同虚设。 - 误区三:失败无限重试。 必须设 tries 上限进入
dead态并告警(kp-023),否则坏 URL 会永久占用调度循环。
自测题
- URL 规范化至少包含哪些规则?
答:scheme/host 小写、去 fragment、查询参数排序、去默认端口、去尾斜杠冗余。
- 布隆过滤器为什么会误判?能漏吗?
答:不同 URL 的哈希位可能全碰撞 → 误判「已存在」;只会误判存在、不会漏判未插入(不存在的 URL 至少一位为 0)。
- 断点续抓的最小要素?
答:任务状态持久化 + 批量提交 + pending 态可重复认领 + 失败计数上限。
与其他知识点的关系
kp-022 把本篇的 SQLite 队列升级为 Redis 共享队列即成分布式;kp-017 的限速约束调度速率;Scrapy 的 Scheduler/DUPEFILTER 是本篇思想的框架化。
延伸阅读
Scrapy 官方文档「Duplicates Filter / Scheduling」;维基百科 Bloom filter 词条(公式推导)。