这篇开始设计一下中间件。当然,普通的中间件,想必大家都写烦了,每次都要新开一个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:零基础爬虫第一天