前言
Scrapy 框架的高扩展性核心体现在其模块化的组件设计,而中间件(Middleware)是连接引擎(Engine)与其他核心组件(下载器、爬虫、响应处理)的关键桥梁。无论是应对反爬机制(如 UA 伪装、IP 代理、Cookie 池),还是实现请求 / 响应的个性化处理(如数据加密 / 解密、请求重试),自定义中间件都是最优解决方案。本文将从中间件的核心原理入手,系统讲解 Scrapy 各类中间件的开发规范,结合实战案例实现 UA 随机切换、代理池集成、请求重试等高频需求,帮助开发者掌握中间件的定制化开发能力,解决爬虫开发中的各类个性化与反爬问题。
摘要
本文聚焦 Scrapy 中间件的自定义开发实战,首先剖析 Scrapy 中间件的分类(下载器中间件、爬虫中间件)及执行流程,明确不同中间件的作用域与优先级规则;其次通过多个实战案例(目标站点:豆瓣图书 Top100),分别实现自定义 User-Agent 中间件、IP 代理中间件、请求重试中间件、响应数据清洗中间件;最后讲解中间件的调试方法与优先级调优策略。通过本文,读者可掌握 Scrapy 中间件的开发逻辑,灵活应对各类爬虫场景的个性化需求,提升爬虫的稳定性与抗反爬能力。
一、Scrapy 中间件核心原理
1.1 中间件分类与作用
Scrapy 中间件分为两大类,核心作用与执行阶段如下表所示:
| 中间件类型 | 作用域 | 核心作用 |
|---|---|---|
| 下载器中间件 | 引擎 ↔ 下载器 | 处理请求(如修改 UA、添加代理、加密参数)、处理响应(如解密、数据清洗)、请求异常重试 |
| 爬虫中间件 | 引擎 ↔ 爬虫 | 处理爬虫产出的 Item、调整请求优先级、过滤无效请求 / 响应 |
1.2 中间件执行流程
- 下载器中间件执行顺序:
- 请求方向(引擎→下载器):按
settings.py中DOWNLOADER_MIDDLEWARES配置的优先级(数字越小越先执行)依次执行process_request方法; - 响应方向(下载器→引擎):按优先级(数字越大越先执行)依次执行
process_response方法; - 异常处理:请求失败时执行
process_exception方法。
- 请求方向(引擎→下载器):按
- 爬虫中间件执行顺序:
- 请求方向(爬虫→引擎):执行
process_spider_output方法; - 响应方向(引擎→爬虫):执行
process_spider_input方法; - 异常处理:执行
process_spider_exception方法。
- 请求方向(爬虫→引擎):执行
1.3 核心方法说明
| 方法名 | 所属中间件 | 触发时机 | 返回值规则 |
|---|---|---|---|
| process_request | 下载器 | 引擎将请求发送至下载器前 | 返回 None:继续执行后续中间件;返回 Response:直接返回响应;返回 Request:重新调度请求 |
| process_response | 下载器 | 下载器返回响应至引擎前 | 返回 Response:继续执行后续中间件;返回 Request:重新调度请求 |
| process_exception | 下载器 | 请求抛出异常时 | 返回 None:继续抛出异常;返回 Response:替代异常结果;返回 Request:重新调度请求 |
| process_spider_input | 爬虫 | 响应发送至爬虫前 | 返回 None:正常执行;抛出异常:触发异常处理 |
| process_spider_output | 爬虫 | 爬虫产出 Item/Request 后 | 返回迭代器:包含 Item/Request 对象 |
二、环境搭建
2.1 基础环境要求
| 软件 / 库 | 版本要求 | 作用 |
|---|---|---|
| Python | ≥3.8 | 基础开发环境 |
| Scrapy | ≥2.6 | 爬虫框架 |
| fake-useragent | ≥1.1.1 | 生成随机 User-Agent |
| requests | ≥2.28 | 代理池接口请求(可选) |
2.2 环境安装
bash
运行
pip install scrapy==2.6.2 fake-useragent==1.1.1 requests==2.28.2三、自定义中间件实战开发
3.1 创建基础爬虫项目
bash
运行
# 创建项目 scrapy startproject douban_book_middleware # 进入项目目录 cd douban_book_middleware # 创建爬虫文件 scrapy genspider douban_book_top100 book.douban.com3.2 实战 1:自定义 User-Agent 中间件(反基础反爬)
3.2.1 开发思路
目标网站常通过固定 User-Agent 识别爬虫,需在请求头中随机切换 UA。通过下载器中间件的process_request方法修改请求头,利用fake-useragent生成随机 UA。
3.2.2 中间件实现(middlewares.py)
python
运行
from fake_useragent import UserAgent class RandomUserAgentMiddleware: """随机切换 User-Agent 中间件""" def __init__(self): # 初始化 UA 生成器 self.ua = UserAgent() def process_request(self, request, spider): """修改请求头中的 User-Agent""" # 随机选择一个 UA(可选指定浏览器类型) random_ua = self.ua.random request.headers['User-Agent'] = random_ua spider.logger.info(f"当前使用 User-Agent:{random_ua}") # 返回 None,继续执行后续中间件 return None3.2.3 启用中间件(settings.py)
python
运行
# 启用自定义 UA 中间件,优先级设置为 543(Scrapy 默认中间件优先级范围 0-1000) DOWNLOADER_MIDDLEWARES = { 'douban_book_middleware.middlewares.RandomUserAgentMiddleware': 543, # 关闭 Scrapy 默认的 UserAgent 中间件 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, }3.2.4 测试验证
修改爬虫文件(douban_book_top100.py):
python
运行
import scrapy class DoubanBookTop100Spider(scrapy.Spider): name = 'douban_book_top100' allowed_domains = ['book.douban.com'] start_urls = ['https://book.douban.com/top250'] def parse(self, response): # 仅打印响应状态码,验证 UA 中间件生效 self.logger.info(f"响应状态码:{response.status}") yield {'url': response.url, 'status': response.status}启动爬虫:
bash
运行
scrapy crawl douban_book_top1003.2.5 输出结果与原理
输出日志示例:
plaintext
2025-12-18 10:00:00 [douban_book_top100] INFO: 当前使用 User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 2025-12-18 10:00:01 [douban_book_top100] INFO: 响应状态码:200核心原理:
process_request方法在请求发送至下载器前被调用,修改request.headers即可替换 UA;- 优先级 543 处于 Scrapy 默认中间件的中间区间,确保自定义 UA 覆盖默认值;
fake-useragent内置主流浏览器的 UA 池,可随机生成不同类型的 UA,降低被识别为爬虫的概率。
3.3 实战 2:自定义 IP 代理中间件(反 IP 封禁)
3.3.1 开发思路
当单 IP 访问频率过高时,目标网站会封禁 IP,需通过代理池动态切换 IP。本案例实现从本地代理池接口获取可用代理,在请求中添加代理配置。
3.3.2 中间件实现(middlewares.py)
python
运行
import requests import random class RandomProxyMiddleware: """随机切换 IP 代理中间件""" def __init__(self): # 代理池接口(需自行搭建代理池,如 scrapy-proxypool、ProxyPool) self.proxy_pool_url = "http://127.0.0.1:5010/get/" # 无效代理列表 self.invalid_proxies = [] def get_random_proxy(self): """从代理池获取随机可用代理""" try: response = requests.get(self.proxy_pool_url, timeout=5) if response.status_code == 200: proxy = response.text.strip() if proxy and proxy not in self.invalid_proxies: return f"http://{proxy}" except Exception as e: self.logger.error(f"获取代理失败:{e}") return None def process_request(self, request, spider): """为请求添加代理""" proxy = self.get_random_proxy() if proxy: request.meta['proxy'] = proxy spider.logger.info(f"当前使用代理:{proxy}") return None def process_exception(self, request, exception, spider): """请求异常时,标记代理无效并重新请求""" if 'proxy' in request.meta: invalid_proxy = request.meta['proxy'].replace('http://', '') self.invalid_proxies.append(invalid_proxy) spider.logger.warning(f"代理 {invalid_proxy} 无效,已加入黑名单") # 重新生成请求,不使用该代理 new_request = request.copy() new_request.dont_filter = True # 避免被去重过滤 return new_request3.3.3 启用中间件(settings.py)
python
运行
DOWNLOADER_MIDDLEWARES = { 'douban_book_middleware.middlewares.RandomUserAgentMiddleware': 543, 'douban_book_middleware.middlewares.RandomProxyMiddleware': 542, # 优先级高于 UA 中间件 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, } # 增加超时时间,适配代理请求 DOWNLOAD_TIMEOUT = 103.3.4 输出结果与原理
输出日志示例:
plaintext
2025-12-18 10:05:00 [douban_book_top100] INFO: 当前使用代理:http://123.125.71.3:8080 2025-12-18 10:05:01 [douban_book_top100] INFO: 响应状态码:200 2025-12-18 10:06:00 [douban_book_top100] WARNING: 代理 192.168.1.1:8888 无效,已加入黑名单核心原理:
process_request为请求添加request.meta['proxy']配置,Scrapy 下载器会通过该代理发送请求;process_exception捕获请求异常(如超时、连接失败),标记代理无效并重新生成请求;- 优先级 542 高于 UA 中间件,确保代理配置先于 UA 生效;
dont_filter = True避免重新生成的请求被去重组件过滤。
3.4 实战 3:自定义请求重试中间件(提升稳定性)
3.4.1 开发思路
网络波动或目标网站临时故障会导致请求失败,需自定义重试逻辑,针对特定状态码(如 403、500)或异常类型进行重试,并限制重试次数。
3.4.2 中间件实现(middlewares.py)
python
运行
from scrapy.downloadermiddlewares.retry import RetryMiddleware from scrapy.utils.response import response_status_message class CustomRetryMiddleware(RetryMiddleware): """自定义请求重试中间件""" def process_response(self, request, response, spider): """根据响应状态码决定是否重试""" # 如果请求设置了 dont_retry,则不重试 if request.meta.get('dont_retry', False): return response # 针对 403、500、502、503 状态码重试 if response.status in [403, 500, 502, 503]: reason = response_status_message(response.status) spider.logger.warning(f"响应状态码 {response.status},触发重试:{reason}") return self._retry(request, reason, spider) or response return response def process_exception(self, request, exception, spider): """针对特定异常重试""" # 捕获连接超时、连接拒绝异常 if isinstance(exception, (scrapy.core.downloader.handlers.http11.TunnelError, scrapy.exceptions.ConnectionTimeout)): reason = f"请求异常:{type(exception).__name__}" spider.logger.warning(reason) return self._retry(request, reason, spider) return super().process_exception(request, exception, spider)3.4.3 启用中间件(settings.py)
python
运行
DOWNLOADER_MIDDLEWARES = { 'douban_book_middleware.middlewares.RandomUserAgentMiddleware': 543, 'douban_book_middleware.middlewares.RandomProxyMiddleware': 542, 'douban_book_middleware.middlewares.CustomRetryMiddleware': 541, 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, # 关闭 Scrapy 默认的重试中间件 'scrapy.downloadermiddlewares.retry.RetryMiddleware': None, } # 重试配置 RETRY_TIMES = 3 # 最大重试次数 RETRY_HTTP_CODES = [403, 500, 502, 503] # 需重试的状态码3.4.4 输出结果与原理
输出日志示例:
plaintext
2025-12-18 10:10:00 [douban_book_top100] WARNING: 响应状态码 403,触发重试:Forbidden 2025-12-18 10:10:02 [douban_book_top100] WARNING: 响应状态码 403,触发重试:Forbidden 2025-12-18 10:10:04 [douban_book_top100] INFO: 响应状态码:200核心原理:
- 继承 Scrapy 原生
RetryMiddleware,重写process_response和process_exception方法,自定义重试规则; _retry方法为内置方法,会自动增加重试次数并重新调度请求;RETRY_TIMES限制最大重试次数,避免无限重试。
3.5 实战 4:自定义爬虫中间件(数据过滤)
3.5.1 开发思路
爬虫产出的 Item 可能包含无效数据(如空值、重复值),需通过爬虫中间件的process_spider_output方法过滤无效 Item。
3.5.2 中间件实现(middlewares.py)
python
运行
class ItemFilterMiddleware: """过滤无效 Item 的爬虫中间件""" def process_spider_output(self, response, result, spider): """处理爬虫输出的 Item/Request""" for item in result: # 仅处理 Item 对象,Request 对象直接放行 if isinstance(item, scrapy.Item): # 过滤空标题的 Item if not item.get('title') or item.get('title').strip() == '': spider.logger.warning(f"过滤无效 Item:标题为空") continue # 过滤评分低于 8.0 的 Item score = item.get('score', 0) try: if float(score) < 8.0: spider.logger.warning(f"过滤无效 Item:评分 {score} 低于 8.0") continue except ValueError: spider.logger.warning(f"过滤无效 Item:评分 {score} 格式错误") continue yield item3.5.3 启用中间件(settings.py)
python
运行
# 启用爬虫中间件 SPIDER_MIDDLEWARES = { 'douban_book_middleware.middlewares.ItemFilterMiddleware': 543, } # 完善 Item 定义(items.py) import scrapy class DoubanBookMiddlewareItem(scrapy.Item): title = scrapy.Field() score = scrapy.Field() author = scrapy.Field()3.5.4 测试验证
修改爬虫文件的parse方法:
python
运行
def parse(self, response): book_list = response.xpath('//tr[@class="item"]') for book in book_list: item = DoubanBookMiddlewareItem() item['title'] = book.xpath('.//a/@title').extract_first() item['score'] = book.xpath('.//span[@class="rating_nums"]/text()').extract_first() item['author'] = book.xpath('.//p[@class="pl"]/text()').extract_first() yield item启动爬虫后,日志输出示例:
plaintext
2025-12-18 10:15:00 [douban_book_top100] WARNING: 过滤无效 Item:评分 7.9 低于 8.0 2025-12-18 10:15:00 [douban_book_top100] WARNING: 过滤无效 Item:标题为空核心原理:
process_spider_output接收爬虫产出的迭代器(包含 Item/Request),遍历并过滤无效 Item;- 仅放行符合条件的 Item,确保最终存储的数据有效性;
- 爬虫中间件优先级规则与下载器中间件一致,数字越小越先执行。
四、中间件调试与优先级调优
4.1 中间件调试方法
| 调试方式 | 适用场景 | 操作方法 |
|---|---|---|
| 日志打印 | 验证中间件是否执行、参数是否正确 | 在中间件方法中添加spider.logger.info/.warning/error打印关键信息 |
| Scrapy shell | 测试单个请求的中间件执行流程 | scrapy shell https://book.douban.com/top250,查看request.headers/meta |
| 断点调试 | 定位中间件逻辑错误 | 在中间件方法中添加import pdb; pdb.set_trace(),启动爬虫后分步调试 |
4.2 优先级调优规则
| 优先级区间 | 作用 | 示例配置 |
|---|---|---|
| 0-500 | 核心基础中间件(如代理、UA) | 代理中间件(542)、UA 中间件(543) |
| 500-800 | 业务逻辑中间件(如重试、解密) | 重试中间件(541)、数据解密中间件(600) |
| 800-1000 | 后置处理中间件(如数据清洗) | 响应数据清洗中间件(900) |
核心原则:
- 依赖前置的中间件优先级更高(如代理配置需先于 UA 配置);
- 数据处理类中间件后置执行(如先获取响应,再清洗数据);
- 避免优先级冲突(相同优先级的中间件执行顺序不确定)。
五、常见问题与解决方案
| 问题现象 | 原因分析 | 解决方案 |
|---|---|---|
| 中间件未执行 | 未在 settings.py 中启用 / 优先级配置错误 | 检查DOWNLOADER_MIDDLEWARES/SPIDER_MIDDLEWARES配置,调整优先级 |
| UA 未生效 | 默认 UserAgent 中间件未关闭 | 关闭scrapy.downloadermiddlewares.useragent.UserAgentMiddleware |
| 代理配置后请求超时 | 代理无效 / 代理池接口不可用 | 验证代理可用性,增加代理池健康检查逻辑 |
| 重试中间件无限重试 | 未设置RETRY_TIMES/ 重试条件过于宽松 | 配置RETRY_TIMES,缩小重试状态码 / 异常范围 |
| 爬虫中间件过滤掉有效 Request | 逻辑错误,误过滤 Request 对象 | 在process_spider_output中判断对象类型,仅过滤 Item |
六、总结
本文系统讲解了 Scrapy 中间件的自定义开发流程,从核心原理出发,通过 4 个实战案例覆盖了下载器中间件(UA 切换、代理池、请求重试)和爬虫中间件(数据过滤)的开发与配置,同时给出了调试方法与优先级调优策略。中间件作为 Scrapy 框架的扩展核心,能够灵活应对反爬机制、数据处理、请求稳定性等各类需求,是企业级爬虫开发的必备技能。
在实际开发中,可根据业务场景扩展更多个性化中间件:如 Cookie 池中间件、响应数据解密中间件、请求参数加密中间件等。掌握中间件的开发逻辑后,可大幅提升爬虫的适应性与稳定性,解决各类复杂的爬虫场景问题。