Scrapy 中间件:请求与响应的强大控制器
# Scrapy 中间件:请求与响应的强大控制器
在 Scrapy 的世界里,如果说 Spider 负责“发现目标”和“解析数据”,Downloader 负责“获取数据”,那么中间件 (Middlewares) 就扮演着“流程控制器”和“增强器”的关键角色。它像一系列的关卡,所有进出 Spider 的请求 (Request) 和响应 (Response) 都必须经过它,这给了我们一个在爬虫流程中进行全局控制和功能注入的强大机会。
# 一、中间件的核心概念
Scrapy 中间件分为两种:
- 下载器中间件 (Downloader Middleware):位于 Scrapy 引擎和下载器之间,主要处理请求的发起和响应的接收。这是最常用、功能最强大的中间件。
- 爬虫中间件 (Spider Middleware):位于 Scrapy 引擎和爬虫之间,主要处理爬虫的输入(响应)和输出(Item 和 Request)。
# 1.1 数据流中的“关卡”
我们可以通过一个流程图来直观地理解下载器中间件在 Scrapy 数据流中的位置:
graph TD
A[引擎 Engine] -->|Request| B(下载器中间件 Downloader Middleware);
B -->|处理后的 Request| C[下载器 Downloader];
C -->|下载页面| D[互联网 Internet];
D -->|Response| C;
C -->|原始 Response| B;
B -->|处理后的 Response| A;
A -->|Response| E[爬虫 Spider];
subgraph "请求路径"
A --> B --> C
end
subgraph "响应路径"
D --> C --> B --> A
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
从图中可以看出,下载器中间件对请求和响应都有拦截和处理的能力。
# 1.2 下载器中间件 vs. 爬虫中间件
特性 | 下载器中间件 (Downloader Middleware) | 爬虫中间件 (Spider Middleware) |
---|---|---|
位置 | 引擎 (Engine) 与下载器 (Downloader) 之间 | 引擎 (Engine) 与爬虫 (Spider) 之间 |
处理对象 | 全局的 Request 和 Response | 从爬虫发出的 Items 和 Requests,以及进入爬虫的 Response |
核心作用 | 在请求发送前修改它(如加UA、加代理),在响应被爬虫处理前修改它(如处理异常状态码、使用Selenium渲染)。 | 在数据进入 Pipeline 前进行过滤,或对爬虫生成的请求进行批量处理。 |
常用场景 | 非常常用。设置代理、更换UA、处理登录Cookie、处理请求重试、集成动态渲染(如Selenium/Playwright)。 | 较少使用。大部分功能可通过下载器中间件或 Pipeline 实现,主要用于深度定制化的场景。 |
结论:在绝大多数情况下,我们打交道的都是功能更强大的下载器中间件。
# 二、下载器中间件 (Downloader Middleware)
一个下载器中间件就是一个定义了特定方法的 Python 类。你无需实现所有方法,只需根据需求选择性地重写。
# 2.1 核心方法
# 1. process_request(self, request, spider)
这是最重要的方法之一。每个从引擎发往下载器的请求都会经过它。
- 参数:
request
: 当前正被处理的Request
对象。spider
: 该请求所属的Spider
对象。
- 返回值:
None
: 最常见的返回值。Scrapy 将继续处理该请求,执行其他中间件的process_request
,最终将请求交给下载器。Response
对象: Scrapy 将跳过下载器,直接将这个Response
对象返回给引擎,再由引擎交给爬虫处理。这相当于“伪造”了一个响应,可以用于读取缓存或快速返回。Request
对象: Scrapy 将停止当前请求的处理,并将这个新的Request
对象重新放入调度器的队列中。这常用于请求重定向或重试。raise IgnoreRequest
: 抛出此异常,该请求将被直接忽略,不会进行任何后续处理。
# 2. process_response(self, request, response, spider)
当下载器完成下载,得到一个 Response
后,该响应在返回给爬虫之前会经过此方法。
- 参数:
request
: 产生此响应的Request
对象。response
: 正在被处理的Response
对象。spider
: 该响应所属的Spider
对象。
- 返回值:
Response
对象: 必须返回一个Response
对象。这个响应会继续被后续的中间件处理,最终交给爬虫。你可以返回原始的response
,也可以创建一个全新的Response
。Request
对象: Scrapy 将停止对当前response
的处理,并将返回的Request
对象重新放入调度器队列。这常用于基于响应内容判断是否需要重试(例如,验证码页面)。raise IgnoreRequest
: 抛出此异常,该响应将被忽略,不会交给爬虫。
# 3. process_exception(self, request, exception, spider)
当下载过程中出现异常(如 DNS 解析失败、连接超时、或者其他中间件的 process_request
抛出异常)时,此方法被调用。
- 参数:
request
: 出现异常的Request
对象。exception
: 捕获到的异常对象 (Exception
类型)。spider
: 该请求所属的Spider
对象。
- 返回值:
None
: Scrapy 将继续处理这个异常,交由后续的中间件处理。Response
对象: 效果同process_request
返回Response
,停止异常传播,开始响应处理流程。Request
对象: 效果同process_request
返回Request
,将新的请求重新调度。这是实现代理重试等功能的关键。
# 2.2 启用中间件
在 settings.py
中配置 DOWNLOADER_MIDDLEWARES
字典来启用你的中间件。
# settings.py
DOWNLOADER_MIDDLEWARES = {
# 键:中间件类的完整路径
# 值:优先级 (0-1000),数字越小,越靠近引擎,越先被执行。
'myproject.middlewares.MyCustomMiddleware': 543,
}
2
3
4
5
6
process_request
的执行顺序:按优先级从小到大依次执行 (500 -> 600 -> 700)。process_response
的执行顺序:按优先级从大到小依次执行 (700 -> 600 -> 500)。
# 三、实战:构建常用的下载器中间件
# 3.1 随机 User-Agent 中间件
这是最基础也最常见的中间件,用于为每个请求更换不同的 User-Agent
,模拟来自不同浏览器的访问。
1. 在 settings.py
中定义 USER_AGENTS_LIST
# settings.py
USER_AGENTS_LIST = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
# ...可以添加更多...
]
2
3
4
5
6
7
2. 编写 RandomUserAgentMiddleware
# middlewares.py
import random
from scrapy.utils.project import get_project_settings
class RandomUserAgentMiddleware:
def __init__(self):
settings = get_project_settings()
self.user_agents = settings.get('USER_AGENTS_LIST')
def process_request(self, request, spider):
# 从列表中随机选择一个 User-Agent
user_agent = random.choice(self.user_agents)
# 为请求设置 User-Agent 头
request.headers['User-Agent'] = user_agent
spider.logger.debug(f"Using User-Agent: {user_agent}")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3. 在 settings.py
中启用
# settings.py
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.RandomUserAgentMiddleware': 200,
}
2
3
4
# 3.2 代理 IP 中间件
当爬虫被封禁时,使用代理 IP 是最有效的解决方案。
1. 代理设置 代理可以是一个固定的 IP,也可以是从代理池 API 获取的。这里以后者为例。
# settings.py
# 代理池 API 地址
PROXY_POOL_URL = 'http://api.proxyprovider.com/get_proxy'
2
3
2. 编写 ProxyMiddleware
这个中间件将在 process_request
中为请求添加代理,并在 process_exception
中处理代理失效的情况。
# middlewares.py
import requests
class ProxyMiddleware:
def __init__(self, proxy_pool_url):
self.proxy_pool_url = proxy_pool_url
@classmethod
def from_crawler(cls, crawler):
return cls(
proxy_pool_url=crawler.settings.get('PROXY_POOL_URL')
)
def get_random_proxy(self):
try:
response = requests.get(self.proxy_pool_url)
if response.status_code == 200:
# 假设 API 返回格式为 'http://ip:port'
return response.text.strip()
except requests.RequestException:
return None
def process_request(self, request, spider):
# 如果请求的 meta 中没有设置代理,则为其分配一个
if 'proxy' not in request.meta:
proxy = self.get_random_proxy()
if proxy:
request.meta['proxy'] = proxy
spider.logger.debug(f"Using proxy: {proxy} for {request.url}")
def process_exception(self, request, exception, spider):
# 当请求出现异常时(如连接超时),尝试使用新的代理重试
proxy = request.meta.get('proxy')
spider.logger.warning(f"Request failed with proxy {proxy}: {exception}")
new_proxy = self.get_random_proxy()
if new_proxy:
spider.logger.info(f"Retrying {request.url} with new proxy: {new_proxy}")
# 创建一个新的请求对象,使用新的代理,并标记为不被过滤
new_request = request.copy()
new_request.meta['proxy'] = new_proxy
new_request.dont_filter = True # 必须设置,否则会被 Scrapy 的去重机制过滤掉
return new_request
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
3. 启用
# settings.py
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyMiddleware': 300,
}
2
3
4
# 3.3 Selenium 与 Scrapy 结合中间件 (处理动态页面)
这个案例将展示如何将 Selenium 的浏览器渲染能力无缝集成到 Scrapy 中。爬虫本身无需关心 Selenium,只需正常发起请求,中间件会自动判断是否需要动用浏览器。
1. 爬虫代码 (wy.py
)
爬虫代码非常简洁,它只需要在需要浏览器渲染的请求的 meta
中做一个标记。
# spiders/wy.py
import scrapy
class WySpider(scrapy.Spider):
name = 'wy'
start_urls = ['https://news.163.com/']
def parse(self, response):
# 提取国内、国际等板块的链接
menu_links = response.xpath('//div[@class="ns_area list"]/ul/li/a/@href').extract()
for url in menu_links:
# 在 meta 中标记这个请求需要 Selenium 处理
if 'domestic' in url or 'world' in url:
yield scrapy.Request(url, callback=self.parse_page, meta={'use_selenium': True})
def parse_page(self, response):
# 这里的 response 已经是经过 Selenium 渲染后的页面
page_detail_urls = response.xpath('//div[@class="ndi_main"]/div/a/@href').extract()
for url in page_detail_urls:
yield scrapy.Request(url, callback=self.parse_page_detail)
def parse_page_detail(self, response):
title = response.xpath('//h1/text()').get()
content = ''.join(response.xpath('//div[@class="post_body"]//p/text()').extract())
yield {'title': title, 'content': content}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2. 编写 SeleniumMiddleware
# middlewares.py
import time
from scrapy.http import HtmlResponse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
class SeleniumMiddleware:
def __init__(self):
chrome_options = Options()
# chrome_options.add_argument("--headless") # 无头模式
self.driver = webdriver.Chrome(options=chrome_options)
def process_request(self, request, spider):
# 检查请求的 meta 是否有 'use_selenium' 标记
if request.meta.get('use_selenium'):
spider.logger.info(f"Using Selenium for: {request.url}")
self.driver.get(request.url)
# 等待并执行滚动操作,以加载动态内容
time.sleep(2)
self.driver.execute_script('window.scrollTo(0, document.body.scrollHeight)')
time.sleep(2)
# 获取渲染后的页面源代码
body = self.driver.page_source
# 创建一个新的 HtmlResponse,用它来替换原始的响应
# 这样,爬虫的 parse 方法接收到的就是渲染后的页面
return HtmlResponse(
self.driver.current_url,
body=body,
encoding='utf-8',
request=request
)
# 如果没有标记,则正常处理
return None
def spider_closed(self):
# 在爬虫关闭时,关闭浏览器
self.driver.quit()
@classmethod
def from_crawler(cls, crawler):
# Scrapy 会使用此方法创建中间件实例
middleware = cls()
# 将 spider_closed 方法连接到 Scrapy 的 spider_closed 信号
crawler.signals.connect(middleware.spider_closed, signal=scrapy.signals.spider_closed)
return middleware
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
3. 启用
# settings.py
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.SeleniumMiddleware': 543,
}
2
3
4
通过这种方式,我们将复杂的浏览器操作逻辑完全封装在了中间件中,Spider 代码保持了极高的纯粹性和可读性,实现了完美的解耦。
# 四、总结与最佳实践
- 明确分工:下载器中间件负责处理网络请求层面(如UA、代理、重试、渲染),爬虫中间件和 Pipeline 负责处理数据层面(如数据清洗、验证、存储)。
- 善用
meta
:request.meta
是在爬虫和中间件之间传递信息的强大工具。 - 注意优先级:合理安排中间件的执行顺序至关重要。例如,UA 中间件的优先级应高于代理中间件。
- 异步是性能关键:对于涉及 I/O 的操作(如请求代理API),应考虑其对性能的影响,尽量使用异步方式。
- 解耦是王道:将通用功能(如代理、Selenium)封装在中间件中,可以让你的爬虫代码更专注于核心的解析逻辑,提高代码的可复用性和可维护性。