前言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 证书异常SSLErrorHTTPS 站点证书过期、证书不匹配、证书不受信任。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.pypython运行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.pypython运行# 开启原生重试 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提升爬虫存活率定期日志巡检基于日志统计异常链接占比分析目标站点反爬强度优化访问策略。