程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Python 基础

    • Python基础
  • Python 进阶

    • 装饰器与生成器
    • 异常处理
    • 标准库精讲
    • 模块与包
    • pip包管理工具
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
前端 (opens new window)
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Python 基础

    • Python基础
  • Python 进阶

    • 装饰器与生成器
    • 异常处理
    • 标准库精讲
    • 模块与包
    • pip包管理工具
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
前端 (opens new window)
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Python 基础

  • Python 进阶

  • Python爬虫

  • Scrapy 爬虫框架

    • Scrapy 框架核心:深入理解其工作流程
    • Scrapy 入门实战:从零到一构建你的第一个爬虫
    • Scrapy核心对象:Response超详细指南
    • Scrapy 核心配置与调试技巧
    • Scrapy Items与Pipeline数据管道
    • Scrapy 图片与文件下载
    • Scrapy 模拟登录与Cookie处理
    • Scrapy CrawlSpider全站爬取
    • Scrapy 中间件:请求与响应的强大控制器
      • 一、中间件的核心概念
        • 1.1 数据流中的“关卡”
        • 1.2 下载器中间件 vs. 爬虫中间件
      • 二、下载器中间件 (Downloader Middleware)
        • 2.1 核心方法
        • 1. process_request(self, request, spider)
        • 2. process_response(self, request, response, spider)
        • 3. process_exception(self, request, exception, spider)
        • 2.2 启用中间件
      • 三、实战:构建常用的下载器中间件
        • 3.1 随机 User-Agent 中间件
        • 3.2 代理 IP 中间件
        • 3.3 Selenium 与 Scrapy 结合中间件 (处理动态页面)
      • 四、总结与最佳实践
    • Scrapy-Redis:从单机到分布式集群
  • Python
  • Scrapy 爬虫框架
scholar
2025-07-28
目录

Scrapy 中间件:请求与响应的强大控制器

# Scrapy 中间件:请求与响应的强大控制器

在 Scrapy 的世界里,如果说 Spider 负责“发现目标”和“解析数据”,Downloader 负责“获取数据”,那么中间件 (Middlewares) 就扮演着“流程控制器”和“增强器”的关键角色。它像一系列的关卡,所有进出 Spider 的请求 (Request) 和响应 (Response) 都必须经过它,这给了我们一个在爬虫流程中进行全局控制和功能注入的强大机会。

# 一、中间件的核心概念

Scrapy 中间件分为两种:

  1. 下载器中间件 (Downloader Middleware):位于 Scrapy 引擎和下载器之间,主要处理请求的发起和响应的接收。这是最常用、功能最强大的中间件。
  2. 爬虫中间件 (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
1
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,
}
1
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",
    # ...可以添加更多...
]
1
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}")
1
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,
}
1
2
3
4

# 3.2 代理 IP 中间件

当爬虫被封禁时,使用代理 IP 是最有效的解决方案。

1. 代理设置 代理可以是一个固定的 IP,也可以是从代理池 API 获取的。这里以后者为例。

# settings.py
# 代理池 API 地址
PROXY_POOL_URL = 'http://api.proxyprovider.com/get_proxy'
1
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
1
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,
}
1
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}
1
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
1
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,
}
1
2
3
4

通过这种方式,我们将复杂的浏览器操作逻辑完全封装在了中间件中,Spider 代码保持了极高的纯粹性和可读性,实现了完美的解耦。

# 四、总结与最佳实践

  1. 明确分工:下载器中间件负责处理网络请求层面(如UA、代理、重试、渲染),爬虫中间件和 Pipeline 负责处理数据层面(如数据清洗、验证、存储)。
  2. 善用 meta:request.meta 是在爬虫和中间件之间传递信息的强大工具。
  3. 注意优先级:合理安排中间件的执行顺序至关重要。例如,UA 中间件的优先级应高于代理中间件。
  4. 异步是性能关键:对于涉及 I/O 的操作(如请求代理API),应考虑其对性能的影响,尽量使用异步方式。
  5. 解耦是王道:将通用功能(如代理、Selenium)封装在中间件中,可以让你的爬虫代码更专注于核心的解析逻辑,提高代码的可复用性和可维护性。
编辑此页 (opens new window)
上次更新: 2025/07/27, 04:30:11
Scrapy CrawlSpider全站爬取
Scrapy-Redis:从单机到分布式集群

← Scrapy CrawlSpider全站爬取 Scrapy-Redis:从单机到分布式集群→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式