可在自定义下载中间件的process_response中解析响应体,对业务错误码(如code=1001/1002)调用request.copy()构造新请求并更新retry_times,优先级设为550以在默认retrymiddleware之后执行。

Scrapy中间件里怎么改重试逻辑?
默认的 RetryMiddleware 会根据状态码(如500、502、503、504、408)和网络异常自动重试,但实际爬虫中常需要更细粒度控制:比如只对特定URL重试、跳过某些错误码、或在重试前加延迟/换User-Agent。直接覆盖默认中间件不如在它之后插入自定义中间件来得稳妥。
关键点是:不要禁用 RetryMiddleware,而是在其 process_response 和 process_exception 阶段介入,通过修改 response.request.meta 中的 retry_times 或手动调用 request.copy() 构造新请求。
- 把自定义中间件放在
settings.py的DOWNLOADER_MIDDLEWARES字典里,优先级数值比scrapy.downloadermiddlewares.retry.RetryMiddleware(默认500)更高(如550),确保它后执行 - 重试判断别只依赖
response.status——有些接口返回200但body里是{"code":500},得解析响应体再决定是否重试 - 手动重试时务必用
request.replace()或request.copy(),不能直接修改原request,否则可能引发并发冲突
如何在process_response里拦截并触发重试?
这是最常用场景:收到响应后检查业务逻辑失败(比如JSON字段缺失、token过期),主动要求重试。注意不能直接返回 None 或原始 response,而要返回一个新请求对象。
def process_response(self, request, response, spider):
# 检查是否是目标API且响应体含错误标识
if 'api.example.com' in request.url and response.status == 200:
try:
data = json.loads(response.text)
if data.get('code') in [1001, 1002]: # 业务错误码
# 手动构造重试请求,增加重试计数
retry_times = request.meta.get('retry_times', 0) + 1
if retry_times
- 用
request.replace()而不是request.copy()更安全,它会保留原始request的所有属性(包括callback、errback) -
dont_filter=True防止被去重中间件丢弃重复请求 - 别在重试时盲目加
time.sleep()——Scrapy的调度器本身不阻塞,sleep会卡住整个Downloader线程
为什么process_exception里重试容易失效?
当DNS失败、连接超时、SSL握手错误等抛出异常时,process_exception 会被调用。但这里有个陷阱:如果异常发生在 RetryMiddleware 之前,它可能还没来得及设置 retry_times,导致你的重试逻辑误判。
- 建议统一在
process_exception里只做「标记」,比如设置request.meta['should_retry'] = True,然后在process_response开头检查这个标记并执行重试 - 对
TwistedTimeoutError、DNSError等底层异常,重试前最好加指数退避(如time.sleep(2 ** retry_times)),但必须用scrapy.utils.defer.deferred_from_coro包装成异步延迟,否则会阻塞事件循环 - 别在
process_exception返回response——它没意义;也不该返回None(这会让Scrapy认为你处理完了,后续中间件不会执行)
重试时怎么避免无限循环或IP被封?
自定义重试最危险的不是写不对,而是没设边界。Scrapy自身有 RETRY_TIMES 配置,但你的中间件逻辑可能绕过它。
- 每次重试前检查
request.meta.get('retry_times', 0),硬性限制不超过3~5次,尤其对高并发任务 - 对同一域名的请求,可用
spider.crawler.stats.inc_value(f'retry_domain/{domain}')记录频次,达到阈值后直接raise IgnoreRequest - 换代理或User-Agent必须在重试请求里显式设置,例如:
new_request.headers['User-Agent'] = random.choice(UA_LIST),否则所有重试都用同一个UA - 如果重试后仍失败,
process_response最终应返回原始response或抛出异常,让RetryMiddleware的兜底逻辑接管,而不是静默吞掉
真正麻烦的是那些返回200但数据残缺的响应——它们躲过了Scrapy默认重试机制,又不像网络异常那样容易捕获。这类情况必须靠业务层解析+主动重试,而且重试条件得足够保守,否则容易把服务器压垮或触发风控。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











