kp-017

礼貌抓取:限速、重试与超时工程

核心 ≈ 25 分钟 限速令牌桶退避重试超时礼貌

前置知识

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

一句话定义

礼貌抓取 = 把「请求多快、失败怎么办、多久算超时」变成显式参数与算法(限速器 + 指数退避 + 分层超时),既保护目标系统也保护自己的任务。

为什么重要

无节制的采集是 kp-004 伦理红线的直接触发器,也是任务自身崩溃的第一原因(429 雪崩、连接堆积)。限速与退避是爬虫工程化(kp-018 起)的底层素养,写进每一个采集脚本。

前置知识

kp-006(请求骨架与 timeout)。

核心概念

  • 限速:控制单位时间请求数;固定间隔太规律,应加随机抖动。
  • 令牌桶:以恒定速率 r 生成令牌、桶容量 b 的经典限速算法,允许小幅突发。
  • 指数退避 + 抖动:失败后按 wait = base × 2^attempt × random(0.5, 1.5) 递增等待。
  • Retry-After:服务器在 429/503 中给出的建议等待秒数,必须优先尊重。
  • 并发上限:单主机同时 1–2 个在途请求是默认礼貌水位。

原理与机制

令牌桶模型:桶以速率 r 补令牌,每个请求消耗 1 枚,空桶则等待。平均速率 = r,同时容忍 b 枚突发,平滑又不失吞吐。

容量 b=5, 速率 r=2/s 的行为:
t=0s   桶满5枚 → 可瞬间发5个(突发)
t=0.5s 补1枚   → 又可发1个
持续速率: 2 req/s, 任何时刻在途请求数受桶约束

可直接复用的限速退避组件:

import time, random
from collections import deque

class TokenBucket:
    def __init__(self, rate: float, capacity: int = 1):
        self.rate, self.cap = rate, capacity
        self.tokens, self.t = capacity, time.monotonic()
    def take(self):
        while True:
            now = time.monotonic()
            self.tokens = min(self.cap, self.tokens + (now - self.t) * self.rate)
            self.t = now
            if self.tokens >= 1:
                self.tokens -= 1
                return
            time.sleep((1 - self.tokens) / self.rate)

bucket = TokenBucket(rate=1.0)          # 单主机 1 QPS 起步
def polite_sleep():
    bucket.take()
    time.sleep(random.uniform(0.5, 1.5))  # 抖动打散节奏

def backoff(attempt: int, retry_after: str | None = None):
    if retry_after and retry_after.isdigit():
        return int(retry_after)                       # 优先尊重服务器
    return min(60, 2 ** attempt) * random.uniform(0.5, 1.5)

实例或案例

对比实验:同一 200 页任务,无限速直跑在约 120 页时收到 429 并被临时封禁 10 分钟(总耗时反而更长);加 1 QPS + 抖动后一次跑完。慢而稳的总时长经常优于快而断,这是新手最难内化的工程直觉。

常见误区

  • 误区一:sleep(1) 当限速。 固定间隔极易被识别且不处理突发;令牌桶/抖动才是合格实现。
  • 误区二:忽略 Retry-After 与 429。 429 后继续请求会把临时限流升级为封禁。
  • 误区三:只限速不限并发。 10 个线程各 sleep 1 秒仍是 10 QPS;并发与速率必须同时约束。

自测题

  1. 令牌桶的 r 与 b 各控制什么?

答:r 控制长期平均速率;b 控制允许的瞬时突发量。

  1. 指数退避为什么要乘随机抖动?

答:避免失败请求同步重试形成「重试风暴」;抖动把重试打散。

  1. 单主机的默认礼貌水位是多少?

答:并发 1–2 个在途请求、低个位数 QPS 起步,并参考 robots.txt 的 Crawl-delay。

与其他知识点的关系

kp-018 的 Scrapy 内建 AUTOTHROTTLE 与重试中间件是本篇的框架化实现;kp-021 的调度同样以限速为约束;kp-016 的第 3 层防御正是本篇的对偶面。

延伸阅读

Scrapy 官方文档「AutoThrottle」;MDN「HTTP 429」。

相关知识点

学习进度