【爬虫框架-6】中间件的另一种写法实现

这篇开始设计一下中间件。当然,普通的中间件,想必大家都写烦了,每次都要新开一个middleware, 单开一个文件,那么这次,给大家带来一个不一样的思路。

首先给场景加点点难度,就是很多个parse,比如这个spider ,parse 和parse_list 的回调都要用这个中间件,但是,parse_detail 不想用中间件 ,或者说单独有个图片的parse_img 不想走中间件的process_request 以及process_response. 这种可以在中间件里判断 ,但是稍微麻烦一点点。

先水点点字数。


一、什么是中间件

中间件(Middleware)是爬虫框架中的拦截器模式实现,它允许你在请求发送前和响应返回后插入自定义逻辑。

中间件的作用

请求中间件(Request Processor):

  • 修改请求头(User-Agent、Cookie、Authorization等)
  • 添加代理
  • 请求参数预处理
  • 请求日志记录
  • 请求限流控制

响应中间件(Response Processor):

  • 响应数据清洗
  • 错误页面重试
  • 数据格式转换
  • 响应日志记录
  • 响应缓存

中间件的执行流程

plain 复制代码
发起请求
   ↓
请求中间件1 (priority=1) ← 优先级最高,最先执行
   ↓
请求中间件2 (priority=5)
   ↓
请求中间件3 (priority=10)
   ↓
发送HTTP请求
   ↓
获取响应
   ↓
响应中间件1 (priority=1) ← 优先级最高,最先执行
   ↓
响应中间件2 (priority=5)
   ↓
响应中间件3 (priority=10)
   ↓
返回给parse方法处理

关键点

  • 优先级数字越小,越先执行
  • 请求中间件按优先级从小到大依次执行
  • 响应中间件也按优先级从小到大依次执行
  • 中间件可以中断请求(返回None)

二、请求中间件详解

基本语法

python 复制代码
@process_request(priority=1, funcs='all', enabled=True, name='custom_name')
def middleware_name(self, request: Request, context: dict):
    """
    参数说明:
    - self: Spider实例
    - request: Request对象,可以直接修改
    - context: 上下文字典,包含spider信息
    
    返回值:
    - Request对象:继续执行后续中间件
    - None:中断请求,不发送HTTP请求
    """
    # 修改请求
    request.update_headers('User-Agent', 'MySpider/1.0')
    return request

装饰器参数详解

priority(优先级)
  • 类型:整数
  • 默认值:10
  • 说明:数字越小优先级越高
python 复制代码
@process_request(priority=1)  # 最先执行
def add_auth(self, request, context):
    request.update_headers('Authorization', 'Bearer token')
    return request

@process_request(priority=5)  # 第二执行
def add_user_agent(self, request, context):
    request.update_headers('User-Agent', 'Python/3.9')
    return request

@process_request(priority=10)  # 最后执行
def log_request(self, request, context):
    logger.info(f"发送请求: {request.url}")
    return request
funcs(目标函数)

指定中间件作用于哪些解析方法:

python 复制代码
# 1. 作用于所有parse方法
@process_request(funcs='all')
def middleware_for_all(self, request, context):
    return request

# 2. 作用于指定方法(列表)
@process_request(funcs=['parse', 'parse_detail'])
def middleware_for_specific(self, request, context):
    return request

# 3. 模式匹配(通配符)
@process_request(funcs='parse*')
def middleware_for_pattern(self, request, context):
    # 匹配所有以parse开头的方法
    return request

# 4. 包含匹配
@process_request(funcs='detail')
def middleware_for_contains(self, request, context):
    # 匹配所有包含'detail'的方法,如parse_detail、get_detail等
    return request
enabled(启用状态)
python 复制代码
@process_request(enabled=False)  # 禁用此中间件
def disabled_middleware(self, request, context):
    return request
name(中间件名称)
python 复制代码
@process_request(name='custom_auth_middleware')
def add_token(self, request, context):
    return request

Context 上下文字典

python 复制代码
context = {
    'spider_name': 'MySpider',           # 爬虫名称
    'timestamp': 1702198765.123,         # 时间戳
    'callback_name': 'parse_detail',     # 回调函数名
    'processor_type': 'request'          # 处理器类型
}

Request 对象常用方法

python 复制代码
@process_request(priority=1, funcs='all')
def modify_request(self, request: Request, context: dict):
    # 1. 更新单个请求头
    request.update_headers('User-Agent', 'MyBot/1.0')
    
    # 2. 批量更新请求头
    request.update_headers({
        'User-Agent': 'MyBot/1.0',
        'Accept-Language': 'zh-CN,zh;q=0.9',
        'Referer': 'https://example.com'
    })
    
    # 3. 设置代理
    request.proxies = {
        'http': 'http://proxy.example.com:8080',
        'https': 'https://proxy.example.com:8080'
    }
    
    # 4. 修改URL参数
    request.url = request.url + '&extra_param=value'
    
    # 5. 设置超时
    request.timeout = 30
    
    # 6. 修改请求方法
    request.method = 'POST'
    
    # 7. 设置请求体
    request.data = {'key': 'value'}
    
    return request

中断请求

python 复制代码
@process_request(priority=1, funcs='all')
def filter_requests(self, request: Request, context: dict):
    # 如果URL包含特定关键词,则不发送请求
    if 'blocked' in request.url:
        logger.info(f"🛑 拦截请求: {request.url}")
        return None  # 返回None中断请求
    
    return request

三、响应中间件详解

基本语法

这是用的装饰器的 ,可以直接和parse平级的写法。写在spider中。 权重就是之前的 "project.middleware.Middleware":300

funcs 就是这个中间件要对应的作用域函数。如果all 就代表对所有的parse 开头的都生效。

python 复制代码
@process_response(priority=5, funcs=['parse', 'parse_detail'])
def middleware_name(self, response: Response, context: dict):
    """
    参数说明:
    - self: Spider实例
    - response: Response对象,可以直接修改
    - context: 上下文字典,包含spider信息和原始request
    
    返回值:
    - Response对象:继续执行后续中间件
    - None:中断响应处理
    """
    # 修改响应
    response.text = response.text.replace('old', 'new')
    return response

Context 上下文字典

python 复制代码
context = {
    'spider_name': 'MySpider',
    'timestamp': 1702198765.123,
    'callback_name': 'parse_detail',
    'processor_type': 'response',
    'request': <Request对象>  # 👈 可以访问原始请求
}

Response 对象常用属性

python 复制代码
@process_response(priority=5, funcs='all')
def process_response(self, response: Response, context: dict):
    # 1. 访问响应内容
    html = response.text           # HTML文本
    binary = response.content      # 二进制内容
    json_data = response.json()    # JSON数据
    
    # 2. 访问状态码
    status = response.status_code
    
    # 3. 访问响应头
    headers = response.headers
    
    # 4. 访问原始请求
    original_request = response.request
    request_url = response.request.url
    
    # 5. 直接修改响应文本
    response.text = '<html>新内容</html>'
    
    return response

实战示例

示例1:错误页面重试
python 复制代码
@process_response(priority=1, funcs='all')
def retry_on_error(self, response: Response, context: dict):
    """检测错误页面,触发重试"""
    
    # 检查是否是错误页面
    if '404 Not Found' in response.text or '服务器错误' in response.text:
        logger.warning(f"检测到错误页面: {response.url}")
        
        # 获取原始请求
        original_request = response.request
        
        # 增加重试计数
        retry_count = original_request.meta.get('retry_count', 0)
        
        if retry_count < 3:
            original_request.meta['retry_count'] = retry_count + 1
            logger.info(f"重试第 {retry_count + 1} 次")
            # 可以在这里重新发布请求
            #  直接写for 循环, 
            # 建议直接raise 走funboost 兜底的重试。
            raise Exception(xxxx) 
    
    return response

当然,重试 建议直接raise 走funboost 兜底的重试。

raise Exception(xxxx)

示例2:响应数据清洗

这个仅demo 。写哪都行。

python 复制代码
@process_response(priority=5, funcs=['parse_detail'])
def clean_html(self, response: Response, context: dict):
    """清理HTML中的脚本和样式"""
    import re
    
    text = response.text
    
    # 移除script标签
    text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL)
    
    # 移除style标签
    text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
    
    # 移除注释
    text = re.sub(r'<!--.*?-->', '', text, flags=re.DOTALL)
    
    # 更新响应文本
    response.text = text
    
    return response

四、中间件管理器原理

ProcessorManager 工作流程

python 复制代码
class ProcessorManager:
    """中间件管理器"""
    
    def __init__(self, spider):
        self.spider = spider
        self.request_processors = {}   # 所有请求中间件
        self.response_processors = {}  # 所有响应中间件
        self.request_processor_cache = {}   # 请求中间件缓存
        self.response_processor_cache = {}  # 响应中间件缓存

发现机制

python 复制代码
def discover_processors(self):
    """自动发现Spider类中的所有中间件"""
    
    # 遍历Spider的所有方法
    for name in dir(self.spider):
        method = getattr(self.spider, name)
        
        # 检查是否是请求中间件
        if (callable(method) and 
            hasattr(method, '_is_request_processor') and 
            method._is_request_processor):
            self.request_processors[method._processor_name] = method
            
        # 检查是否是响应中间件
        elif (callable(method) and 
              hasattr(method, '_is_response_processor') and 
              method._is_response_processor):
            self.response_processors[method._processor_name] = method

函数映射缓存

python 复制代码
def _build_function_mapping(self):
    """构建parse方法到中间件的映射关系"""
    
    # 找到所有parse方法
    parse_methods = [
        name for name in dir(self.spider)
        if name.startswith('parse') and callable(getattr(self.spider, name))
    ]
    
    # 为每个parse方法匹配中间件
    for method_name in parse_methods:
        # 匹配请求中间件
        matched_request = self._match_processors(
            method_name, 
            self.request_processors
        )
        self.request_processor_cache[method_name] = matched_request
        
        # 匹配响应中间件
        matched_response = self._match_processors(
            method_name, 
            self.response_processors
        )
        self.response_processor_cache[method_name] = matched_response

匹配规则

python 复制代码
def _function_matches_targets(self, function_name: str, target_funcs: List[str]) -> bool:
    """检查函数是否匹配目标函数列表"""
    
    for target in target_funcs:
        # 1. 'all' 匹配所有
        if target == 'all':
            return True
            
        # 2. 精确匹配
        elif target == function_name:
            return True
            
        # 3. 通配符匹配(如 'parse*')
        elif '*' in target:
            pattern = target.replace('*', '.*')
            if re.match(f'^{pattern}$', function_name):
                return True
                
        # 4. 包含匹配
        elif target in function_name:
            return True
    
    return False

五、完整的Spider示例

python 复制代码
from funspider import BaseSpider
from funspider.network.request import Request
from funspider.network.response import Response
from funspider.decorators import process_request, process_response, queue_params


class MySpider(BaseSpider):
    name = 'my_spider'
    
    # ========== 请求中间件 ==========
    
    @process_request(priority=1, funcs='all')
    def add_common_headers(self, request: Request, context: dict):
        """添加通用请求头(优先级最高)"""
        logger.info(f"添加通用请求头: {request.url}")
        
        request.update_headers({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
            'Accept': 'text/html,application/json',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Accept-Encoding': 'gzip, deflate, br',
        })
        
        return request
    
    @process_request(priority=2, funcs=['parse_detail'])
    def add_auth_for_detail(self, request: Request, context: dict):
        """为详情页添加认证信息"""
        logger.info(f"添加认证信息: {request.url}")
        
        # 从meta中获取token
        token = request.meta.get('auth_token', 'default_token')
        request.update_headers('Authorization', f'Bearer {token}')
        
        return request
    
    @process_request(priority=5, funcs='all')
    def add_timestamp(self, request: Request, context: dict):
        """添加请求时间戳"""
        import time
        request.meta['request_time'] = time.time()
        logger.debug(f"请求时间戳: {request.meta['request_time']}")
        return request
    
    @process_request(priority=10, funcs='all')
    def log_request(self, request: Request, context: dict):
        """记录请求日志(优先级最低,最后执行)"""
        logger.info(f"📤 发送请求: {request.url}")
        logger.debug(f"   Headers: {request.headers}")
        return request
    
    # ========== 响应中间件 ==========
    
    @process_response(priority=1, funcs='all')
    def check_status_code(self, response: Response, context: dict):
        """检查状态码"""
        if response.status_code != 200:
            logger.warning(f"⚠️ 异常状态码: {response.status_code} - {response.url}")
            # 可以选择返回None中断处理
            # return None
        return response
    
    @process_response(priority=5, funcs=['parse', 'parse_detail'])
    def retry_on_error_page(self, response: Response, context: dict):
        """检测错误页面并重试"""
        # 检测常见错误标志
        error_keywords = ['404', '403', '服务器错误', 'Server Error', 'Access Denied']
        
        for keyword in error_keywords:
            if keyword in response.text:
                logger.warning(f"检测到错误页面: {keyword} - {response.url}")
                
                # 获取重试次数
                retry_count = response.request.meta.get('retry_count', 0)
                
                if retry_count < 3:
                    response.request.meta['retry_count'] = retry_count + 1
                    logger.info(f"准备重试第 {retry_count + 1} 次")
                    # 这里可以重新发布请求
                    return None
        
        return response
    
    @process_response(priority=10, funcs='all')
    def calculate_response_time(self, response: Response, context: dict):
        """计算响应时间"""
        import time
        request_time = response.request.meta.get('request_time')
        if request_time:
            response_time = time.time() - request_time
            logger.info(f"⏱️ 响应时间: {response_time:.2f}秒 - {response.url}")
        return response
    
    @process_response(priority=15, funcs='all')
    def clean_html(self, response: Response, context: dict):
        """清理HTML内容"""
        import re
        
        # 移除脚本和样式
        text = response.text
        text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL)
        text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
        
        response.text = text
        return response
    
    # ========== 解析方法 ==========
    
    @queue_params(
        funboost_params={'qps': 10, 'concurrent_num': 5},
        priority_params={'function_timeout': 60}
    )
    def parse(self, request: Request, response: Response):
        """解析列表页"""
        logger.info(f"解析列表页: {response.url}")
        
        # 提取详情页链接
        detail_urls = ['https://example.com/detail/1', 'https://example.com/detail/2']
        
        for url in detail_urls:
            detail_request = Request(
                url=url,
                callback='parse_detail',
                meta={'auth_token': 'user_token_123'}
            )
            yield detail_request
    
    @queue_params(
        funboost_params={'qps': 5, 'concurrent_num': 3},
        priority_params={'function_timeout': 120}
    )
    def parse_detail(self, request: Request, response: Response):
        """解析详情页"""
        logger.info(f"解析详情页: {response.url}")
        
        # 提取数据
        data = {
            'url': response.url,
            'title': 'Example Title',
            'content': response.text[:100]
        }
        
        yield data

更多文章,敬请关注gzh:零基础爬虫第一天

相关推荐
cqsztech17 小时前
基于UOS20 东方通tongweb8 安装简约步骤
中间件
sugar椰子皮18 小时前
【web补环境篇-0】document.all
爬虫
interception19 小时前
js逆向之京东原型链补环境h5st
javascript·爬虫·网络爬虫
yuanmenghao20 小时前
自动驾驶中间件iceoryx - 同步与通知机制(二)
开发语言·单片机·中间件·自动驾驶·信息与通信
半路_出家ren21 小时前
17.python爬虫基础,基于正则表达式的爬虫,基于BeautifulSoup的爬虫
网络·爬虫·python·网络协议·正则表达式·网络爬虫·beautifulsoup
云雾J视界21 小时前
从Boost的设计哲学到工业实践:解锁下一代AI中间件架构的密码
c++·人工智能·中间件·架构·stackoverflow·boost
yuanmenghao1 天前
自动驾驶中间件iceoryx - 同步与通知机制(一)
开发语言·网络·驱动开发·中间件·自动驾驶
我想吃烤肉肉2 天前
Playwright中page.locator和Selenium中find_element区别
爬虫·python·测试工具·自动化
lbb 小魔仙2 天前
【Python】零基础学 Python 爬虫:从原理到反爬,构建企业级爬虫系统
开发语言·爬虫·python
努力变大白2 天前
借助AI零基础快速学会Python爬取网页信息-以天眼查爬虫为例
人工智能·爬虫·python