【爬虫框架-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 ,动态生成队列 ,以及使用类方法。但是动态生成队列 +使用类方法 就有点麻烦了。

这个我们回头再解决~

相关推荐
空中海1 小时前
1.Flutter 简介与架构原理
flutter·架构
CClaris1 小时前
PyTorch 损失函数与激活函数的正确组合
人工智能·pytorch·python·深度学习·机器学习
AAA简单玩转程序设计1 小时前
Python避坑指南:基础玩家的3个"开挂"技巧
python
斯普信专业组1 小时前
Calico网络架构与实现深度解析(上)
网络·架构
Brduino脑机接口技术答疑1 小时前
脑机接口数据处理连载(六) 脑机接口频域特征提取实战:傅里叶变换与功率谱分析
人工智能·python·算法·机器学习·数据分析·脑机接口
轻竹办公PPT1 小时前
写开题报告花完精力了,PPT 没法做了。
python·powerpoint
dagouaofei1 小时前
AI 生成开题报告 PPT 会自动提炼重点吗?
人工智能·python·powerpoint
AAA简单玩转程序设计1 小时前
Python基础:被低估的"偷懒"技巧,新手必学!
python