news 2025/12/19 23:48:44

Python 爬虫实战:Scrapy 中间件自定义开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 爬虫实战:Scrapy 中间件自定义开发

前言

Scrapy 框架的高扩展性核心体现在其模块化的组件设计,而中间件(Middleware)是连接引擎(Engine)与其他核心组件(下载器、爬虫、响应处理)的关键桥梁。无论是应对反爬机制(如 UA 伪装、IP 代理、Cookie 池),还是实现请求 / 响应的个性化处理(如数据加密 / 解密、请求重试),自定义中间件都是最优解决方案。本文将从中间件的核心原理入手,系统讲解 Scrapy 各类中间件的开发规范,结合实战案例实现 UA 随机切换、代理池集成、请求重试等高频需求,帮助开发者掌握中间件的定制化开发能力,解决爬虫开发中的各类个性化与反爬问题。

摘要

本文聚焦 Scrapy 中间件的自定义开发实战,首先剖析 Scrapy 中间件的分类(下载器中间件、爬虫中间件)及执行流程,明确不同中间件的作用域与优先级规则;其次通过多个实战案例(目标站点:豆瓣图书 Top100),分别实现自定义 User-Agent 中间件、IP 代理中间件、请求重试中间件、响应数据清洗中间件;最后讲解中间件的调试方法与优先级调优策略。通过本文,读者可掌握 Scrapy 中间件的开发逻辑,灵活应对各类爬虫场景的个性化需求,提升爬虫的稳定性与抗反爬能力。

一、Scrapy 中间件核心原理

1.1 中间件分类与作用

Scrapy 中间件分为两大类,核心作用与执行阶段如下表所示:

中间件类型作用域核心作用
下载器中间件引擎 ↔ 下载器处理请求(如修改 UA、添加代理、加密参数)、处理响应(如解密、数据清洗)、请求异常重试
爬虫中间件引擎 ↔ 爬虫处理爬虫产出的 Item、调整请求优先级、过滤无效请求 / 响应

1.2 中间件执行流程

  1. 下载器中间件执行顺序
    • 请求方向(引擎→下载器):按settings.pyDOWNLOADER_MIDDLEWARES配置的优先级(数字越小越先执行)依次执行process_request方法;
    • 响应方向(下载器→引擎):按优先级(数字越大越先执行)依次执行process_response方法;
    • 异常处理:请求失败时执行process_exception方法。
  2. 爬虫中间件执行顺序
    • 请求方向(爬虫→引擎):执行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.com

3.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 None
3.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_top100
3.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_request
3.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 = 10
3.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_responseprocess_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 item
3.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 池中间件、响应数据解密中间件、请求参数加密中间件等。掌握中间件的开发逻辑后,可大幅提升爬虫的适应性与稳定性,解决各类复杂的爬虫场景问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/18 19:26:02

2025年度GEO服务商权威甄选指南:技术深度与商业价值的双重考量

当一家高端智能家居品牌在三个月内将AI搜索推荐率从行业寂寂无名提升至头部阵营时&#xff0c;其市场总监只说了八个字&#xff1a;“这不是优化&#xff0c;是重构。”随着AIGC从技术演示走向规模化应用&#xff0c;一个品牌的“数字存在”正被彻底改写。权威咨询机构Gartner在…

作者头像 李华
网站建设 2025/12/18 19:25:36

收藏备用!Java程序员转AI大模型:从技术沉淀到AI爆发的进阶之路

AI大模型的规模化应用&#xff0c;正在重构技术人才的价值坐标系。对于深耕Java技术栈的程序员而言&#xff0c;这绝非“被替代”的危机&#xff0c;而是一场基于技术沉淀的“顺势突围”。你在企业级开发中锤炼的架构思维、工程化能力&#xff0c;将成为大模型从技术原型走向产…

作者头像 李华
网站建设 2025/12/18 19:25:35

Python 爬虫实战:Session 会话维持爬取需登录内容

摘要 本文聚焦 Python 爬虫中 Session 会话维持技术&#xff0c;针对需登录访问的网站数据爬取场景&#xff0c;深入解析 Session 的核心工作原理、会话维持机制及实战应用方案。实战验证基于GitHub 个人仓库页&#xff08;需登录访问的私密资源场景&#xff09;&#xff0c;读…

作者头像 李华
网站建设 2025/12/18 19:25:25

基于移相全桥变换器的电池充电仿真模型,采用电压电流双闭环PI控制。 电池先经历CC模式而后进入...

基于移相全桥变换器的电池充电仿真模型&#xff0c;采用电压电流双闭环PI控制。 电池先经历CC模式而后进入CV模式。 运行环境为matlab/simulink在电池充电的世界里&#xff0c;移相全桥变换器&#xff08;PSFB&#xff09;因其高效率和高功率密度而备受青睐。今天&#xff0c;我…

作者头像 李华
网站建设 2025/12/18 19:24:42

基于COMSOL模拟的水力压裂技术研究:固体力学与达西定理的应用

comsol模拟水力压裂&#xff0c;固体力学达西定理。在工程领域&#xff0c;水力压裂技术是一种常用的增强油气开采效率的方法。通过模拟这一过程&#xff0c;我们可以更好地理解裂缝的扩展和流体的流动。今天&#xff0c;我们就来聊聊如何使用COMSOL Multiphysics来模拟水力压裂…

作者头像 李华
网站建设 2025/12/18 19:24:19

Redis 性能调优(二)

Redis 性能调优是一个系统工程&#xff0c;涉及多个层面。以下是全面的调优指南&#xff0c;分为关键方向、具体措施和实战建议&#xff1a;&#x1f527; 核心配置优化1. 内存优化# 配置建议 maxmemory 16gb # 根据物理内存的70-80%设置 maxmemory-policy allkeys-lru # 根据…

作者头像 李华