import logging
from scrapy import Request
from scrapy.exceptions import IgnoreRequest
class ErrorHandlingMiddleware:
"""
Scrapy 网络请求exception-handling Middleware
功能:
1. 对超时、5xx 临时错误自动重试(除 500 外,因为 500 常代表服务端内部逻辑错误)
2. 对 4xx 客户端错误直接放弃(404/403 等重试也没用)
3. 所有错误都用详细日志记录,方便后续排查
"""
# 允许自动重试的状态码
ALLOWED_RETRY_STATUS_CODES = [502, 503, 504]
# 允许自动重试的异常类型
ALLOWED_RETRY_EXCEPTIONS = [
TimeoutError, # 请求超时
ConnectionRefusedError, # 连接被拒绝
]
# 独立设置最大重试次数(可与 settings.py 的 RETRY_TIMES 配合)
MAX_RETRY_TIMES = 3
def process_response(self, request, response, spider):
"""
处理正常返回但状态码不对的响应
"""
if response.status in self.ALLOWED_RETRY_STATUS_CODES:
retry_times = request.meta.get("retry_times", 0)
if retry_times < self.MAX_RETRY_TIMES:
retry_times += 1
spider.logger.warning(
f"🔄 状态码 {response.status} 触发重试:第 {retry_times} 次 "
f"| URL:{request.url}"
)
new_request = request.copy()
new_request.meta["retry_times"] = retry_times
# dont_filter=True 防止被去重中间件拦截
new_request.dont_filter = True
return new_request
else:
spider.logger.error(
f"❌ 放弃重试(超过 {self.MAX_RETRY_TIMES} 次):"
f"状态码 {response.status} | URL:{request.url}"
)
raise IgnoreRequest
# 4xx 客户端错误直接放弃
elif response.status >= 400:
spider.logger.warning(
f"🚫 放弃请求(客户端/不可恢复服务端错误):"
f"状态码 {response.status} | URL:{request.url}"
)
raise IgnoreRequest
# 状态码正常,直接返回给 Spider
return response
def process_exception(self, request, exception, spider):
"""
处理请求过程中直接抛出的异常
"""
if isinstance(exception, tuple(self.ALLOWED_RETRY_EXCEPTIONS)):
retry_times = request.meta.get("retry_times", 0)
if retry_times < self.MAX_RETRY_TIMES:
retry_times += 1
spider.logger.warning(
f"🔄 异常触发重试:{type(exception).__name__} "
f"| 第 {retry_times} 次 | URL:{request.url}"
)
new_request = request.copy()
new_request.meta["retry_times"] = retry_times
new_request.dont_filter = True
return new_request
else:
spider.logger.error(
f"❌ 放弃重试(超过 {self.MAX_RETRY_TIMES} 次):"
f"{type(exception).__name__} | URL:{request.url}"
)
return None
# 其他未知异常直接记录并放弃
spider.logger.error(
f"🚫 放弃请求(未知异常):{type(exception).__name__} "
f"| 详情:{str(exception)} | URL:{request.url}"
)
return None