Scrapy Downloader Middleware 深入分析
Downloader Middleware(下载器中间件)是 Scrapy 框架中最核心的扩展机制之一,它位于引擎(Engine)与下载器(Downloader)之间,是所有 HTTP 请求与响应的必经之路。理解其工作原理、执行顺序和开发范式,是从 "会用 Scrapy" 迈向 "精通 Scrapy" 的关键一步。
一、核心定位与架构价值
1.1 在 Scrapy 架构中的位置
Scrapy 采用经典的引擎 - 调度器 - 下载器 - 爬虫四层架构,Downloader Middleware 恰好处于引擎与下载器之间的咽喉位置:
- 请求下行路径:Engine → Scheduler → Downloader Middleware → Downloader → Internet
- 响应上行路径:Internet → Downloader → Downloader Middleware → Engine → Spider
所有从调度器取出的 Request 在发送到网络之前,必须依次经过所有下载中间件的 process_request 处理;所有从网络返回的 Response 在交给爬虫解析之前,必须反向依次经过所有下载中间件的 process_response 处理Scrapy。
1.2 为什么需要下载中间件
下载中间件本质上是一套面向请求 / 响应的全局钩子系统,它提供了一种非侵入式的扩展方式,让你可以在不修改 Scrapy 核心代码和 Spider 业务逻辑的前提下,实现以下能力:
- 请求发送前的统一修改(Header、Cookie、代理、超时等)
- 响应返回后的统一处理(状态码校验、内容解码、数据清洗)
- 异常发生时的统一处理(重试、降级、丢弃)
- 短路下载流程(直接返回缓存响应、模拟响应)
- 全局流量控制(限速、并发控制、频率限制)
如果没有中间件,这些逻辑就会散落在每个 Spider 的 start_requests 或 parse 方法中,造成大量重复代码和维护灾难。
二、工作原理与核心方法
每个下载中间件类最多可以实现三个核心方法,分别对应请求处理、响应处理和异常处理三个生命周期节点。
2.1 process_request(request, spider)
调用时机:请求从引擎发往下载器之前,按优先级从小到大依次调用。
返回值语义:
- 返回
None:继续执行后续中间件,最终进入下载器 - 返回
Response对象:短路整个下载流程,不再继续调用后续中间件和下载器,直接进入响应处理链路 - 返回
Request对象:将当前请求替换为新的请求,重新进入中间件链处理 - 抛出
IgnoreRequest异常:静默丢弃该请求,触发process_exception链路
这是最常用的方法,User-Agent 切换、代理注入、Cookie 携带、请求签名等都在此处实现。
2.2 process_response(request, response, spider)
调用时机:响应从下载器返回引擎之前,按优先级从大到小反向依次调用。
返回值语义:
- 返回
Response对象:继续执行后续(优先级更低的)中间件 - 返回
Request对象:中断响应流程,将该请求重新放回调度队列 - 抛出
IgnoreRequest异常:丢弃该响应
该方法常用于响应校验、内容解密、状态码拦截、元数据注入等场景。
2.3 process_exception(request, exception, spider)
调用时机:下载器或任意 process_request 抛出异常时触发,按优先级从大到小反向依次调用。
返回值语义:
- 返回
None:继续执行后续异常处理中间件 - 返回
Response对象:异常恢复,将该 Response 送入响应处理链路 - 返回
Request对象:异常恢复,将该请求重新调度
失败重试、代理切换、异常日志等功能都在此方法中实现。
重要提示:Scrapy 2.0+ 支持将这三个方法定义为协程函数(
async def),可以进一步提升异步处理效率。
三、执行顺序与优先级机制
3.1 优先级数字规则
下载中间件通过一个数字类型的 order 值决定执行顺序,数字越小优先级越高:
- 请求方向:按 order 升序执行(小 → 大)
- 响应 / 异常方向:按 order 降序执行(大 → 小)
可以把中间件想象成一个洋葱:请求时从外到内层层包裹,响应时从内到外层层剥离。优先级最小的中间件最靠近引擎,优先级最大的最靠近下载器。
3.2 默认内置中间件顺序
Scrapy 内置了十余个下载中间件,定义在 DOWNLOADER_MIDDLEWARES_BASE 中,以下是核心组件及其默认优先级:
表格
| 优先级 | 中间件类 | 核心功能 |
|---|---|---|
| 50 | OffsiteMiddleware | 过滤跨域请求 |
| 100 | RobotsTxtMiddleware | robots.txt 协议校验 |
| 300 | HttpAuthMiddleware | HTTP 基础认证 |
| 350 | DownloadTimeoutMiddleware | 下载超时控制 |
| 400 | DefaultHeadersMiddleware | 默认请求头注入 |
| 500 | UserAgentMiddleware | User-Agent 设置 |
| 550 | RetryMiddleware | 失败自动重试 |
| 600 | RedirectMiddleware | 重定向自动跟随 |
| 620 | MetaRefreshMiddleware | meta 标签刷新跟随 |
| 700 | HttpProxyMiddleware | HTTP 代理支持 |
| 750 | CookiesMiddleware | Cookie 自动管理 |
| 830 | CompressionMiddleware | 响应内容解压 |
| 900 | ChunkedTransferMiddleware | 分块传输支持 |
| 950 | StatsDownloaderMiddleware | 下载统计数据收集 |
3.3 自定义中间件的顺序选择
自定义中间件的优先级通常设置在 400-700 之间,但具体位置取决于功能依赖:
- 如果你的中间件依赖 Cookie 或代理,应该放在
HttpProxyMiddleware(700)和CookiesMiddleware(750)之前 - 如果你的中间件需要修改 User-Agent,应该放在
UserAgentMiddleware(500)之前或直接替换它 - 如果你的中间件需要处理重试逻辑,应该放在
RetryMiddleware(550)之前
禁用某个内置中间件的方式是在项目 DOWNLOADER_MIDDLEWARES 中将其值设为 None。
四、内置中间件深度解析
4.1 RetryMiddleware:失败重试机制
这是生产环境最重要的中间件之一,负责处理临时网络故障和服务端错误。
核心配置项:
RETRY_ENABLED:是否启用重试,默认 TrueRETRY_TIMES:最大重试次数,默认 2 次(加上首次请求共 3 次)RETRY_HTTP_CODES:触发重试的 HTTP 状态码,默认 [500, 502, 503, 504, 408, 429]RETRY_PRIORITY_ADJUST:重试请求的优先级调整量,默认 -1(降低优先级)
工作机制:
- 在
process_response中检测状态码是否在重试列表中 - 在
process_exception中捕获连接超时、DNS 失败等网络异常 - 每次重试时在
request.meta['retry_times']中计数 - 达到最大重试次数后,请求被标记为失败
4.2 CookiesMiddleware:会话自动管理
模拟浏览器的 Cookie 行为,自动跟踪服务端 Set-Cookie 并在后续请求中携带。
工作原理:
- 维护一个基于域名的 Cookie 罐(Cookie Jar)
- 响应经过时,解析
Set-Cookie头并存入对应域名的罐中 - 请求经过时,从对应域名的罐中取出 Cookie 注入请求头
注意事项:
- 通过
Request.cookies参数设置的 Cookie 会被正确处理 - 直接在
Request.headers中设置的Cookie头会被忽略(已知限制) - 可以通过
request.meta['cookiejar']指定使用多个独立的 Cookie 罐
4.3 RedirectMiddleware:重定向处理
自动处理 3xx 重定向响应,生成新的请求继续抓取。
核心配置:
REDIRECT_ENABLED:是否启用重定向,默认 TrueREDIRECT_MAX_TIMES:最大重定向次数,默认 20 次REDIRECT_PRIORITY_ADJUST:重定向请求的优先级调整,默认 +1
支持的重定向类型:
- 301/302/303/307/308 标准 HTTP 重定向
- 303 自动将 POST 转为 GET
- 可通过
request.meta['dont_redirect']单独禁用某个请求的重定向
4.4 HttpProxyMiddleware:代理支持
读取 request.meta['proxy'] 中的代理地址并设置到请求中。
使用方式:
# 在 Spider 中为单个请求设置代理
yield scrapy.Request(url, meta={'proxy': 'http://127.0.0.1:8080'})
该中间件本身只负责读取并应用代理,不负责代理池管理和自动切换。代理池轮换功能通常需要自定义中间件实现。
五、自定义中间件开发实战
5.1 开发步骤
- 在项目中创建
middlewares.py文件 - 定义中间件类,继承
object或DownloaderMiddleware - 实现需要的
process_request/process_response/process_exception方法 - 在
settings.py的DOWNLOADER_MIDDLEWARES中注册并设置优先级
5.2 实战案例一:随机 User-Agent 中间件
import random
from scrapy import signals
class RandomUserAgentMiddleware:
"""随机切换 User-Agent 的下载中间件"""
def __init__(self, user_agents):
self.user_agents = user_agents
@classmethod
def from_crawler(cls, crawler):
"""从 Crawler 上下文初始化,读取配置"""
return cls(
user_agents=crawler.settings.getlist('USER_AGENT_POOL')
)
def process_request(self, request, spider):
# 从池中随机选择一个 UA
ua = random.choice(self.user_agents)
request.headers['User-Agent'] = ua
# 返回 None 表示继续后续处理
return None
配置:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.RandomUserAgentMiddleware': 450,
# 禁用默认的 UA 中间件
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
}
USER_AGENT_POOL = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...',
# 更多 UA...
]
5.3 实战案例二:动态请求签名中间件
很多网站会对请求参数进行签名校验,这类逻辑非常适合放在中间件中统一处理。
import hashlib
import time
class DynamicSignMiddleware:
"""动态请求签名中间件"""
def process_request(self, request, spider):
# 只对特定域名的请求签名
if 'example.com' not in request.url:
return None
timestamp = str(int(time.time()))
# 构造签名字符串
sign_str = f"{request.url}{timestamp}SECRET_KEY"
sign = hashlib.md5(sign_str.encode()).hexdigest()
# 注入签名头
request.headers['X-Timestamp'] = timestamp
request.headers['X-Sign'] = sign
return None
5.4 实战案例三:代理池自动轮换中间件
class ProxyPoolMiddleware:
"""代理池自动轮换中间件"""
def __init__(self, proxy_pool):
self.proxy_pool = proxy_pool
self.bad_proxies = set()
@classmethod
def from_crawler(cls, crawler):
return cls(proxy_pool=crawler.settings.getlist('PROXY_POOL'))
def process_request(self, request, spider):
# 跳过已设置 dont_proxy 的请求
if request.meta.get('dont_proxy'):
return None
# 从可用代理中随机选择
available = [p for p in self.proxy_pool if p not in self.bad_proxies]
if available:
proxy = random.choice(available)
request.meta['proxy'] = proxy
return None
def process_exception(self, request, exception, spider):
# 代理失败时标记为不可用
proxy = request.meta.get('proxy')
if proxy:
self.bad_proxies.add(proxy)
spider.logger.warning(f"代理失效: {proxy}")
# 返回 request 触发重新调度
return request.replace(dont_filter=True)
return None
六、性能优化与最佳实践
6.1 性能影响因素
下载中间件是每个请求的必经之路,每增加一个中间件就意味着每个请求多两次函数调用(请求一次、响应一次)。对于百万级请求的爬虫,累积开销不可忽视。
优化建议:
- 只启用真正需要的内置中间件,将无用的设为
None - 避免在中间件中进行同步 IO 操作(如数据库查询、文件读写)
- 耗时操作尽量异步化,或使用
from_crawler在初始化阶段一次性完成 - 简单的 Header 设置优先使用
DEFAULT_REQUEST_HEADERS配置,不必写中间件
6.2 常见坑点与避坑指南
坑点 1:在 process_request 中返回 Request 导致死循环 如果返回的新请求与原请求 URL 相同且不过滤,会无限循环。务必加上 dont_filter=True 或修改 URL。
坑点 2:修改 request 对象后忘记返回 None process_request 默认返回 None,直接修改 request 对象即可生效。不要写成 return request,否则会被当作新请求重新处理。
坑点 3:中间件优先级顺序错误 例如将自定义 Cookie 处理放在 CookiesMiddleware 之后,导致设置被覆盖。调试时可以用 scrapy settings --get DOWNLOADER_MIDDLEWARES 查看最终排序。
坑点 4:异常处理中吞掉关键错误 process_exception 中如果返回 Response 或 Request,异常就被 "恢复" 了。慎用此功能,避免掩盖真正的问题。
6.3 调试技巧
- 开启详细日志:设置
LOG_LEVEL='DEBUG',可以看到每个中间件的调用过程 - 使用
downloader_middleware信号:监听中间件执行前后的信号 - 局部禁用:在
request.meta中设置dont_redirect、dont_retry等标志,单个请求跳过特定中间件 - 优先级检查:运行
scrapy fetch --view命令,观察实际请求头和响应状态
七、总结
Downloader Middleware 是 Scrapy 强大扩展性的基石,它通过洋葱模型的链式处理机制,将横切关注点(认证、缓存、重试、代理、伪装等)与业务爬取逻辑完美解耦。
掌握它的核心在于三点:
- 理解双向执行顺序:请求升序、响应降序,数字越小越靠近引擎
- 吃透返回值语义:None、Response、Request、IgnoreRequest 四种返回值各有用途
- 合理设计中间件职责:每个中间件只做一件事,通过组合实现复杂能力
在实际项目中,建议优先复用 Scrapy 内置中间件,只在必要时自定义扩展。过多的自定义中间件会增加调试难度和性能开销,保持中间件链的简洁和清晰,是构建高可用爬虫系统的重要原则。
更多推荐



所有评论(0)