Spider Middleware完全指南 - 数据预处理与后处理技术详解
📂 所属阶段:第五阶段 — 战力升级(分布式与进阶篇)
🔗 相关章节:Downloader Middleware · Pipeline管道实战
Spider Middleware 是 Scrapy 中非常灵活的一个扩展点,它直接运行在引擎和你的 Spider 之间,让你有机会在数据流入 / 流出 Spider 时做统一处理。用好它,你就能轻松实现编码修复、数据清洗、异常降级、前置去重等通用功能,而不用在每个 Spider 里复制粘贴相同的代码。
本文会带你从最基础的概念开始,一步步掌握 Spider Middleware 的配置、三个核心方法,并结合实际场景写出可复用的中间件。最后还会分享排坑经验和最佳实践,帮你少走弯路。
目录
核心基础与位置
在 Scrapy 架构中,Spider Middleware 是夹在引擎和 Spider 之间的钩子,它主要拦截两类数据:
- 输入方向:从引擎传递到 Spider 的 Response —— 可以在这里做预处理,比如清洗 HTML、修复编码、识别反爬页面。
- 输出方向:Spider 产生的 Item 和 Request —— 可以在这里做后处理,比如补充元数据、文本清洗、过滤重复数据。
简化请求流:
Engine → Scheduler → Downloader → ① Spider Middleware(输入层)→ Spider → ② Spider Middleware(输出层)→ Engine → Pipeline / Scheduler
也就是说,Spider Middleware 正好站在“解析数据”的前后,非常适合做全站通用的数据预处理和后处理逻辑。
配置与极简生命周期
激活中间件
在 settings.py 中通过 SPIDER_MIDDLEWARES 字典配置,键为中间件路径,值为优先级(0-1000)。数字越小,越先处理输入响应;数字越小,也越先处理输出结果(因为输出要经过中间件链,顺序相反)。
比如下面这个配置:
# settings.py
SPIDER_MIDDLEWARES = {
'myproject.middlewares.AntiCrawlEncodingFixMiddleware': 400,
'myproject.middlewares.EnrichCleanMiddleware': 450,
'myproject.middlewares.ClassifiedExceptionMiddleware': 500,
'myproject.middlewares.DistributedPreDedupMiddleware': 600,
}
解释一下执行顺序:
- 输入阶段(响应到达 Spider 前):先经过
AntiCrawlEncodingFixMiddleware(优先级 400),再经过 EnrichCleanMiddleware(450)……
- 输出阶段(Spider 生成结果后):因为优先级越小的中间件越靠外,所以
DistributedPreDedupMiddleware(600)先处理,然后才是 ClassifiedExceptionMiddleware(500),最后才轮到最早的中间件。输出链的顺序与输入链正好相反。
三个必须掌握的方法
Spider Middleware 有四个钩子方法,最常用的是下面三个(第四个 process_start_requests 用得较少,本文暂不展开)。
-
process_spider_input(response, spider)
在响应进入 Spider 之前触发。
- 返回
None:继续传递给下一个中间件。
- 抛出异常:中断输入流程,转入exception-handling链。
-
process_spider_output(response, result, spider)
在 Spider 产生结果(Item 或 Request)之后触发,result 是 Spider 返回的迭代器。
- 必须返回一个可迭代对象(推荐直接使用生成器,节省内存)。
- 可以在这里删除、替换或添加输出的 item/request。
-
process_spider_exception(response, exception, spider)
当 process_spider_input 或 Spider 回调本身抛出异常时触发。
- 返回
None:异常将继续向上传递。
- 返回 a 可迭代对象:认为异常已经被处理,Scrapy 会继续处理该可迭代对象(比如返回新的 Request 或空列表跳过该页面)。
三大核心方法实战
下面我们直接动手,用三个单文件可用的中间件,覆盖最常见的输入预处理、输出后处理和异常分类场景。
3.1 输入预处理:反爬检测 + 编码修正
很多网站会返回短小的反爬页面(比如验证码页面),或者 Response 声明的编码不正确。我们可以在一个中间件里同时解决这两个问题。
import chardet
import re
from scrapy.exceptions import IgnoreRequest
class AntiCrawlEncodingFixMiddleware:
"""
输入预处理:
1. 快速检测反爬特征(仅检查小体积响应)
2. 使用 chardet 自动修正编码
"""
def process_spider_input(self, response, spider):
# ---------- 反爬检测 ----------
# 只对体积较小的响应做检查,避免浪费资源
if len(response.body) < 10240:
anti_keywords = ['验证码', 'blocked', '访问频繁', 'forbidden']
if any(kw in response.text.lower() for kw in anti_keywords):
spider.logger.warning(f"疑似反爬页面,跳过:{response.url}")
raise IgnoreRequest("Anti-crawling detected")
# ---------- 编码修正 ----------
# chardet 检测编码,置信度超过 0.9 时才覆盖 header 中的编码
detect = chardet.detect(response.body)
if detect['confidence'] > 0.9:
response._encoding = detect['encoding']
response.meta['fixed_encoding'] = detect['encoding'] # 记录一下,便于后续查看
return None
关键点:
- 通过抛出
IgnoreRequest 直接丢弃响应,避免后续无意义的解析。
- 修改
response._encoding 会影响 Scrapy 的文本解码,确保 response.text 以正确编码展示。
3.2 输出后处理:数据增强 + 文本清洗
Spider 产出的数据经常需要统一清洗(比如去除多余换行和空格),并补充一些元信息(来源 URL、爬取时间、内容 MD5)。这一套操作很适合放在输出中间件里。
import time
import hashlib
import re
from itemadapter import ItemAdapter
from scrapy.item import Item
class EnrichCleanMiddleware:
"""输出后处理:文本清洗 + 元数据增强"""
def process_spider_output(self, response, result, spider):
for obj in result:
if isinstance(obj, (dict, Item)):
self._clean_text(obj)
self._enrich_meta(obj, response)
yield obj
else:
# Request 对象直接通过,不做修改
yield obj
def _clean_text(self, obj):
adapter = ItemAdapter(obj)
for k, v in adapter.items():
if isinstance(v, str):
# 把换行和连续空格替换成一个空格,并去掉首尾空白
adapter[k] = re.sub(r'\s+', ' ', v.strip())
def _enrich_meta(self, obj, response):
adapter = ItemAdapter(obj)
# 生成稳定的内容 MD5(用于后续去重)
content_bytes = str(sorted(adapter.asdict().items())).encode()
adapter['_item_md5'] = hashlib.md5(content_bytes).hexdigest()
# 添加来源和时间
adapter['_source_url'] = response.url
adapter['_crawl_time'] = time.strftime('%Y-%m-%d %H:%M:%S')
这样,所有经过这个中间件的 Item 都会自动带上干净的文本和标准化的辅助字段,实战中非常方便。
3.3 异常分类处理:重试 vs 跳过
爬虫运行中难免遇到网络波动或页面结构变化。我们可以根据异常类型决定是重试请求还是直接跳过,避免一个页面错误导致整个爬虫中断。
from scrapy.http import Request
class ClassifiedExceptionMiddleware:
"""
异常分类处理:
- 网络异常自动重试(可配置次数)
- 解析异常直接跳过
- 其他异常继续向上抛出
"""
def __init__(self, max_retry=2):
self.max_retry = max_retry
@classmethod
def from_crawler(cls, crawler):
return cls(max_retry=crawler.settings.getint('SM_MAX_RETRY', 2))
def process_spider_exception(self, response, exception, spider):
exc_name = type(exception).__name__
spider.logger.error(f"{exc_name} 处理失败:{response.url} | {exception}")
# ---------- 网络类异常:重试 ----------
network_errors = ['ConnectionError', 'TimeoutError', 'ConnectTimeout']
if exc_name in network_errors:
retry_cnt = response.meta.get('retry_cnt', 0) + 1
if retry_cnt <= self.max_retry:
spider.logger.info(f"第{retry_cnt}次重试:{response.url}")
# 返回新 Request,dont_filter=True 保证被调度
return [response.request.replace(dont_filter=True, meta={'retry_cnt': retry_cnt})]
# ---------- 解析类异常:跳过 ----------
parse_errors = ['ValueError', 'KeyError', 'IndexError']
if exc_name in parse_errors:
spider.logger.warning(f"解析失败,跳过页面:{response.url}")
return [] # 返回空列表表示已处理异常,不再向上传递
# 其他异常,不做处理,让 Scrapy 按默认逻辑处理
return None
这样,你的 Spider 就会变得非常健壮:网络问题自动重试,解析问题自动跳过,其他未知异常保留原有行为。
高频场景:分布式前置去重
很多项目都会使用 Item Pipeline 进行数据去重,但 Pipeline 的缺点是等 Spider 把所有数据都解析完才去重,解析阶段白白浪费了 CPU 和网络资源。更高效的做法是前置去重——在 Spider Middleware 的输出阶段就把重复数据过滤掉。
下面的中间件利用 Redis 的 Set 结构,根据前面生成的 _item_md5 进行去重,避免重复数据进入 Pipeline。
import redis
from itemadapter import ItemAdapter
from scrapy.item import Item
class DistributedPreDedupMiddleware:
"""分布式前置去重,基于 Redis Set 和 Item MD5"""
def __init__(self, redis_url, dedup_key, expire_days=7):
self.redis = redis.from_url(redis_url)
self.dedup_key = dedup_key
self.expire_seconds = 86400 * expire_days
@classmethod
def from_crawler(cls, crawler):
return cls(
redis_url=crawler.settings.get('REDIS_URL', 'redis://localhost:6379'),
dedup_key=crawler.settings.get('SM_DEDUP_KEY', 'spider:item_dedup:md5'),
expire_days=crawler.settings.getint('SM_DEDUP_EXPIRE', 7)
)
def process_spider_output(self, response, result, spider):
for obj in result:
if isinstance(obj, (dict, Item)):
item_md5 = ItemAdapter(obj).get('_item_md5')
if item_md5 and self.redis.sismember(self.dedup_key, item_md5):
spider.logger.debug(f"已存在,前置去重跳过:{response.url}")
continue # 直接丢弃重复 item
# 添加到去重集合
self.redis.sadd(self.dedup_key, item_md5)
self.redis.expire(self.dedup_key, self.expire_seconds)
yield obj
配置提醒:这个中间件依赖前面的 EnrichCleanMiddleware 先添加 _item_md5,因此需要注意中间件优先级顺序——数据增强中间件的优先级必须小于(数值更小)去重中间件,这样才能保证在去重之前 MD5 已经生成。
避坑指南&最佳实践
⚠️ 常见坑点
-
优先级搞反导致输出顺序错乱
输入和输出的处理顺序正好相反。一个容易犯的错误是把“先做增强再去重”的优先级写成了 500(增强)和 400(去重),就会导致去重时 _item_md5 还没生成。记住:输出链上,优先级数字大的先执行。
-
内存泄漏
- 不要在中间件实例中保存大量数据(比如全局缓存),否则会随爬虫运行时间增长而耗尽内存。如需缓存,请使用
functools.lru_cache 或 Redis 等外部存储。
process_spider_output 里尽量直接 yield 结果,不要先转成列表再返回,否则内存占用会瞬间飙升。
-
异常被错误“吃掉”
在 process_spider_exception 中,只有当你确实能处理异常时才返回可迭代对象(比如 [])。如果返回了 None,异常会继续向上传递;如果返回了可迭代对象,Scrapy 就认为异常已经处理完毕,这会抑制你未预期的错误。所以,返回前务必记录详细的日志。
🎯 最佳实践
-
单一职责
每个中间件只专注做 1~2 件强相关的事。比如上面我们拆分了“反爬+编码”、“增强+清洗”、“异常分类”、“去重”,而不是把所有逻辑塞到一个中间件里。
-
参数可配置
像重试次数、Redis 地址这些容易变化的值,通过 settings 和 from_crawler 方式传入,保持中间件的可复用性。
-
前置去重优于后置去重
尽量在数据进入 Pipeline 之前就去重,能显著减少后续资源的浪费。结合 Scrapy-Redis 等分布式方案,还能在集群级别去重。
-
轻量处理原则
Spider Middleware 运行在异步循环中,不要执行 CPU 密集型操作(如大图 OCR、复杂 NLP)。这类任务适合丢到 Pipeline 或 Celery 等异步任务中处理。
🔗 相关教程推荐
🏷️ 标签云: Scrapy Spider Middleware 数据预处理 数据后处理 exception-handling 网络爬虫