4.1 下载器中间件与蜘蛛中间件


文档摘要

4.1 中间件:下行隧道里的两道关卡 本节摘要:中间件是插在引擎与下载器(下载器中间件)、引擎与 Spider(蜘蛛中间件)之间的钩子层:去程可拦请求、回程可拦响应、失败可兜底。本节讲清两类中间件的方法签名、执行顺序与三种返回值约定——掌握了返回值约定,就掌握了框架对抗的全部入口。 第 3 章结束在"引擎从队列取出下一张票"。本节跟票进入下行隧道:隧道里有两道关卡,下载器中间件离下载器近,管请求改写与响应初检;蜘蛛中间件离 Spider 近,管响应进回调前的最后一道处理。第 2 章看票面时说过"去程关卡可改写",现在轮到你站到关卡里。

4.1 中间件:下行隧道里的两道关卡

本节摘要:中间件是插在引擎与下载器(下载器中间件)、引擎与 Spider(蜘蛛中间件)之间的钩子层:去程可拦请求、回程可拦响应、失败可兜底。本节讲清两类中间件的方法签名、执行顺序与三种返回值约定——掌握了返回值约定,就掌握了框架对抗的全部入口。

第 3 章结束在"引擎从队列取出下一张票"。本节跟票进入下行隧道:隧道里有两道关卡,下载器中间件离下载器近,管请求改写与响应初检;蜘蛛中间件离 Spider 近,管响应进回调前的最后一道处理。第 2 章看票面时说过"去程关卡可改写",现在轮到你站到关卡里。

方法签名与三种返回值

下载器中间件有三个钩子,每个钩子的返回值决定数据流的走向:

class MyMiddleware: def process_request(self, request, spider): # 去程关卡。三种走法: # 1. 返回 None:放行,继续传给下一道关卡 # 2. 返回 Response:不再下载,直接当作回程响应(本地缓存场景) # 3. 返回 Request:这张票作废,新票重新排队(换址重发场景) request.headers["User-Agent"] = pick_ua() return None def process_response(self, request, response, spider): # 回程关卡。两种走法: # 1. 返回 Response:继续向上交给蜘蛛中间件 # 2. 返回 Request:判这张响应不合格,新票重排(被反爬拦截时常用) if response.status in (403, 429): return request.replace(dont_filter=True) # 换张票面重发 return response def process_exception(self, request, exception, spider): # 失联兜底:下载抛异常时触发(超时、断连、DNS 失败) return None # 返回 Request 可换票重发,返回 None 交给错误通道

蜘蛛中间件对称地也有三个钩子:process_spider_input 在响应进回调前、process_spider_output 在回调产出 Item 与 Request 之后、process_spider_exception 在回调抛错时。日常九成需求落在下载器中间件,蜘蛛中间件多用于结果过滤与错误兜底。

执行顺序:数字决定站位

第 1 章讲装配数字时埋了个伏笔,现在揭晓。下载器中间件的数字含义:

  • 去程(process_request):数字小的先执行,靠引擎侧;
  • 回程(process_response):数字大的先执行,靠下载器侧。

内置中间件占用了 0 到 900 之间的号段。自定义中间件的常用策略:改请求头的选 500 上下(在默认 UA 中间件之后覆盖它),做响应过滤的选 600 以上(在重试中间件之后,避免抢它的工作):

DOWNLOADER_MIDDLEWARES = { "myproject.middlewares.RotateUA": 543, # 去程第 543 号关卡 "myproject.middlewares.BanDetector": 620, # 回程在重试件之后补刀 }

顺序错了的症状很隐蔽:比如你的 UA 轮换写在 300 号(默认 UA 中间件之前),去程先被你改了头、随后又被默认中间件覆盖——表现为"我明明换了 UA,日志里怎么还是默认值"。遇到这类灵异现象,先查装配数字。

图7 中间件双程关卡:顺序与返回值的决定作用

图7 中间件双程关卡:顺序与返回值的决定作用

实战:一个带计数的最小中间件

把约定拼成一个真实可用的例子——统计被 429 拦截的次数并自动退避:

class ThrottleOn429: def __init__(self): self.hits = 0 def process_response(self, request, response, spider): if response.status == 429: self.hits += 1 # 返回 Request:判这张响应无效,原票重发 # download_delay 是框架保留的行李舱键,可动态改间隔 request.meta["download_delay"] = min(30, 2 ** self.hits) spider.logger.warning("第 %s 次被限流,退避至 %s 秒", self.hits, request.meta["download_delay"]) return request.replace(dont_filter=True) return response @classmethod def from_crawler(cls, crawler): return cls()

运行日志能完整看到它的作用:

WARNING: 第 1 次被限流,退避至 2 秒 WARNING: 第 3 次被限流,退避至 8 秒 DEBUG: Crawled (200) books.example.com/list?page=7

⚠️ 常见坑:process_response 里重发请求若不加 dont_filter,票会被去重指纹拦下——上次那张同款票已经登记过了。换票重发几乎总伴着 dont_filter,忘了它就是"改了等于没改"的第一大来源。

排错案例:一次"配置了却没生效"的完整定位

背景:工程里写了一个加自定义请求头的中间件,运行后抓包发现头没带上。操作分四步排查。第一步看装配清单——crawl 启动日志里翻中间件列表,自定义件在列,说明装配成功;第二步查数字——自定义件写着 543,而被依赖的默认头部中间件编号更大、在其后才执行,前者的修改被后者覆盖;第三步把数字调整到 680,重新抓包,头正常出现;第四步补一条回归动作——每次改动中间件后抓包核对一次,写进部署自检。

# 启动日志中的中间件装配段(节选) scrapy.middleware] Enabled downloader middlewares: ... myproject.middlewares.AddHeader (680) scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware (750)

解读:中间件问题的定位套路永远是三连问——"在不在清单里(装配)、站在第几位(顺序)、返回了什么(逻辑)",三问分别对应三类故障。变式:要确认返回值逻辑,给中间件方法加一行临时日志打出返回对象的类型,比断点调试更快——它直接暴露这一票是放行、拦截还是换票。

蜘蛛中间件的三件事

蜘蛛中间件常被略过,它真正高频的三件事值得单列。结果过滤:process_spider_output 里检查回调产出的 Item 与 Request,把残缺件、越界请求就地拦下——比在管道里晚发现省一轮传递。异常转译:process_spider_exception 把回调里的崩溃转换成登记 Item 或替代请求,让单页错误不至于中断整程。请求瘦身:对回调产出的新请求统一补默认 meta、清洗地址参数,集中一处做,Spider 里就不用每处都写。三件事的共同点是"离回调最近",所以适合放与解析结果直接相关的检查。

本节要点回顾

  • 两类中间件三钩子:去程改票、回程验货、异常兜底,蜘蛛侧对称三钩;
  • 返回值即控制流:None 放行、Response 免下载、Request 换票重发;
  • 数字定站位:去程小号先过、回程大号先过,顺序错则逻辑被覆盖;
  • 重发必配 dont_filter:否则被自己的去重机制拦在门外。

关卡的骨架搭好了。下一节往关卡里装最常用的三件装备:UA 轮换、代理池与 Cookie 会话。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U