应只对500、502、503、504及ConnectionError、Timeout等临时错误重试;429必须优先读Retry-After头;重试须包裹requests.get()单步,避免解析失败也重试;最大重试次数设为3–4;tenacity需在函数内定义重试逻辑以支持动态URL和headers;并发下各请求须独立退避,代理轮换须在重试前完成。

requests 重试时怎么避免被目标站当攻击流量?
直接用 time.sleep() 硬等是错的——它不感知响应头、不区分错误类型、也不防重试风暴。真正有效的退避必须结合状态码、Retry-After 头和网络异常分类。
关键点:
- 只对
500、502、503、504和网络异常(ConnectionError、Timeout)重试;429必须优先读Retry-After,而不是套指数退避 - 别在重试逻辑里包整个解析流程,只包裹
requests.get()这一步——否则解析失败也重试,浪费代理带宽 - 最大重试次数设为
3或4,再高只会加速 IP 被封,不是提高成功率
tenacity.retry 装饰器如何正确传入动态 URL 和 headers?
@retry 是函数级装饰器,定义时就固化了参数,无法自动感知每次调用的 url 或 headers。硬塞进去会导致所有重试共用同一套参数,出错时连日志都难定位。
正确做法是把重试逻辑下沉到请求函数内部:
立即学习“Python免费学习笔记(深入)”;
from tenacity import retry, retry_if_exception_type, wait_exponential, stop_after_attempt
def fetch_url(url, headers=None, proxies=None):
@retry(
retry=retry_if_exception_type((requests.exceptions.ConnectionError, requests.exceptions.Timeout)),
wait=wait_exponential(multiplier=1, min=1, max=10),
stop=stop_after_attempt(3)
)
def _request():
return requests.get(url, headers=headers, proxies=proxies, timeout=10)
return _request()
这样每次调用 fetch_url() 都会新建一个带当前参数的重试上下文,URL 和 headers 不会串。
高并发下多个请求共用同一个代理 IP,怎么防止退避策略互相干扰?
每个请求必须独立管理自己的重试状态。用全局计数器或共享锁会让所有请求串行化,彻底丧失并发意义。
实操要点:
- 不要在装饰器外维护重试次数变量,
tenacity默认按函数实例隔离状态,只要确保每次请求走独立函数调用即可 - 代理 IP 轮换必须在重试前完成——比如先从代理池取一个可用 IP,再进重试循环;不能重试三次都用同一个 IP
- 如果用了异步(
aiohttp),必须用tenacity.AsyncRetrying,普通@retry在协程里会阻塞整个 event loop
为什么指数退避比固定间隔更容易触发限流?
因为 wait_exponential(multiplier=0.1) 会产生 0.1s → 0.2s → 0.4s 这种极短间隔,实际是在高频刷接口,比固定 1s 更容易被识别为攻击。
真正安全的起点是:
-
min=1:首重试至少等 1 秒,给服务端恢复时间 -
max=10:防止某次卡死导致整个任务挂住 -
multiplier=1:标准 1s→2s→4s,节奏可控
最常被忽略的一点:退避只是兜底,优先级永远低于 Retry-After 响应头和代理 IP 的实时健康度反馈——后者失效时,再好的退避也救不回被封的 IP。


















