bigywater.tech
Scrapy · 12

Spider 中间件:它夹在引擎和 spider 之间做了什么

爬虫中间件夹在引擎和 spider 之间,管的是“进入回调的响应”和“回调产出的请求与 item”。这篇用三个自定义中间件加记录钩子,把输入、输出、异常三条链的方向与短路规则一条条实测出来。

C B A
process_spider_output 与 process_spider_exception 的调用顺序(input 是 A B C)
TypeError
2.19 里只写同步 process_spider_output 的启动结果
0
process_spider_input 抛异常后 spider_exceptions/count 的值(直接进 errback)

三条链

实测 · 本机执行输出
  StartSpiderMiddleware(25) → HttpErrorMiddleware(50) → RefererMiddleware(700) → UrlLengthMiddleware(800) → DepthMiddleware(900) → MetaCopyDetectionMiddleware(1000)
   1. A       process_start             
   2. B       process_start             
   3. C       process_start             
   4. A       process_spider_input      
   5. B       process_spider_input      
   6. C       process_spider_input      
   7. spider  callback                  url=list
   8. C       process_spider_output     what=item {'n': 1}
   9. B       process_spider_output     what=item {'n': 1}
  10. A       process_spider_output     what=item {'n': 1}
  11. C       process_spider_output     what=Request second
  12. B       process_spider_output     what=Request second
  13. A       process_spider_output     what=Request second
  14. A       process_spider_input      
  15. B       process_spider_input      
  16. C       process_spider_input      
  17. spider  callback                  url=second

响应进来走 process_spider_input(数字小 → 大),回调产出走 process_spider_output(数字大 → 小),process_start 处理 start() 的输出。

input 抛异常

实测 · 本机执行输出
   1. A       process_start             
   2. B       process_start             
   3. C       process_start             
   4. A       process_spider_input      
   5. B       process_spider_input      
   6. spider  errback                   exc=ValueError: input rejected by B
  spider_exceptions/count=0

在 process_spider_input 里抛异常会跳过回调和更靠后的 input 方法,Failure 直接交给请求的 errback。HttpErrorMiddleware 就是这么拒绝非 2xx 响应的。

过滤产出

实测 · 本机执行输出
   1. A       process_start             
   2. A       process_spider_input      
   3. A       process_spider_output     what=Request i0
   4. A       process_spider_output     what=Request i1
   5. A         丢弃                      what=Request i1
   6. A       process_spider_output     what=Request i2
   7. A       process_spider_output     what=Request i3
   8. A         丢弃                      what=Request i3
   9. A       process_spider_output     what=item {'price': 5}
  10. A       process_spider_output     what=item {'price': -1}
  11. A         丢弃                      what=item {'price': -1}
  12. A       process_spider_input      
  13. spider  callback                  url=i2
  14. A       process_spider_input      
  15. spider  callback                  url=i0
  服务器收到 ['/list', '/i2', '/i0'];item_scraped_count=1

process_spider_output 对每个请求和 item 单独决定放行与否,丢弃的对象不会进入调度器或 pipeline。

异常处理链

实测 · 本机执行输出
  没有人恢复:
   1. A       process_spider_input      
   2. B       process_spider_input      
   3. C       process_spider_input      
   4. spider  callback                  url=list
   5. C       process_spider_output     what=item {'n': 1}
   6. B       process_spider_output     what=item {'n': 1}
   7. A       process_spider_output     what=item {'n': 1}
   8. C       process_spider_exception  exc=RuntimeError
   9. B       process_spider_exception  exc=RuntimeError
  10. A       process_spider_exception  exc=RuntimeError
      spider_exceptions/count=1;item_scraped_count=1
  B(300)恢复:process_spider_exception 里返回 [{'recovered_by': 'B'}]:
   1. A       process_spider_input      
   2. B       process_spider_input      
   3. C       process_spider_input      
   4. spider  callback                  url=list
   5. C       process_spider_output     what=item {'n': 1}
   6. B       process_spider_output     what=item {'n': 1}
   7. A       process_spider_output     what=item {'n': 1}
   8. C       process_spider_exception  exc=RuntimeError
   9. B       process_spider_exception  exc=RuntimeError
  10. A       process_spider_output     what=item {'recovered_by': 'B'}
      spider_exceptions/count=0;item_scraped_count=2

回调抛异常后,各中间件的 process_spider_exception 依次被调用:返回 None 交给下一个,返回可迭代对象则终止异常链,内容只经过更靠近引擎的 output。

异步要求

实测 · 本机执行输出
  只有同步版本的 Old: TypeError: Middleware spider.Old doesn't support asynchronous spider output. Its process_spider_output() method should be an async generator function or it should additionally define a process_spider_output_asy
  同步 + process_spider_output_async 的 New: 正常运行,item_scraped_count=1,[{'what': 'async 版本被调用', 't': 161.3}]

2.19 不接受只有同步 process_spider_output 的中间件:要写成 async 生成器,或再定义 process_spider_output_async。

内置中间件

实测 · 本机执行输出
  回调被调用的:['ok'];服务器收到 ['/404', '/ok']
  stats:downloader/response_status_count/200=1, downloader/response_status_count/404=1, httperror/response_ignored_count=1, httperror/response_ignored_status_count/404=1, response_received_count=2, urllength/request_ignored_count=1
  日志:[scrapy.spidermiddlewares.urllength] INFO: Ignoring link (url length > 2083): http://127.0.0.1:9960/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  日志:[scrapy.spidermiddlewares.httperror] INFO: Ignoring response <404 http://127.0.0.1:9960/404>: HTTP status code is not handled or not allowed

HttpError 过滤非 2xx 响应,UrlLength 丢弃过长的 URL,都会在 stats 里计数。

分步演示

爬虫中间件
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Scrapy 2.19.0 / Twisted 26.4.0 / lxml 6.1.3 / parsel 1.12.1(临时虚拟环境);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、重定向、压缩、断连等行为,并记录收到的原始请求;每个实验在临时目录里新建 Scrapy 项目或单文件 spider,命令输出来自真实的 scrapy 进程;计时用 time.perf_counter,数字每次运行会有小幅抖动。中间件用工厂函数生成并通过 custom_settings 加入,每个钩子往 events.jsonl 记一行;内置顺序取自 SPIDER_MIDDLEWARES_BASE
    • 没有验证:process_spider_exception 写成异步生成器时的行为(试过一次:异步生成器对象会被当成可迭代对象而终止异常链,不是推荐用法);Referer、Depth、MetaCopyDetection 三个内置中间件的细节(Depth 见后面的文章);与 spider_middleware 里 from_crawler / NotConfigured 的组合;process_start 的异常处理
    京ICP备2026067502号-1京公网安备11010502063487号