kp-021

调度、去重与断点续抓

进阶 ≈ 30 分钟 调度去重布隆过滤器断点续抓状态持久化

前置知识

本文基于模型知识整理,建议核对官方文档(见 参考资料)。

一句话定义

调度决定「下一个抓谁」,去重决定「不重复抓」,断点续抓保证「崩了不重来」;三者合起来是采集任务从「能跑」到「可靠」的分界线。

为什么重要

十万级 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 会永久占用调度循环。

自测题

  1. URL 规范化至少包含哪些规则?

答:scheme/host 小写、去 fragment、查询参数排序、去默认端口、去尾斜杠冗余。

  1. 布隆过滤器为什么会误判?能漏吗?

答:不同 URL 的哈希位可能全碰撞 → 误判「已存在」;只会误判存在、不会漏判未插入(不存在的 URL 至少一位为 0)。

  1. 断点续抓的最小要素?

答:任务状态持久化 + 批量提交 + pending 态可重复认领 + 失败计数上限。

与其他知识点的关系

kp-022 把本篇的 SQLite 队列升级为 Redis 共享队列即成分布式;kp-017 的限速约束调度速率;Scrapy 的 Scheduler/DUPEFILTER 是本篇思想的框架化。

延伸阅读

Scrapy 官方文档「Duplicates Filter / Scheduling」;维基百科 Bloom filter 词条(公式推导)。

相关知识点

学习进度