bricks
开发指南
2.6 爬虫基类
2.6.4 Spider Context 上下文

2.6.4 Spider Context 上下文

Spider Context 是 Bricks 爬虫框架中的核心概念,它贯穿整个爬虫生命周期,承载着爬虫执行过程中的所有状态和数据。

核心概念

1. Context 的作用

Context 对象在爬虫执行过程中扮演着数据载体的角色:

  • 状态管理:保存爬虫当前的执行状态
  • 数据传递:在不同阶段间传递数据
  • 资源访问:提供对爬虫实例、请求、响应等资源的访问
  • 流程控制:控制爬虫的执行流程

2. Context 的生命周期

创建 Context → 设置种子 → 构造请求 → 发送请求 → 接收响应 → 解析数据 → 处理结果 → 完成/失败

Context 属性详解

1. 基础属性

class Context:
    # 爬虫实例
    target: Spider
    
    # 种子数据
    seeds: dict
    
    # 请求对象
    request: Request
    
    # 响应对象
    response: Response
    
    # 解析结果
    items: list
    
    # 异常信息
    exception: Exception
    
    # 自定义数据
    custom_data: dict

2. 状态属性

# 执行状态
context.is_success  # 是否成功
context.is_failure  # 是否失败
context.is_retry    # 是否重试
 
# 重试信息
context.retry_times  # 重试次数
context.max_retry   # 最大重试次数

Context 使用示例

1. 在 Air Spider 中使用

from bricks.spider import air
from bricks import Request
 
class MySpider(air.Spider):
    def make_seeds(self, context, **kwargs):
        """初始化阶段的 context"""
        print(f"Target: {context.target}")  # 爬虫实例
        print(f"Seeds: {context.seeds}")    # 空字典
        
        return [{"page": i} for i in range(1, 4)]
    
    def make_request(self, context) -> Request:
        """请求构造阶段的 context"""
        print(f"Seeds: {context.seeds}")    # 当前种子数据
        print(f"Target: {context.target}")  # 爬虫实例
        
        page = context.seeds["page"]
        return Request(url=f"https://httpbin.org/json?page={page}")
    
    def parse(self, context):
        """解析阶段的 context"""
        print(f"Request: {context.request.url}")      # 请求对象
        print(f"Response: {context.response.status_code}")  # 响应对象
        print(f"Seeds: {context.seeds}")              # 种子数据
        
        return context.response.json()
    
    def item_pipeline(self, context):
        """处理阶段的 context"""
        print(f"Items: {context.items}")     # 解析结果
        print(f"Seeds: {context.seeds}")     # 种子数据
        
        # 标记成功
        context.success()

2. 在 Form/Template Spider 中使用

from bricks.spider.form import Spider, Config, Download, Parse, Pipeline
 
def custom_parser(context):
    """自定义解析函数"""
    response = context.response
    seeds = context.seeds
    
    print(f"解析 URL: {response.url}")
    print(f"种子数据: {seeds}")
    
    return response.json()
 
def custom_pipeline(context):
    """自定义处理函数"""
    items = context.items
    seeds = context.seeds
    
    print(f"处理 {len(items)} 条数据")
    print(f"来源页面: {seeds.get('page', 'unknown')}")
    
    # 保存自定义数据
    context.custom_data = {"processed_count": len(items)}
    
    return items
 
@dataclass
class MyConfig(Config):
    spider = [
        Download(url="https://httpbin.org/json?page={page}"),
        Parse(func=custom_parser),
        Pipeline(func=custom_pipeline, success=True)
    ]

Context 方法详解

1. 状态控制方法

# 标记成功
context.success()
 
# 标记失败
context.failure(exception=None)
 
# 标记重试
context.retry(exception=None)
 
# 检查状态
if context.is_success:
    print("处理成功")
elif context.is_failure:
    print("处理失败")
elif context.is_retry:
    print("需要重试")

2. 数据提交方法

# 提交新种子(仅在 Form Spider 中可用)
context.submit(
    {"url": "https://example.com/page1"},
    {"url": "https://example.com/page2"}
)
 
# 批量提交
new_seeds = [{"url": f"https://example.com/page{i}"} for i in range(1, 6)]
context.submit(*new_seeds)

3. 自定义数据操作

# 设置自定义数据
context.custom_data = {"start_time": time.time()}
 
# 更新自定义数据
context.custom_data.update({"end_time": time.time()})
 
# 获取自定义数据
start_time = context.custom_data.get("start_time")

高级用法

1. Context 传递数据

class DataPassingSpider(air.Spider):
    def make_request(self, context) -> Request:
        # 在种子中添加额外信息
        context.seeds["request_time"] = time.time()
        
        return Request(url=context.seeds["url"])
    
    def parse(self, context):
        # 获取请求时间
        request_time = context.seeds.get("request_time")
        response_time = time.time()
        
        # 计算响应时间
        duration = response_time - request_time
        
        return {
            "url": context.response.url,
            "duration": duration,
            "data": context.response.json()
        }

2. Context 错误处理

class ErrorHandlingSpider(air.Spider):
    def parse(self, context):
        try:
            return context.response.json()
        except Exception as e:
            # 记录错误信息
            context.custom_data["parse_error"] = str(e)
            # 标记失败
            context.failure(e)
            return []
    
    def item_pipeline(self, context):
        # 检查是否有解析错误
        if "parse_error" in context.custom_data:
            print(f"解析错误: {context.custom_data['parse_error']}")
            return
        
        # 正常处理
        print(f"处理数据: {context.items}")
        context.success()

3. Context 条件控制

def conditional_download(context):
    """条件下载函数"""
    seeds = context.seeds
    
    # 根据种子数据决定是否下载
    if seeds.get("skip_download"):
        return None
    
    # 根据时间决定是否下载
    if time.time() - seeds.get("last_update", 0) < 3600:
        return None
    
    return Request(url=seeds["url"])
 
def conditional_parse(context):
    """条件解析函数"""
    response = context.response
    
    # 检查响应状态
    if response.status_code != 200:
        context.failure(Exception(f"HTTP {response.status_code}"))
        return []
    
    # 检查内容类型
    content_type = response.headers.get("content-type", "")
    if "application/json" not in content_type:
        context.failure(Exception("不是JSON响应"))
        return []
    
    return response.json()

4. Context 统计信息

class StatisticsSpider(air.Spider):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.stats = {
            "total_requests": 0,
            "successful_requests": 0,
            "failed_requests": 0,
            "total_items": 0
        }
    
    def make_request(self, context) -> Request:
        self.stats["total_requests"] += 1
        return Request(url=context.seeds["url"])
    
    def parse(self, context):
        if context.response.status_code == 200:
            self.stats["successful_requests"] += 1
            data = context.response.json()
            return data
        else:
            self.stats["failed_requests"] += 1
            context.failure(Exception(f"HTTP {context.response.status_code}"))
            return []
    
    def item_pipeline(self, context):
        items = context.items
        self.stats["total_items"] += len(items)
        
        # 将统计信息保存到 context
        context.custom_data["spider_stats"] = self.stats.copy()
        
        print(f"统计信息: {self.stats}")
        context.success()

Context 最佳实践

1. 数据验证

def validate_context(context):
    """验证 context 数据"""
    # 验证种子数据
    if not context.seeds.get("url"):
        raise ValueError("种子数据缺少 URL")
    
    # 验证响应数据
    if hasattr(context, "response") and context.response.status_code >= 400:
        raise ValueError(f"HTTP 错误: {context.response.status_code}")
    
    # 验证解析结果
    if hasattr(context, "items") and not isinstance(context.items, list):
        raise ValueError("解析结果必须是列表")

2. 资源清理

def cleanup_context(context):
    """清理 context 资源"""
    # 清理大型数据
    if hasattr(context, "response") and len(context.response.content) > 1024*1024:
        context.response._content = b""  # 清理响应内容
    
    # 清理临时文件
    temp_files = context.custom_data.get("temp_files", [])
    for file_path in temp_files:
        if os.path.exists(file_path):
            os.remove(file_path)

3. 调试支持

def debug_context(context):
    """调试 context 信息"""
    print(f"=== Context Debug Info ===")
    print(f"Seeds: {context.seeds}")
    
    if hasattr(context, "request"):
        print(f"Request URL: {context.request.url}")
    
    if hasattr(context, "response"):
        print(f"Response Status: {context.response.status_code}")
        print(f"Response Size: {len(context.response.content)} bytes")
    
    if hasattr(context, "items"):
        print(f"Items Count: {len(context.items)}")
    
    if context.custom_data:
        print(f"Custom Data: {context.custom_data}")
    
    print(f"========================")

Context 是 Bricks 爬虫框架的核心,理解和正确使用 Context 对于开发高效、稳定的爬虫至关重要。