前言
Scrapy 爬虫在长期运行过程中,受网络波动、目标站点反爬策略、链接失效、协议异常、服务器限制等因素影响,各类请求错误、响应异常、连接故障会频繁出现。若未对异常进行统一捕获、分类记录与异常重试,不仅会造成部分数据采集缺失,还会因未处理的异常导致爬虫进程意外中断,大幅降低爬虫稳定性与数据完整性。
Scrapy 中间件作为框架请求与响应的核心拦截层,是实现异常统一管控的最佳载体。相较于在爬虫解析函数中零散编写异常捕获代码,基于下载器中间件集中处理请求异常,具备代码解耦、统一规则、便于运维排查、可扩展异常策略等优势。本文围绕 Scrapy 异常捕获中间件展开深度实战,讲解请求异常分类、中间件工作机制、自定义异常中间件开发、异常日志分级、异常重试策略、异常状态码拦截、告警联动等内容,同时结合生产场景优化异常处理逻辑,适配单机爬虫与多爬虫集群运行环境,所有代码均可直接部署使用。
本文涉及的核心依赖、官方文档及参考资源链接如下,可直接跳转查阅:
- Scrapy 官方文档 - 下载器中间件:中间件生命周期、回调方法、执行规则完整说明
- Scrapy 官方文档 - 异常类型:框架内置异常类、状态码定义规范
- Python logging 标准库文档:日志分级、日志格式化、持久化日志配置
- Twisted 网络异常文档:Scrapy 底层网络异常、连接错误相关说明
一、基础认知:Scrapy 异常分类与中间件原理
1.1 爬虫常见异常类型
Scrapy 运行阶段的异常主要分为网络层异常、HTTP 响应异常、业务解析异常三大类,其中请求环节的异常全部由下载器中间件拦截处理,也是本文重点管控范围。
1.1.1 网络层异常
此类异常源于客户端与目标服务器之间的网络通信故障,属于底层 IO 异常,框架会抛出 Twisted 或 Scrapy 内置异常类:
- 连接超时:
TimeoutError,请求发出后长时间未收到服务器响应; - 连接拒绝:
ConnectionRefusedError,目标服务器端口未开放、防火墙拦截请求; - 连接中断:
ConnectionDone、ConnectionLost,通信过程中链接被强制断开; - DNS 解析失败:
DNSLookupError,域名无法正常解析,链接地址失效; - SSL 证书异常:
SSLError,HTTPS 站点证书过期、证书不匹配、证书不受信任。
1.1.2 HTTP 响应状态码异常
服务器正常接收请求并返回响应,但通过 HTTP 状态码标识访问限制、权限问题、资源不存在等情况,常用异常状态码划分如下:
- 4xx 客户端错误:404 页面不存在、403 禁止访问、401 未授权、400 请求参数错误;
- 5xx 服务端错误:500 服务器内部错误、502 网关错误、503 服务不可用、504 网关超时。
1.1.3 框架内置异常
Scrapy 为爬虫场景封装专属异常类,多用于反爬拦截、规则限制场景:
IgnoreRequest:主动忽略当前请求,终止该条请求后续流程;NotConfigured:组件配置缺失引发的异常;StopDownload:强制终止响应内容下载,节省网络与内存资源。
1.2 下载器中间件生命周期与异常回调
下载器中间件是 Scrapy 框架中位于调度器与下载器之间的组件,每一次请求发送、响应接收、异常触发都会依次经过中间件的对应回调方法。其中处理请求异常的核心方法为process_exception,也是捕获报错的核心入口。
1.2.1 核心回调方法说明
process_request(request, spider):请求发送至下载器之前执行,可修改请求头、代理、Cookie 等参数;process_response(request, response, spider):下载器获取响应之后、交由爬虫解析之前执行,可过滤、修改响应内容;process_exception(request, exception, spider):请求执行出现异常唯一回调,所有网络错误、超时、连接异常都会进入该方法,是异常捕获的核心函数。
1.2.2 中间件执行优先级
Scrapy 支持同时配置多个下载器中间件,配置文件中数值越小,中间件执行顺序越靠前。原生中间件与自定义中间件共存时,需合理设置优先级,避免异常捕获逻辑被覆盖。框架默认下载器中间件优先级区间为 0~900,自定义异常中间件建议设置优先级为 500~600,保证正常拦截所有异常。
1.3 异常处理基础原则
结合爬虫运维经验,总结异常处理通用原则,贯穿全文所有代码实现:
- 分类处理:区分网络异常、状态码异常、SSL 异常,执行不同应对策略;
- 日志分级:严重错误记录 ERROR 级别日志,普通超时记录 WARNING 级别日志,便于日志筛选;
- 有限重试:对临时故障(超时、5xx 错误)进行重试,对永久故障(404、无效域名)禁止重试;
- 资源可控:重试次数设置上限,避免死循环请求,消耗服务器资源;
- 信息完整:日志中记录请求地址、异常类型、异常描述、爬虫名称,提升问题排查效率。
二、项目环境准备与基础配置
2.1 项目创建
沿用 Scrapy 标准项目结构,若新建测试项目,执行以下终端命令:
bash
运行
scrapy startproject exception_middleware_demo cd exception_middleware_demo scrapy genspider test_spider example.com项目核心文件说明:middlewares.py用于编写自定义异常中间件,settings.py负责中间件注册、全局参数配置,爬虫文件用于发起测试请求。
2.2 全局基础配置
修改settings.py文件,关闭冗余配置、设置基础日志格式、预留重试次数等全局参数,为异常处理做准备:
python
运行
# 关闭robots协议检测 ROBOTSTXT_OBEY = False # 关闭Cookie(根据业务需求调整) COOKIES_ENABLED = False # 全局日志级别,开发环境设为DEBUG,生产环境设为WARNING/ERROR LOG_LEVEL = "DEBUG" # 自定义全局参数:异常最大重试次数 MAX_RETRY_TIMES = 3 # 请求超时时间,单位秒 DOWNLOAD_TIMEOUT = 102.3 测试爬虫编写
编写测试爬虫,主动引入异常链接、超时链接、无效域名,模拟各类报错场景,用于验证中间件捕获效果,打开spiders/test_spider.py:
python
运行
import scrapy class TestSpider(scrapy.Spider): name = 'test_spider' allowed_domains = ['example.com'] # 组合正常链接、无效链接、超时链接、403链接,模拟多类异常 start_urls = [ "https://example.com", "https://192.168.99.99", # 无效IP,触发连接拒绝 "https://httpstat.us/403", # 403 禁止访问 "https://httpstat.us/500", # 500 服务器错误 "https://httpstat.us/404" # 404 页面不存在 ] def parse(self, response): self.logger.info(f"正常响应:{response.url},状态码:{response.status}")三、基础版异常中间件:统一捕获与日志记录
基础版本中间件实现最核心功能:拦截所有请求异常、识别异常类型、打印结构化日志,不包含重试逻辑,适合仅做异常监控、无需自动重试的场景。
3.1 编写基础异常中间件
打开项目middlewares.py文件,编写基础异常捕获中间件代码:
python
运行
from twisted.internet.error import TimeoutError, ConnectionRefusedError, ConnectionLost from twisted.names.error import DNSLookupError from OpenSSL.SSL import SSLError from scrapy.exceptions import IgnoreRequest class BasicExceptionMiddleware: """基础异常捕获中间件:分类捕获异常并记录日志""" def process_exception(self, request, exception, spider): """请求异常统一入口""" url = request.url # 网络超时异常 if isinstance(exception, TimeoutError): spider.logger.warning(f"【请求超时】链接:{url},异常信息:{str(exception)}") return IgnoreRequest() # 连接拒绝异常 elif isinstance(exception, ConnectionRefusedError): spider.logger.error(f"【连接被拒绝】链接:{url},异常信息:{str(exception)}") return IgnoreRequest() # 连接中断异常 elif isinstance(exception, ConnectionLost): spider.logger.warning(f"【连接意外中断】链接:{url},异常信息:{str(exception)}") return IgnoreRequest() # DNS解析失败 elif isinstance(exception, DNSLookupError): spider.logger.error(f"【DNS解析失败】链接:{url},异常信息:{str(exception)}") return IgnoreRequest() # SSL证书异常 elif isinstance(exception, SSLError): spider.logger.error(f"【SSL证书异常】链接:{url},异常信息:{str(exception)}") return IgnoreRequest() # 其他未知异常 else: spider.logger.error(f"【未知请求异常】链接:{url},异常类型:{type(exception)},详情:{str(exception)}") return IgnoreRequest()3.2 中间件代码原理解析
- 异常类导入:从 Twisted、OpenSSL、Scrapy 官方模块导入对应异常类,用于精准判断异常类型;
- process_exception 入参:
request为当前请求对象,可获取请求地址、请求头、自定义参数;exception为捕获到的异常实例;spider为当前运行的爬虫实例,用于打印日志; - 异常分类判断:通过
isinstance精准匹配异常类型,区分错误等级,分别使用warning、error日志级别; - 返回值规则:返回
IgnoreRequest()表示终止当前请求,不再进入后续流程,避免无效流转;若返回None,框架会继续执行原有重试逻辑。
3.3 注册并启用中间件
在settings.py的DOWNLOADER_MIDDLEWARES配置项中添加自定义中间件,设置执行优先级:
python
运行
DOWNLOADER_MIDDLEWARES = { 'exception_middleware_demo.middlewares.BasicExceptionMiddleware': 550, # 注释/保留原生中间件,根据需求调整 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, }3.4 运行测试
执行命令启动爬虫:
bash
运行
scrapy crawl test_spider观察终端日志,不同异常链接会输出对应分类日志,超时、连接拒绝、DNS 错误等异常全部被精准捕获,无未处理异常抛出。
四、进阶版异常中间件:异常捕获 + 智能重试
单纯的日志记录无法解决临时网络故障问题,对于超时、5xx 服务端错误、连接抖动等临时性异常,需要配置自动重试机制。本节实现异常分类重试,区分可重试异常与不可重试异常,结合全局重试次数限制,打造工业级异常处理逻辑。
4.1 进阶中间件完整代码
在middlewares.py中新增智能重试异常中间件,整合异常判断、重试计数、重试限制、日志记录逻辑:
python
运行
from twisted.internet.error import TimeoutError, ConnectionRefusedError, ConnectionLost from twisted.names.error import DNSLookupError from OpenSSL.SSL import SSLError from scrapy.exceptions import IgnoreRequest from scrapy.http import Request class RetryExceptionMiddleware: """带智能重试的异常中间件:临时异常自动重试,永久异常直接拦截""" def process_exception(self, request, exception, spider): url = request.url # 从全局配置读取最大重试次数 max_retry = spider.settings.get("MAX_RETRY_TIMES", 3) # 获取当前请求的重试次数,若无则默认为0 current_retry = request.meta.get("retry_count", 0) # 定义可重试的异常类型列表 retry_exceptions = (TimeoutError, ConnectionLost) # 定义不可重试的异常类型列表 no_retry_exceptions = (ConnectionRefusedError, DNSLookupError, SSLError) # 场景1:可重试异常(超时、连接中断) if isinstance(exception, retry_exceptions): if current_retry < max_retry: # 重试次数自增 current_retry += 1 request.meta["retry_count"] = current_retry spider.logger.warning( f"【临时异常-第{current_retry}次重试】链接:{url},异常:{str(exception)}" ) # 生成新请求,放回调度器等待重试 new_request = request.copy() return new_request else: # 达到最大重试次数,终止请求 spider.logger.error( f"【重试次数耗尽】链接:{url},最大重试{max_retry}次,放弃采集" ) return IgnoreRequest() # 场景2:不可重试异常(连接拒绝、DNS失败、SSL错误) elif isinstance(exception, no_retry_exceptions): spider.logger.error(f"【永久异常,禁止重试】链接:{url},异常:{str(exception)}") return IgnoreRequest() # 场景3:其他未知异常 else: spider.logger.error(f"【未知异常】链接:{url},异常详情:{str(exception)}") return IgnoreRequest()4.2 核心功能原理详解
- 重试计数实现:利用
request.meta字典存储当前请求的重试次数,meta是 Scrapy 请求对象的自定义参数容器,请求复制后参数可保留,实现次数累加; - 异常分组:将异常划分为可重试、不可重试两类,符合实际运维逻辑:网络抖动、超时属于临时问题,可重试;域名失效、防火墙拦截、证书错误属于永久问题,无需重试;
- 请求重试逻辑:调用
request.copy()复制原请求对象并返回,框架会将新请求重新放入调度队列,等待再次发起请求; - 重试上限控制:对比当前重试次数与全局最大次数,避免无限重试占用资源,达到上限后主动放弃请求。
4.3 切换中间件并测试
修改settings.py中间件配置,启用带重试功能的中间件:
python
运行
DOWNLOADER_MIDDLEWARES = { 'exception_middleware_demo.middlewares.RetryExceptionMiddleware': 550, }重启爬虫观察日志,超时、连接中断类异常会按照配置次数逐步重试,达到上限后终止;403、DNS 错误等异常直接拦截,不再重试。
五、HTTP 状态码异常捕获与处理
网络异常由process_exception捕获,而 4xx、5xx 等 HTTP 状态码异常属于正常响应,不会触发异常回调,需要在process_response方法中单独拦截处理。本节实现状态码分类管控、状态码重试、无效页面过滤。
5.1 状态码处理中间件代码
继续在middlewares.py编写状态码拦截中间件,区分客户端错误、服务端错误、正常状态码:
python
运行
from scrapy.http import Request class HttpStatusMiddleware: """HTTP状态码异常处理中间件""" def process_response(self, request, response, spider): url = response.url status_code = response.status max_retry = spider.settings.get("MAX_RETRY_TIMES", 3) current_retry = request.meta.get("status_retry", 0) # 5xx 服务端错误:临时故障,允许重试 if 500 <= status_code < 600: if current_retry < max_retry: current_retry += 1 request.meta["status_retry"] = current_retry spider.logger.warning(f"【服务端异常{status_code}】第{current_retry}次重试:{url}") new_req = request.copy() return new_req else: spider.logger.error(f"【{status_code}重试耗尽】链接:{url},放弃采集") return response # 4xx 客户端错误:永久异常,禁止重试 elif 400 <= status_code < 500: spider.logger.error(f"【客户端异常{status_code}】链接:{url},直接拦截") return response # 2xx 正常状态码,正常流转至爬虫解析 else: return response5.2 状态码处理逻辑说明
- 5xx 状态码:服务器内部错误、网关超时等属于临时故障,参照网络异常规则进行有限次数重试;
- 4xx 状态码:页面不存在、权限不足、请求非法等属于永久性问题,直接记录日志并放行响应,不再重试;
- 200 系列正常状态码:不作任何处理,正常交给爬虫
parse函数解析数据。
5.3 多中间件共存配置
生产环境中,网络异常中间件与状态码中间件可同时启用,settings.py配置如下,优先级互不冲突:
python
运行
DOWNLOADER_MIDDLEWARES = { 'exception_middleware_demo.middlewares.RetryExceptionMiddleware': 540, 'exception_middleware_demo.middlewares.HttpStatusMiddleware': 550, }六、高级功能:日志持久化与异常分级告警
在单机测试环境中,日志输出至终端即可,但在服务器长期运行的爬虫集群中,需要将异常日志持久化到本地文件,同时针对严重异常实现简易告警,便于运维及时发现问题。
6.1 日志持久化配置
修改settings.py,配置日志文件路径、日志分割规则,实现日志落地:
python
运行
# 日志文件路径 LOG_FILE = "./spider_exception.log" # 日志编码 LOG_ENCODING = "utf-8" # 日志格式:时间 - 日志级别 - 爬虫名称 - 日志内容 LOG_FORMAT = '%(asctime)s [%(levelname)s] %(name)s: %(message)s' # 时间格式 LOG_DATEFORMAT = '%Y-%m-%d %H:%M:%S'配置完成后,爬虫运行产生的所有日志(包含异常日志)都会写入spider_exception.log文件,支持后续检索、分析。
6.2 简易异常告警实现
针对 403 封禁、SSL 错误、大量链接重试失败等严重异常,增加本地日志告警标识,也可扩展为邮件、接口推送告警。在原有异常中间件中添加告警逻辑,示例如下:
python
运行
# 在异常判断逻辑后增加告警标记 elif isinstance(exception, ConnectionRefusedError): spider.logger.critical(f"【严重告警-IP被封禁】链接:{url},请及时检查代理与访问策略") return IgnoreRequest()使用critical最高级别日志区分紧急异常,运维可通过日志监控工具筛选该级别日志,实现快速预警。
七、原生重试配置与自定义中间件协同规则
Scrapy 框架自带原生重试中间件RetryMiddleware,在自定义异常中间件上线后,需要理清二者的优先级与冲突问题,保证逻辑统一。
7.1 原生重试配置参数
框架默认重试相关配置,位于settings.py:
python
运行
# 开启原生重试 RETRY_ENABLED = True # 最大重试次数 RETRY_TIMES = 2 # 需要重试的HTTP状态码 RETRY_HTTP_CODES = [500, 502, 503, 504, 408]7.2 协同使用方案
- 方案一:保留原生中间件若仅需简单重试,直接使用原生配置即可,无需自定义中间件;
- 方案二:使用自定义中间件(推荐生产环境)注释 / 禁用原生重试中间件,统一由自定义中间件管控所有异常与重试逻辑,避免两套规则冲突:
python
运行
DOWNLOADER_MIDDLEWARES = { 'scrapy.downloadermiddlewares.retry.RetryMiddleware': None, 'exception_middleware_demo.middlewares.RetryExceptionMiddleware': 550, }
八、常见问题排查与生产最佳实践
8.1 高频问题及解决方案
异常未被中间件捕获原因:中间件未注册、优先级过低、异常类型判断错误。 解决:检查
DOWNLOADER_MIDDLEWARES配置,调高优先级,核对异常类导入路径。请求无限重试原因:未使用
request.meta记录重试次数,计数逻辑失效。 解决:严格基于meta存储重试次数,设置全局最大重试阈值。状态码 404/403 触发重试原因:状态码分类错误,将不可重试状态码加入重试逻辑。 解决:明确区分 4xx、5xx 状态码,4xx 禁止重试。
日志乱码原因:日志文件编码未设置。 解决:配置
LOG_ENCODING = "utf-8",统一编码格式。
8.2 生产环境最佳实践
- 异常分层处理:网络异常、HTTP 状态码、解析异常分层管控,各司其职;
- 禁用原生重试:生产环境统一使用自定义中间件,规则集中、便于维护;
- 重试次数差异化:普通超时设 2~3 次重试,高危站点设 1~2 次重试,降低封禁风险;
- 日志分类存储:正常日志、异常日志拆分文件,提升检索效率;
- 结合代理池联动:捕获 403、连接拒绝异常时,自动切换代理 IP,提升爬虫存活率;
- 定期日志巡检:基于日志统计异常链接占比,分析目标站点反爬强度,优化访问策略。