Python中Scrapy的RetryMiddleware不生效,如何解决?

按照惯例,我先贴出来代码

class CustomerRetryMiddleware(RetryMiddleware):
    """
        去掉了一些判断,增加了 log
    """
def process_response(self, request, response, spider):
    if response.status in self.retry_http_codes:
        retries = request.meta.get('retry_times', 0) + 1
        reason = response_status_message(response.status)
        spider.logger.warn('下载中收到错误 http code:{0},丢入重试,第{1}次重试'.format(reason, retries))
        return self._retry(request, reason, spider) or response
    else:
        return response

"""
    去掉了 EXCEPTIONS_TO_RETRY 判断,无脑重试
"""

def process_exception(self, request, exception, spider):
    retries = request.meta.get('retry_times', 0) + 1
    spider.logger.warn('下载中收到:{0},丢入重试,第{1}次重试'.format(exception.reasons, retries))
    return self._retry(request, 'CustomerRetryError', spider)

def _retry(self, request, reason, spider):
    retries = request.meta.get('retry_times', 0) + 1

    retry_times = self.max_retry_times

    if 'max_retry_times' in request.meta:
        retry_times = request.meta['max_retry_times']

    stats = spider.crawler.stats
    if retries <= retry_times:
        spider.logger.debug("Retrying %(request)s (failed %(retries)d times): %(reason)s",
                            {'request': request, 'retries': retries, 'reason': reason},
                            extra={'spider': spider})
        retryreq = request.copy()
        retryreq.meta['retry_times'] = retries
        retryreq.dont_filter = True
        retryreq.priority = request.priority + self.priority_adjust

        if isinstance(reason, Exception):
            reason = global_object_name(reason.__class__)

        stats.inc_value('retry/count')
        stats.inc_value('retry/reason_count/%s' % reason)
        return retryreq
    else:
        stats.inc_value('retry/max_reached')
        spider.logger.warn('超过重试次数,停止重试')

process_response 方法主要是为了加一点 log process_exception 方法是为了出现任何 Exception 都能重试 同样,重写 _retry 方法也是为了加点 log

我遇到的问题是,如果我 settings.py 中设定了 5 次重试,那么,log 中一定只打印第 6 次重试的 log,前 5 次的 log 并没有打出来,我很疑惑,求解答

另外,我为了去除次数的问题,我还改配置改到 100 次,然而 log 中只打印第 101 次重试,然后就是 _retry 方法中的 else 语句的 log "超过重试次数,停止重试"


Python中Scrapy的RetryMiddleware不生效,如何解决?

9 回复

现在手机没法调试代码,scrapy 的下载异常记得有两种情况来着:

- 下载失败(超时等等)
- 下载成功但状态码不对( 40x,50x 等等)

process_exception 应该是下载失败才会触发(当然也有可能我记错了,好久不用了已经)


遇到Scrapy的RetryMiddleware不生效的问题,通常是因为配置或自定义逻辑覆盖了默认行为。下面是一个完整的排查和解决方案。

首先,确保你在 settings.py 中正确启用了重试中间件并设置了相关参数:

# settings.py
RETRY_ENABLED = True  # 默认就是True,但检查一下没坏处
RETRY_TIMES = 2  # 除第一次请求外,重试次数
RETRY_HTTP_CODES = [500, 502, 503, 504, 408]  # 需要重试的HTTP状态码
DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
}

如果已经配置了但还是不生效,最常见的原因是你的spider或自定义中间件返回了 None 或抛出了异常,阻止了重试逻辑的执行。检查你的spider代码:

import scrapy
from scrapy.downloadermiddlewares.retry import get_retry_request

class MySpider(scrapy.Spider):
    name = 'example'
    
    def start_requests(self):
        yield scrapy.Request('http://example.com', callback=self.parse, errback=self.errback)
    
    def parse(self, response):
        # 如果在这里直接返回None或item,错误处理链会中断
        if response.status == 404:
            self.logger.warning('Page not found')
            # 不要直接return,让异常传播或返回Request对象
            return
        # ... 正常解析逻辑
    
    def errback(self, failure):
        # 自定义errback可能会覆盖默认重试逻辑
        # 如果你在这里处理了failure但没有重新yield request,重试就不会发生
        self.logger.error(f'Request failed: {failure.value}')
        # 若要保留重试,可以调用原始重试逻辑:
        # return get_retry_request(failure.request, failure.value, spider=self)

另一个常见坑点是自定义了 DOWNLOADER_MIDDLEWARES 但顺序不对,或者你的中间件返回了 Response/Request 对象。中间件顺序很重要,RetryMiddleware通常应该在550左右。检查你的自定义中间件:

# middlewares.py
from scrapy.downloadermiddlewares.retry import RetryMiddleware

class CustomRetryMiddleware(RetryMiddleware):
    def process_response(self, request, response, spider):
        # 如果你的逻辑在这里直接返回了response,原始RetryMiddleware就不会执行
        if response.status in [403, 429]:
            # 特殊状态码,你可能想自定义重试逻辑
            reason = f'Status {response.status}'
            return self._retry(request, reason, spider) or response
        # 确保调用父类方法以保留默认重试行为
        return super().process_response(request, response, spider)
    
    def process_exception(self, request, exception, spider):
        # 同样,确保异常处理调用父类
        return super().process_exception(request, exception, spider)

然后在settings中正确配置中间件顺序:

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.CustomRetryMiddleware': 540,  # 比默认RetryMiddleware优先级高
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
}

最后,用这个简单的测试spider验证重试是否工作:

import scrapy
from scrapy.utils.test import get_crawler

class TestRetrySpider(scrapy.Spider):
    name = 'test_retry'
    start_urls = ['http://httpbin.org/status/500']  # 模拟服务器错误
    
    custom_settings = {
        'RETRY_TIMES': 2,
        'RETRY_HTTP_CODES': [500],
        'LOG_LEVEL': 'DEBUG',  # 查看重试日志
    }
    
    def parse(self, response):
        self.logger.info(f'最终成功: {response.url}')

运行后查看日志,你应该看到类似 Retrying <GET http://...> (failed 1 times): 500 Internal Server Error 的消息。

总结一下:检查设置、中间件顺序和自定义回调是否干扰了重试链。

还有一个你 if 里面 logger 是 debug,else 里面是 warn,如果 settings 里面改了 log_level 的话可能会看不到。

if 里边的是 Scrapy 的代码,我没有改,但是经过你的提醒,我终于发现我确实看不到 log,因为我配置的 loglevel 是 warn,多谢

我发现,我爬取了一批视频,总是有几个下载报 403,然而我手动打开这个视频是正常的。 我有 headers 中 random UA 和 X-Forwarded-For,不知道是不是因为下载太多被封了

403 一般来说是触发反爬了

我重新试了一下,我上边这段代码确实打印不出来 if retries <= retry_times: 判断里边的 log

重新看了一下,发现

<br>INFO: Enabled downloader middlewares:<br>['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',<br> 'New91Crawler.middlewares.CustomerRetryMiddleware',<br> 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',<br> 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',<br> 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',<br> 'scrapy.downloadermiddlewares.retry.RetryMiddleware',<br> 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',<br> 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',<br> 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',<br> 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware',<br> 'scrapy.downloadermiddlewares.stats.DownloaderStats']<br><br>

我自定义的 retry 和 Scrapy 自带的 retry 都加载了,理论上不应该用我的覆盖系统的吗?

你可以看看 settings 里面的优先级,记得是数值越低的越先执行,系统自带的 retry 优先级好像是 550。

我只知道如何让自己的先执行,如何覆盖掉确实不知道了。

我自己的是 200,但是感觉还是先走的系统的,重试了之后才走的我的,奇怪

回到顶部