跳转至

中间件系统 (Middleware System)

Maize 框架提供了一个强大而灵活的中间件系统,允许你在请求/响应处理的各个阶段插入自定义逻辑。

概述

中间件系统包含三种类型的中间件:

  1. 下载器中间件 (Downloader Middleware) - 处理请求和响应
  2. 爬虫中间件 (Spider Middleware) - 处理爬虫的输入输出
  3. 管道中间件 (Pipeline Middleware) - 处理 Item 数据

中间件类型

1. 下载器中间件 (DownloaderMiddleware)

下载器中间件在请求发送到下载器之前和响应返回之后进行处理。

钩子方法

Python
from maize.middlewares import DownloaderMiddleware

class MyDownloaderMiddleware(DownloaderMiddleware):
    async def process_request(self, request, spider):
        """
        在请求发送前处理请求

        返回值:
            - Request: 修改后的请求,继续处理
            - Response: 跳过下载,直接使用该响应
            - None: 丢弃请求,不再处理
        """
        return request

    async def process_response(self, request, response, spider):
        """
        在响应返回后处理响应

        返回值:
            - Response: 修改后的响应,继续处理
            - Request: 重新发送请求(重试)
            - None: 丢弃响应,不再处理
        """
        return response

    async def process_exception(self, request, exception, spider):
        """
        处理下载过程中的异常

        返回值:
            - Request: 重新发送请求
            - Response: 使用该响应替代
            - None: 忽略异常,继续
        """
        return None

执行顺序

  • process_request: 按优先级**升序**执行(数字越小越先执行)
  • process_response: 按优先级**降序**执行(数字越大越先执行)
  • process_exception: 按优先级**降序**执行

2. 爬虫中间件 (SpiderMiddleware)

爬虫中间件处理进入和离开爬虫的数据流。

钩子方法

Python
from maize.middlewares import SpiderMiddleware

class MySpiderMiddleware(SpiderMiddleware):
    async def process_spider_input(self, response, spider):
        """
        在响应传递给爬虫回调之前处理

        抛出异常会触发 process_spider_exception
        """
        pass

    async def process_spider_output(self, response, result, spider):
        """
        处理爬虫回调返回的结果

        Args:
            result: AsyncGenerator[Request | Item]

        Yields:
            Request 或 Item 对象
        """
        async for item in result:
            yield item

    async def process_spider_exception(self, response, exception, spider):
        """
        处理爬虫回调中的异常

        返回值:
            - AsyncGenerator: 返回新的结果
            - None: 继续传播异常
        """
        return None

    async def process_start_requests(self, start_requests, spider):
        """
        处理 start_requests 生成器

        Yields:
            Request 对象
        """
        async for request in start_requests:
            yield request

执行顺序

  • process_start_requests: 按优先级**降序**执行
  • process_spider_input: 按优先级**升序**执行
  • process_spider_output: 按优先级**降序**执行
  • process_spider_exception: 按优先级**降序**执行

3. 管道中间件 (PipelineMiddleware)

管道中间件在 Item 进入和离开 Pipeline 时进行处理。

钩子方法

Python
from maize.middlewares import PipelineMiddleware

class MyPipelineMiddleware(PipelineMiddleware):
    async def process_item_before(self, item, spider):
        """
        在 Item 进入 Pipeline 前处理

        返回值:
            - Item: 修改后的 Item,继续处理
            - None: 丢弃 Item,不再处理
        """
        return item

    async def process_item_after(self, item, spider):
        """
        在 Item 离开 Pipeline 后处理

        返回值:
            - Item: 修改后的 Item
            - None: 丢弃 Item
        """
        return item

执行顺序

  • process_item_before: 按优先级**升序**执行
  • process_item_after: 按优先级**降序**执行

配置中间件

方法 1: 在 Spider 中配置

Python
from maize import Spider


class MySpider(Spider):
    custom_settings = {
        'middleware': {
            'downloader_middlewares': {
                'myproject.middleware.CustomMiddleware': 100,
                'maize.middleware.downloader.RetryMiddleware': 200,
            },
            'spider_middlewares': {
                'myproject.middleware.DepthMiddleware': 100,
            },
            'pipeline_middlewares': {
                'myproject.middleware.ValidationMiddleware': 100,
            },
        }
    }

方法 2: 在配置文件中配置

YAML 配置 (settings.yaml)

YAML
middleware:
  downloader_middlewares:
    myproject.middlewares.CustomMiddleware: 100
    maize.middlewares.downloader.RetryMiddleware: 200

  spider_middlewares:
    myproject.middlewares.DepthMiddleware: 100

  pipeline_middlewares:
    myproject.middlewares.ValidationMiddleware: 100

TOML 配置 (settings.toml)

TOML
[middleware.downloader_middlewares]
"myproject.middlewares.CustomMiddleware" = 100
"maize.middlewares.downloader.RetryMiddleware" = 200

[middleware.spider_middlewares]
"myproject.middlewares.DepthMiddleware" = 100

[middleware.pipeline_middlewares]
"myproject.middlewares.ValidationMiddleware" = 100

内置中间件

下载器中间件

1. UserAgentMiddleware

轮换 User-Agent 请求头。

Python
custom_settings = {
    'middleware': {
        'downloader_middlewares': {
            'maize.middleware.downloader.UserAgentMiddleware': 100,
        }
    },
    'user_agent_list': [
        'Mozilla/5.0 ...',
        'Mozilla/5.0 ...',
    ],
    'user_agent_mode': 'random',  # 或 'sequential'
}

2. DefaultHeadersMiddleware

添加默认请求头。

Python
custom_settings = {
    'middleware': {
        'downloader_middlewares': {
            'maize.middleware.downloader.DefaultHeadersMiddleware': 50,
        }
    },
    'default_headers': {
        'Accept': 'text/html,application/xhtml+xml',
        'Accept-Language': 'en-US,en;q=0.9',
    },
}

3. RetryMiddleware

请求重试中间件。

Python
custom_settings = {
    'middleware': {
        'downloader_middlewares': {
            'maize.middleware.downloader.RetryMiddleware': 200,
        }
    },
    'request': {
        'max_retry_count': 3,
    },
    'retry_http_codes': [500, 502, 503, 504, 408, 429],
    'retry_delay': 1,
    'exponential_backoff': True,
}

爬虫中间件

1. DepthMiddleware

限制爬取深度。

Python
custom_settings = {
    'middleware': {
        'spider_middlewares': {
            'maize.middleware.spider.DepthMiddleware': 100,
        }
    },
    'max_depth': 3,
    'depth_priority_enabled': False,
}

2. HttpErrorMiddleware

过滤 HTTP 错误响应。

Python
custom_settings = {
    'middleware': {
        'spider_middlewares': {
            'maize.middleware.spider.HttpErrorMiddleware': 50,
        }
    },
    'http_error_allowed_codes': [200, 201, 202],
    'http_error_log_level': 'warning',
}

管道中间件

1. ItemValidationMiddleware

验证 Item 数据。

Python
custom_settings = {
    'middleware': {
        'pipeline_middlewares': {
            'maize.middleware.pipeline.ItemValidationMiddleware': 100,
        }
    },
    'required_fields': ['title', 'url', 'content'],
    'drop_invalid_items': True,
}

2. ItemCleanerMiddleware

清理 Item 数据。

Python
custom_settings = {
    'middleware': {
        'pipeline_middlewares': {
            'maize.middleware.pipeline.ItemCleanerMiddleware': 50,
        }
    },
    'strip_whitespace': True,
    'remove_html': False,
    'normalize_whitespace': True,
    'empty_to_none': False,
    'excluded_fields': ['raw_html'],
}

速查表

内置中间件一览

类型 中间件 功能 配置项
下载器 UserAgentMiddleware 轮换 UA user_agent_list, user_agent_mode
下载器 DefaultHeadersMiddleware 默认请求头 default_headers
下载器 RetryMiddleware 请求重试 max_retry_count, retry_http_codes
爬虫 DepthMiddleware 深度限制 max_depth
爬虫 HttpErrorMiddleware HTTP 错误过滤 http_error_allowed_codes
管道 ItemValidationMiddleware 数据验证 required_fields
管道 ItemCleanerMiddleware 数据清洗 strip_whitespace, remove_html

方法返回值速查

DownloaderMiddleware

方法 返回值 效果
process_request Request 继续处理
Response 跳过下载
None 丢弃请求
process_response Response 继续处理
Request 重试请求
None 丢弃响应
process_exception Request 重试请求
Response 使用响应
None 忽略异常

SpiderMiddleware

方法 返回值 效果
process_spider_input None 正常
抛出异常 触发 exception 处理
process_spider_output AsyncGenerator 返回结果
process_spider_exception AsyncGenerator 处理异常
None 继续传播

PipelineMiddleware

方法 返回值 效果
process_item_before Item 继续处理
None 丢弃 Item
process_item_after Item 继续处理
None 丢弃 Item

常用代码片段

添加自定义请求头:

Python
async def process_request(self, request, spider):
    request.headers = request.headers or {}
    request.headers['X-Custom'] = 'value'
    return request

基于状态码重试:

Python
async def process_response(self, request, response, spider):
    if response.status == 429:
        return request  # 重试
    return response

过滤 URL:

Python
async def process_spider_output(self, response, result, spider):
    async for item in result:
        if isinstance(item, Request):
            if 'allowed.com' not in item.url:
                continue
        yield item

验证必填字段:

Python
async def process_item_before(self, item, spider):
    if not hasattr(item, 'title') or not item.title:
        return None  # 丢弃
    return item

清洗数据:

Python
async def process_item_before(self, item, spider):
    if hasattr(item, 'title'):
        item.title = item.title.strip()
    return item

创建自定义中间件

示例:自定义代理中间件

Python
from maize.middlewares import DownloaderMiddleware
import random

class ProxyMiddleware(DownloaderMiddleware):
    """自定义代理中间件"""

    def __init__(self, settings=None, proxy_list=None):
        super().__init__(settings)
        self.proxy_list = proxy_list or []

    @classmethod
    def from_crawler(cls, crawler):
        """从 crawler 创建中间件实例"""
        proxy_list = getattr(crawler.settings, 'proxy_list', [])
        return cls(crawler.settings, proxy_list=proxy_list)

    async def open(self):
        """中间件启动时调用"""
        self.logger.info(f"Loaded {len(self.proxy_list)} proxies")

    async def process_request(self, request, spider):
        """为请求设置代理"""
        if self.proxy_list:
            proxy = random.choice(self.proxy_list)
            request.proxy = proxy
            self.logger.debug(f"Using proxy: {proxy}")
        return request

    async def process_exception(self, request, exception, spider):
        """处理代理错误"""
        self.logger.warning(f"Proxy error for {request.url}: {exception}")
        # 重试请求
        return request

    async def close(self):
        """中间件关闭时调用"""
        self.logger.info("Proxy middleware closed")

使用自定义中间件

Python
class MySpider(Spider):
    custom_settings = {
        'middleware': {
            'downloader_middlewares': {
                'myproject.middleware.ProxyMiddleware': 100,
            }
        },
        'proxy_list': [
            'http://proxy1.com:8080',
            'http://proxy2.com:8080',
        ],
    }

中间件优先级

优先级是一个整数,决定中间件的执行顺序。建议的优先级范围:

  • 0-99: 系统保留(核心中间件)
  • 100-299: 内置中间件
  • 300-599: 第三方中间件
  • 600-999: 用户自定义中间件

最佳实践

1. 使用 from_crawler 初始化

Python
@classmethod
def from_crawler(cls, crawler):
    """推荐:使用 from_crawler 访问设置和 crawler"""
    setting_value = getattr(crawler.settings, 'my_setting', default_value)
    return cls(crawler.settings, my_param=setting_value)

2. 正确处理返回值

Python
async def process_request(self, request, spider):
    # 返回 Request 继续处理
    return request

    # 返回 Response 跳过下载
    # return Response(...)

    # 返回 None 丢弃请求
    # return None

3. 使用 logger

Python
async def process_request(self, request, spider):
    self.logger.debug(f"Processing request: {request.url}")
    self.logger.info(f"Added header to request")
    self.logger.warning(f"Potential issue detected")
    self.logger.error(f"Error processing request")
    return request

4. 实现生命周期方法

Python
async def open(self):
    """在爬虫启动时初始化资源"""
    self.db = await connect_to_database()

async def close(self):
    """在爬虫关闭时清理资源"""
    await self.db.close()

5. 处理异常

Python
async def process_request(self, request, spider):
    try:
        # 处理逻辑
        return request
    except Exception as e:
        self.logger.error(f"Error in middleware: {e}")
        # 决定是否继续处理
        return request  # 或 return None

常见场景

场景 1: 添加自定义请求头

Python
class CustomHeaderMiddleware(DownloaderMiddleware):
    async def process_request(self, request, spider):
        request.headers = request.headers or {}
        request.headers['X-Custom-Header'] = 'value'
        return request

场景 2: 基于响应状态码重试

Python
class StatusRetryMiddleware(DownloaderMiddleware):
    async def process_response(self, request, response, spider):
        if response.status == 429:  # Too Many Requests
            self.logger.warning("Rate limited, retrying...")
            await asyncio.sleep(5)
            return request  # 重试
        return response

场景 3: 过滤特定 URL

Python
class UrlFilterMiddleware(SpiderMiddleware):
    async def process_spider_output(self, response, result, spider):
        async for item in result:
            if isinstance(item, Request):
                if 'example.com' not in item.url:
                    continue  # 跳过不匹配的 URL
            yield item

场景 4: 数据清洗

Python
class DataCleanerMiddleware(PipelineMiddleware):
    async def process_item_before(self, item, spider):
        # 清理数据
        if hasattr(item, 'title'):
            item.title = item.title.strip()
        return item

调试技巧

1. 启用详细日志

Python
custom_settings = {
    'log_level': 'DEBUG',
}

2. 添加调试信息

Python
async def process_request(self, request, spider):
    self.logger.debug(f"Request URL: {request.url}")
    self.logger.debug(f"Request headers: {request.headers}")
    self.logger.debug(f"Request meta: {request.meta}")
    return request

3. 统计信息

Python
class StatsMiddleware(DownloaderMiddleware):
    def __init__(self, settings=None):
        super().__init__(settings)
        self.request_count = 0
        self.response_count = 0

    async def process_request(self, request, spider):
        self.request_count += 1
        return request

    async def process_response(self, request, response, spider):
        self.response_count += 1
        return response

    async def close(self):
        self.logger.info(f"Total requests: {self.request_count}")
        self.logger.info(f"Total responses: {self.response_count}")

参考资料