【爬虫框架-2】funspider架构

上次说到:基本流程已经通了。那么这次就开始写代码。当然,由于借鉴了funboost 和feapder的架构,就叫funspider吧。

一、基础数据类

均为伪代码
python 复制代码
# utils/request.py
class Request:
    """
    爬虫请求对象,类似 Scrapy 的 Request
    """
    def __init__(self, url, callback, downloader=None, meta=None):
        self.url = url
        self.callback_name = callback.__name__
        self.downloader_name = downloader.__name__ if downloader else None
        self.meta = meta or {}
    
    def to_dict(self):
        return {
            'url': self.url,
            'callback_name': self.callback_name,
            'downloader_name': self.downloader_name,
            'meta': self.meta
        }


# utils/response.py
class Response:
    """
    统一的响应对象,兼容 requests/httpx/playwright
    """
    def __init__(self, url, text, encoding='utf-8'):
        self.url = url
        self.text = text
        self.encoding = encoding
        self.meta = {}
    
    @classmethod
    def from_text(cls, url, text, encoding='utf-8'):
        return cls(url, text, encoding)


# utils/item.py
class Item:
    """
    数据对象,类似 Scrapy 的 Item
    """
    def __init__(self, **kwargs):
        self._data = kwargs
    
    @classmethod
    def update(cls, **kwargs):
        return cls(**kwargs)
    
    @property
    def to_dict(self):
        return self._data
核心爬虫基类
python 复制代码
# core/spider.py
from funboost import BoostersManager, BoosterParams, BrokerEnum
import requests
from parsel import Selector
import copy
from funboost.utils.class_utils import ClsHelper
from argparse import ArgumentParser
from typing import Generator

import sys
sys.path.append('../')

from utils.item import Item
from utils.request import Request
from utils.response import Response


class CoreSpider:
    """
    核心爬虫基类
    
    功能:
    1. 自动管理两个队列:crawl_queue(爬取)和 save_queue(保存)
    2. 支持自定义下载器(requests/httpx/playwright/自定义)
    3. 自动处理 Request 和 Item 的流转
    4. 提供命令行工具(--publish / --consume)
    """
    
    def __init__(self, site_name):
        # Funboost 需要的初始化参数(用于类的序列化传递)
        self.obj_init_params = ClsHelper.get_obj_init_params_for_funboost(copy.copy(locals()))
        
        self.site_name = site_name
        self.crawl_queue_name = f"fun_spider_{self.site_name}_crawl"
        self.save_queue_name = f"fun_spider_{self.site_name}_save"
    
    def download(self, url: str, meta: dict) -> Response:
        """
        默认下载器:使用 requests
        
        可以在子类中重写此方法,实现自定义下载逻辑:
        - 添加代理
        - 设置 Cookie
        - 处理验证码
        """
        print(f"  [默认下载器] GET {url}")
        response = requests.get(url, timeout=15)
        response.raise_for_status()
        return Response.from_text(text=response.text, url=url, encoding=response.encoding)
    
    def _internal_crawl_executor(self, request: Request):
        """
        [内部核心函数] Funboost 消费的函数
        
        工作流程:
        1. 接收一个 Request 对象
        2. 动态选择下载器(自定义或默认)
        3. 调用用户定义的回调函数(如 parse_list, parse_detail)
        4. 处理回调返回的 Request(继续爬)或 Item(保存)
        """
        request_dict = request.to_dict()
        url = request_dict['url']
        callback_name = request_dict['callback_name']
        downloader_name = request_dict['downloader_name']
        meta = request_dict['meta']
        
        # 获取回调函数
        callback_method = getattr(self, callback_name)
        print(f"[爬取] {url} (回调: {callback_name})")
        
        try:
            # 动态选择下载器
            if downloader_name:
                downloader_method = getattr(self, downloader_name)
            else:
                downloader_method = self.download
            
            response = downloader_method(url=url, meta=meta)
            response.meta = meta
        
        except requests.RequestException as e:
            print(f"[下载失败] {url} - {e}")
            return
        except Exception as e:
            print(f"[下载器错误] 在 '{downloader_name or 'default'}' 中: {e}")
            return
        
        # 调用回调函数(如 parse_list, parse_detail)
        results = callback_method(response)
        if not results:
            return
        
        # 处理返回值(Request 或 Item)
        for result in results:
            if isinstance(result, Request):
                # 发布到爬取队列(递归爬取)
                self._queue_push(
                    queue_name=self.crawl_queue_name,
                    consuming_function=self._internal_crawl_executor,
                    push_kwargs=(result,)
                )
            elif isinstance(result, Item):
                # 发布到保存队列
                self._queue_push(
                    queue_name=self.save_queue_name,
                    consuming_function=self.save_item,
                    push_kwargs=(result,)
                )
    
    def _queue_push(self, queue_name, consuming_function, push_kwargs):
        """
        统一的任务发布接口
        
        作用:动态创建 booster 并发布任务
        """
        booster = BoostersManager.build_booster(BoosterParams(
            queue_name=queue_name,
            consuming_function=consuming_function,
            broker_kind=BrokerEnum.REDIS
        ))
        print(f"[发布任务] 队列: {queue_name}")
        booster.push(*push_kwargs)
    
    def start_requests(self) -> Generator[Request, None, None]:
        """
        [必须实现] 子类必须重写此方法,定义初始 URL
        
        示例:
        def start_requests(self):
            yield Request(url='https://example.com', callback=self.parse)
        """
        raise NotImplementedError(f"请在 '{self.__class__.__name__}' 中实现 'start_requests'")
    
    def save_item(self, item: Item):
        """
        [可选重写] 默认保存逻辑
        
        子类可以重写此方法,实现自定义保存逻辑:
        - 保存到 MySQL
        - 保存到 MongoDB
        - 保存到 CSV/JSON 文件
        """
        print(f"[入库] {item.to_dict}")
    
    def _publish(self):
        """
        发布初始任务
        
        从 start_requests() 中获取初始 URL,发布到 crawl_queue
        """
        print("[发布任务] 正在发布初始URL...")
        for request in self.start_requests():
            booster = BoostersManager.build_booster(BoosterParams(
                queue_name=self.crawl_queue_name,
                consuming_function=self._internal_crawl_executor,
                broker_kind=BrokerEnum.REDIS
            ))
            booster.push(request)
        print("[完成] 初始URL发布完成")
    
    def start_consume_crawl(self):
        """
        启动消费者
        
        同时启动两个消费者:
        1. crawl_queue:爬取网页
        2. save_queue:保存数据
        """
        print(f"[启动] 开始消费爬取队列: '{self.crawl_queue_name}'")
        BoostersManager.build_booster(BoosterParams(
            queue_name=self.crawl_queue_name,
            consuming_function=self._internal_crawl_executor,
            broker_kind=BrokerEnum.REDIS,
            max_workers=5  # 5个并发消费者
        )).consume()
        
        print(f"[启动] 开始消费保存队列: '{self.save_queue_name}'")
        BoostersManager.build_booster(BoosterParams(
            queue_name=self.save_queue_name,
            consuming_function=self.save_item,
            broker_kind=BrokerEnum.REDIS,
            max_workers=2
        )).consume()
    
    def start(self):
        """
        命令行入口
        
        用法:
        - python spider.py --publish   # 发布初始任务
        - python spider.py --consume   # 启动消费者
        """
        parser = ArgumentParser(description=f"{self.site_name} 爬虫")
        parser.add_argument(
            '--publish', 
            action="store_true", 
            help=f"发布 '{self.site_name}' 的初始任务"
        )
        parser.add_argument(
            '--consume', 
            action="store_true", 
            help=f"启动消费者"
        )
        
        args = parser.parse_args()
        
        if args.publish:
            self._publish()
        elif args.consume:
            self.start_consume_crawl()
        else:
            parser.print_help()

运行流程:

bash 复制代码
# 终端1: 发布初始任务
$ python baidu_spider.py --publish
[发布任务] 正在发布初始URL...
[发布任务] 队列: fun_spider_baidu_crawl
[完成] 初始URL发布完成

# 终端2: 启动消费者
$ python baidu_spider.py --consume
[启动] 开始消费爬取队列: 'fun_spider_baidu_crawl'
[爬取] https://www.baidu.com (回调: parse_final_page)
  [自定义下载器] GET https://www.baidu.com
    - [解析] 正在解析: https://www.baidu.com
[发布任务] 队列: fun_spider_baidu_save
[保存到MySQL] 标题: 百度一下,你就知道, URL: https://www.baidu.com

二、核心功能讲解

2.1 最重要的 就是自动创建队列

根据解析名创建队列。

python 复制代码
    def _publish(self):
        """
        发布初始任务
        
        从 start_requests() 中获取初始 URL,发布到 crawl_queue
        """
        print("[发布任务] 正在发布初始URL...")
        for request in self.start_requests():
            booster = BoostersManager.build_booster(BoosterParams(
                queue_name=self.crawl_queue_name,
                consuming_function=self._internal_crawl_executor,
                broker_kind=BrokerEnum.REDIS
            ))
            booster.push(request)
        print("[完成] 初始URL发布完成")
 ## 根据

2.2 动态下载器选择

python 复制代码
# 根据 Request 对象的 downloader_name 动态选择
if downloader_name:
    downloader_method = getattr(self, downloader_name)
else:
    downloader_method = self.download  # 默认 requests

实际应用场景:

  • 普通静态页面 → requests
  • 需要 JS 渲染 → playwright
  • 需要 HTTP/2 → httpx
  • 需要代理池 → 自定义 download_with_proxy

示例:使用 Playwright 下载器

python 复制代码
def download_with_playwright(self, url: str, meta: dict) -> Response:
    """
    使用 Playwright 处理 JavaScript 渲染
    """
    from playwright.sync_api import sync_playwright
    
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(url)
        page.wait_for_selector('.content')  # 等待内容加载
        html = page.content()
        browser.close()
    
    return Response.from_text(url=url, text=html)

def start_requests(self):
    # 指定使用 Playwright 下载器
    yield Request(
        url='https://spa-website.com',
        callback=self.parse,
        downloader=self.download_with_playwright
    )

2.3 Request 和 Item 的自动流转

python 复制代码
# 在解析函数中 yield Request → 自动发到 crawl_queue
# 在解析函数中 yield Item → 自动发到 save_queue

for result in callback_method(response):
    if isinstance(result, Request):
        # 递归爬取
        self._queue_push(self.crawl_queue_name, ...)
        # 再次发布。
    elif isinstance(result, Item):
        # 保存数据
        # self._queue_push(self.save_queue_name, ...)
        # 也可以直接入库

如果能看到这里,说明这个流程基本已经了解了。其实就是一个同步的,先publish任务扔进队列,然后启动消费,采集+ 解析+入库 。当然,任意一个部分出错+重试,都有funboost 兜底来重试,这个框架简直是绝了。爬虫有相当多的参数功能就可以直接用funboost。


三、存在的问题:类传递 Bug

3.1 问题现象

在上述代码中,有一个严重的 Bug

python 复制代码
        print("[发布任务] 正在发布初始URL...")
        for request in self.start_requests():
            booster = BoostersManager.build_booster(BoosterParams(
                queue_name=self.crawl_queue_name,
                consuming_function=self._internal_crawl_executor,
                broker_kind=BrokerEnum.REDIS
            ))
            booster.push(request)
            

就是这里动态创建队列的时候,官方文档给了demo ,动态生成队列 ,以及使用类方法。但是动态生成队列 +使用类方法 就有点麻烦了。

这个我们回头再解决~

相关推荐
ZGIAI10 小时前
ZGI 记忆隔离:多人共用不串号
人工智能·架构
星栈独行11 小时前
决定 Agent 交付下限的「操作系统」:Harness 六层架构拆解
人工智能·架构
Cicada12811 小时前
配置文件格式选择指南:JSON、YAML、TOML、INI、ENV、XML
架构
临沂GEO12 小时前
GEO搜索优化科普|正规地理位置流量运营入门指南
大数据·人工智能·python·流量运营
大模型码小白12 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring
美狐美颜SDK开放平台13 小时前
视频美颜SDK是什么?直播APP开发中美颜功能实现方式介绍
深度学习·架构·实时互动·音视频·视频美颜sdk
深蓝电商API13 小时前
常见反爬策略全解析
爬虫·反爬策略
荷蒲14 小时前
【小白量化Qbuddy】利用AI学习中文Python
人工智能·python·学习
画中有画14 小时前
论多源异构数据集成与数据架构设计
架构
数据狐(Datafox)15 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json