首页
/ MediaCrawler 项目架构深度解析:爬虫基类体系、存储工厂与反爬基础设施实现

MediaCrawler 项目架构深度解析:爬虫基类体系、存储工厂与反爬基础设施实现

2026-09-04 13:21:24作者:蔡丛锟

本文基于仓库内的 项目架构文档 展开,逐层剖析 MediaCrawler 这个多平台自媒体爬虫框架的系统架构:从入口层、爬虫基类体系、平台客户端层到存储工厂与代理/缓存等基础设施,并结合 main.pybase/base_crawler.pyproxy/proxy_ip_pool.py 等源码印证每一层的设计意图与调用关系。读完本篇,你可以掌握该项目的分层设计、三种爬虫模式的运作机制、7 种数据存储方式的工厂化实现,以及如何在此基础上扩展新平台、新存储方式与新代理提供商。

1. 项目概述

1.1 项目简介

MediaCrawler 是一个多平台自媒体爬虫框架,采用 Python 异步编程实现,支持爬取主流社交媒体平台的内容、评论和创作者信息。项目基于 asyncio + Playwright 构建浏览器自动化能力,以 httpx 发起 API 请求,以 SQLAlchemy / Motor 完成多形态数据落盘。

1.2 支持的平台

平台 代号 主要功能
小红书 xhs 笔记搜索、详情、创作者
抖音 dy 视频搜索、详情、创作者
快手 ks 视频搜索、详情、创作者
B站 bili 视频搜索、详情、UP主
微博 wb 微博搜索、详情、博主
百度贴吧 tieba 帖子搜索、详情
知乎 zhihu 问答搜索、详情、答主

这 7 个平台代号与 main.pyCrawlerFactory.CRAWLERS 的注册表一一对应:

class CrawlerFactory:
    CRAWLERS: dict[str, Type[AbstractCrawler]] = {
        "xhs": XiaoHongShuCrawler,
        "dy": DouYinCrawler,
        "ks": KuaishouCrawler,
        "bili": BilibiliCrawler,
        "wb": WeiboCrawler,
        "tieba": TieBaCrawler,
        "zhihu": ZhihuCrawler,
    }

传入未知平台时,工厂会抛出 ValueError 并列出所有受支持的代号(main.py)。

1.3 核心功能特性

  • 多平台支持:统一的爬虫接口,支持 7 大主流平台;
  • 多种登录方式:二维码、手机号、Cookie 三种登录方式(对应 base/base_crawler.pyAbstractLogin 的三个抽象方法);
  • 多种存储方式:CSV、JSON、JSONL、SQLite、MySQL、MongoDB、Excel,另含 PostgreSQL 选项(见 config/base_config.pycmd_arg/arg.pySaveDataOptionEnum);
  • 反爬虫对策:CDP 模式、代理 IP 池、请求签名;
  • 异步高并发:基于 asyncio 的异步架构,高效并发爬取;
  • 词云生成:自动生成评论词云图(由 main.py 在爬取结束后按需触发,依赖 docs/hit_stopwords.txt 停用词表与 docs/STZHONGS.TTF 中文字体)。

2. 系统架构总览

2.1 高层架构图

flowchart TB
    subgraph Entry["入口层"]
        main["main.py<br/>程序入口"]
        cmdarg["cmd_arg<br/>命令行参数"]
        config["config<br/>配置管理"]
    end

    subgraph Core["核心爬虫层"]
        factory["CrawlerFactory<br/>爬虫工厂"]
        base["AbstractCrawler<br/>爬虫基类"]

        subgraph Platforms["平台实现"]
            xhs["XiaoHongShuCrawler"]
            dy["DouYinCrawler"]
            ks["KuaishouCrawler"]
            bili["BilibiliCrawler"]
            wb["WeiboCrawler"]
            tieba["TieBaCrawler"]
            zhihu["ZhihuCrawler"]
        end
    end

    subgraph Client["API客户端层"]
        absClient["AbstractApiClient<br/>客户端基类"]
        xhsClient["XiaoHongShuClient"]
        dyClient["DouYinClient"]
        ksClient["KuaiShouClient"]
        biliClient["BilibiliClient"]
        wbClient["WeiboClient"]
        tiebaClient["BaiduTieBaClient"]
        zhihuClient["ZhiHuClient"]
    end

    subgraph Storage["数据存储层"]
        storeFactory["StoreFactory<br/>存储工厂"]
        csv["CSV存储"]
        json["JSON存储"]
        sqlite["SQLite存储"]
        mysql["MySQL存储"]
        mongodb["MongoDB存储"]
        excel["Excel存储"]
    end

    subgraph Infra["基础设施层"]
        browser["浏览器管理<br/>Playwright/CDP"]
        proxy["代理IP池"]
        cache["缓存系统"]
        login["登录管理"]
    end

    main --> factory
    cmdarg --> main
    config --> main
    factory --> base
    base --> Platforms
    Platforms --> Client
    Client --> Storage
    Client --> Infra
    Storage --> storeFactory
    storeFactory --> csv & json & sqlite & mysql & mongodb & excel

入口层的实际调用链在 main.py 中可以看到:main() 先调用 cmd_arg.parse_cmd() 解析命令行并覆写全局配置,随后由 CrawlerFactory.create_crawler(platform=config.PLATFORM) 构造平台爬虫实例并 await crawler.start()。此外还有两个收尾动作:若 SAVE_DATA_OPTION == "excel" 则调用 ExcelStoreBase.flush_all() 刷盘(main.py);若存储方式为 json/jsonl 且开启了 ENABLE_GET_WORDCLOUD,则生成评论词云。

2.2 数据流向图

flowchart LR
    subgraph Input["输入"]
        keywords["关键词/ID"]
        config["配置参数"]
    end

    subgraph Process["处理流程"]
        browser["启动浏览器"]
        login["登录认证"]
        search["搜索/爬取"]
        parse["数据解析"]
        comment["获取评论"]
    end

    subgraph Output["输出"]
        content["内容数据"]
        comments["评论数据"]
        creator["创作者数据"]
        media["媒体文件"]
    end

    subgraph Storage["存储"]
        file["文件存储<br/>CSV/JSON/Excel"]
        db["数据库<br/>SQLite/MySQL"]
        nosql["NoSQL<br/>MongoDB"]
    end

    keywords --> browser
    config --> browser
    browser --> login
    login --> search
    search --> parse
    parse --> comment
    parse --> content
    comment --> comments
    parse --> creator
    parse --> media
    content & comments & creator --> file & db & nosql
    media --> file

3. 目录结构

MediaCrawler/
├── main.py                 # 程序入口
├── var.py                  # 全局上下文变量
├── pyproject.toml          # 项目配置
│
├── base/                   # 基础抽象类
│   └── base_crawler.py     # 爬虫、登录、存储、客户端基类
│
├── config/                 # 配置管理
│   ├── base_config.py      # 核心配置
│   ├── db_config.py        # 数据库配置
│   └── {platform}_config.py # 平台特定配置
│
├── media_platform/         # 平台爬虫实现
│   ├── xhs/                # 小红书
│   ├── douyin/             # 抖音
│   ├── kuaishou/           # 快手
│   ├── bilibili/           # B站
│   ├── weibo/              # 微博
│   ├── tieba/              # 百度贴吧
│   └── zhihu/              # 知乎
│
├── store/                  # 数据存储
│   ├── excel_store_base.py # Excel存储基类
│   └── {platform}/         # 各平台存储实现
│
├── database/               # 数据库层
│   ├── models.py           # ORM模型定义
│   ├── db_session.py       # 数据库会话管理
│   └── mongodb_store_base.py # MongoDB基类
│
├── proxy/                  # 代理管理
│   ├── proxy_ip_pool.py    # IP池管理
│   ├── proxy_mixin.py      # 代理刷新混入
│   └── providers/          # 代理提供商
│
├── cache/                  # 缓存系统
│   ├── abs_cache.py        # 缓存抽象类
│   ├── local_cache.py      # 本地缓存
│   └── redis_cache.py      # Redis缓存
│
├── tools/                  # 工具模块
│   ├── app_runner.py       # 应用运行管理
│   ├── browser_launcher.py # 浏览器启动
│   ├── cdp_browser.py      # CDP浏览器管理
│   ├── crawler_util.py     # 爬虫工具
│   └── async_file_writer.py # 异步文件写入
│
├── model/                  # 数据模型
│   └── m_{platform}.py     # Pydantic模型
│
├── libs/                   # JS脚本库
│   └── stealth.min.js      # 反检测脚本
│
└── cmd_arg/                # 命令行参数
    └── arg.py              # 参数定义

从源码结构看,除上述架构文档列出的目录外,仓库根目录还包含 api/(FastAPI 路由与服务层,提供 Web 端运行爬虫与数据查询接口)和 webui/(React + Vite 前端)两个目录,它们构成面向 Web 的操作入口,可视为入口层的补充形态;本架构文档的主体聚焦于 main.py 驱动的 CLI 运行路径。

4. 核心模块详解

4.1 爬虫基类体系

classDiagram
    class AbstractCrawler {
        <<abstract>>
        +start()* 启动爬虫
        +search()* 搜索功能
        +launch_browser() 启动浏览器
        +launch_browser_with_cdp() CDP模式启动
    }

    class AbstractLogin {
        <<abstract>>
        +begin()* 开始登录
        +login_by_qrcode()* 二维码登录
        +login_by_mobile()* 手机号登录
        +login_by_cookies()* Cookie登录
    }

    class AbstractStore {
        <<abstract>>
        +store_content()* 存储内容
        +store_comment()* 存储评论
        +store_creator()* 存储创作者
        +store_image()* 存储图片
        +store_video()* 存储视频
    }

    class AbstractApiClient {
        <<abstract>>
        +request()* HTTP请求
        +update_cookies()* 更新Cookies
    }

    class ProxyRefreshMixin {
        +init_proxy_pool() 初始化代理池
        +_refresh_proxy_if_expired() 刷新过期代理
    }

    class XiaoHongShuCrawler {
        +xhs_client: XiaoHongShuClient
        +start()
        +search()
        +get_specified_notes()
        +get_creators_and_notes()
    }

    class XiaoHongShuClient {
        +playwright_page: Page
        +cookie_dict: Dict
        +request()
        +pong() 检查登录状态
        +get_note_by_keyword()
        +get_note_by_id()
    }

    AbstractCrawler <|-- XiaoHongShuCrawler
    AbstractApiClient <|-- XiaoHongShuClient
    ProxyRefreshMixin <|-- XiaoHongShuClient

对照 base/base_crawler.py 的源码,这套基类体系有几个值得注意的实现细节:

  1. AbstractCrawler 定义了 start()search() 两个抽象方法与 launch_browser() 抽象方法;launch_browser_with_cdp()可选实现,默认实现会回退到标准模式:
async def launch_browser_with_cdp(self, playwright, playwright_proxy, user_agent, headless=True):
    # Default implementation: fallback to standard mode
    return await self.launch_browser(playwright.chromium, playwright_proxy, user_agent, headless)

这意味着不支持 CDP 的平台无需额外处理,自动走 Playwright 启动 Chromium 的路径(base/base_crawler.py)。

  1. AbstractStore 只强制要求 store_contentstore_commentstore_creator 三个抽象方法;store_image / store_video 被拆分为独立的 AbstractStoreImage / AbstractStoreVideo,源码注释标明当前仅微博平台实现了媒体文件存储(base/base_crawler.py)。

  2. ProxyRefreshMixin 是一个横切关注点,被各平台 Client 组合继承。其实现约定非常明确(proxy/proxy_mixin.py):

    • 客户端类先调用 init_proxy_pool(proxy_ip_pool) 注入代理池引用;
    • 每次发起 request() 前调用 await _refresh_proxy_if_expired(),若当前代理过期则调用 get_or_refresh_proxy() 取出新代理,并刷新客户端自身的 self.proxy(httpx 代理 URL);
    • 若未注入代理池(即未开启代理),该方法直接返回,对无代理场景零侵入。

4.2 爬虫生命周期

sequenceDiagram
    participant Main as main.py
    participant Factory as CrawlerFactory
    participant Crawler as XiaoHongShuCrawler
    participant Browser as Playwright/CDP
    participant Login as XiaoHongShuLogin
    participant Client as XiaoHongShuClient
    participant Store as StoreFactory

    Main->>Factory: create_crawler("xhs")
    Factory-->>Main: crawler实例

    Main->>Crawler: start()

    alt 启用IP代理
        Crawler->>Crawler: create_ip_pool()
    end

    alt CDP模式
        Crawler->>Browser: launch_browser_with_cdp()
    else 标准模式
        Crawler->>Browser: launch_browser()
    end
    Browser-->>Crawler: browser_context

    Crawler->>Crawler: create_xhs_client()
    Crawler->>Client: pong() 检查登录状态

    alt 未登录
        Crawler->>Login: begin()
        Login->>Login: login_by_qrcode/mobile/cookie
        Login-->>Crawler: 登录成功
    end

    alt search模式
        Crawler->>Client: get_note_by_keyword()
        Client-->>Crawler: 搜索结果
        loop 获取详情
            Crawler->>Client: get_note_by_id()
            Client-->>Crawler: 笔记详情
        end
    else detail模式
        Crawler->>Client: get_note_by_id()
    else creator模式
        Crawler->>Client: get_creator_info()
    end

    Crawler->>Store: store_content/comment/creator
    Store-->>Crawler: 存储完成

    Main->>Crawler: cleanup()
    Crawler->>Browser: close()

生命周期末端的清理逻辑在 main.py 中有具体落地:async_cleanup() 会优先清理 CDP 管理器(cdp_manager.cleanup(force=True)),否则关闭 browser_context;若存储方式为 db/sqlite 还会关闭数据库连接。程序入口通过 run(main, async_cleanup, cleanup_timeout_seconds=15.0, on_first_interrupt=_force_stop) 挂接信号处理——第一次 Ctrl+C 触发清理流程,第二次则强制退出,这与后文第 9.2 节的应用运行管理流程图一致。

4.3 平台爬虫实现结构

每个平台目录包含以下核心文件:

media_platform/{platform}/
├── __init__.py         # 模块导出
├── core.py             # 爬虫主实现类
├── client.py           # API客户端
├── login.py            # 登录实现
├── field.py            # 字段/枚举定义
├── exception.py        # 异常定义
├── help.py             # 辅助函数
└── {特殊实现}.py       # 平台特定逻辑

以小红书为例,media_platform/xhs/ 下除了上述标准文件外还有 xhs_sign.pyplaywright_sign.pyextractor.py 等平台特定逻辑文件(签名算法与数据提取);快手平台则额外携带 graphql/ 目录存放 GraphQL 查询语句,B站的 field.py 与贴吧的测试数据 test_data/ 也体现了各平台差异化实现。这种"标准文件 + 特殊实现"的组织方式正是扩展新平台时的模板。

4.4 三种爬虫模式

模式 配置值 功能描述 适用场景
搜索模式 search 根据关键词搜索内容 批量获取特定主题内容
详情模式 detail 获取指定ID的详情 精确获取已知内容
创作者模式 creator 获取创作者所有内容 追踪特定博主/UP主

detailcreator 模式分别由命令行参数 --specified_id--creator_id 提供 ID 列表(支持完整 URL 或纯 ID,逗号分隔)。cmd_arg/arg.py 会按平台将解析结果写入对应平台配置,例如 config.XHS_SPECIFIED_NOTE_URL_LISTconfig.DY_CREATOR_ID_LIST;贴吧平台还会做归一化处理(_normalize_tieba_note_id/p/<id> 链接中提取数字 ID)。

5. 数据存储层

5.1 存储架构图

classDiagram
    class AbstractStore {
        <<abstract>>
        +store_content()*
        +store_comment()*
        +store_creator()*
    }

    class StoreFactory {
        +STORES: Dict
        +create_store() AbstractStore
    }

    class CsvStoreImplement {
        +async_file_writer: AsyncFileWriter
        +store_content()
        +store_comment()
    }

    class JsonStoreImplement {
        +async_file_writer: AsyncFileWriter
        +store_content()
        +store_comment()
    }

    class DbStoreImplement {
        +session: AsyncSession
        +store_content()
        +store_comment()
    }

    class SqliteStoreImplement {
        +session: AsyncSession
        +store_content()
        +store_comment()
    }

    class MongoStoreImplement {
        +mongo_base: MongoDBStoreBase
        +store_content()
        +store_comment()
    }

    class ExcelStoreImplement {
        +excel_base: ExcelStoreBase
        +store_content()
        +store_comment()
    }

    AbstractStore <|-- CsvStoreImplement
    AbstractStore <|-- JsonStoreImplement
    AbstractStore <|-- DbStoreImplement
    AbstractStore <|-- SqliteStoreImplement
    AbstractStore <|-- MongoStoreImplement
    AbstractStore <|-- ExcelStoreImplement
    StoreFactory --> AbstractStore

5.2 存储工厂模式

# 以抖音为例
class DouyinStoreFactory:
    STORES = {
        "csv": DouyinCsvStoreImplement,
        "db": DouyinDbStoreImplement,
        "json": DouyinJsonStoreImplement,
        "jsonl": DouyinJsonlStoreImplement,
        "sqlite": DouyinSqliteStoreImplement,
        "mongodb": DouyinMongoStoreImplement,
        "excel": DouyinExcelStoreImplement,
    }

    @staticmethod
    def create_store() -> AbstractStore:
        store_class = DouyinStoreFactory.STORES.get(config.SAVE_DATA_OPTION)
        return store_class()

store/douyin/_store_impl.py 可以看到各实现的真实形态:

  • 文件类存储(CSV/JSON/JSONL)统一委托给 AsyncFileWriter,按 contents/comments/creators 三种 item_type 分文件写入;JSONL 采用追加写入,适合大规模增量爬取;
  • 数据库类存储DouyinDbStoreImplementDouyinSqliteStoreImplement 直接继承它)实现了 upsert 去重:先按业务唯一键(如 aweme_idcomment_id)查询,不存在则新建并写入 add_ts 时间戳,存在则逐字段 setattr 更新后 commitstore/douyin/_store_impl.py),这正是数据库存储"带去重功能"的底层依据;
  • MongoDB 存储基于 database/mongodb_store_base.py 封装的 MongoDBStoreBase,以 collection_prefix="douyin" 组织集合,通过 save_or_update 完成去重写入;
  • Excel 存储通过 ExcelStoreBase.get_instance(...) 返回全局单例(利用 __new__ 实现),配合 main.pyflush_all() 的收尾刷盘,保证程序退出前所有行数据完整落盘。

各平台存储实现集中在 store/{platform}/_store_impl.py 中,media_platform/{platform}/core.py 在构造爬虫时经由各平台的 StoreFactoryconfig.SAVE_DATA_OPTION 取得具体实例,存储方式的选择对爬取主流程完全透明。

5.3 存储方式对比

存储方式 配置值 优点 适用场景
CSV csv 简单、通用 小规模数据、快速查看
JSON json 结构完整、易解析 API对接、数据交换
JSONL jsonl 追加写入、性能好 大规模数据、增量爬取(默认)
SQLite sqlite 轻量、无需服务 本地开发、小型项目
MySQL db 性能好、支持并发 生产环境、大规模数据
MongoDB mongodb 灵活、易扩展 非结构化数据、快速迭代
Excel excel 可视化、易分享 报告、数据分析

补充一点:当前仓库的 SaveDataOptionEnumcmd_arg/arg.py)还支持 postgresconfig/db_config.py 中已提供 PostgreSQL 的连接配置;而词云生成仅在 json/jsonl 两种文件存储方式下可用(main.py)。

6. 基础设施层

6.1 代理系统架构

flowchart TB
    subgraph Config["配置"]
        enable["ENABLE_IP_PROXY"]
        provider["IP_PROXY_PROVIDER"]
        count["IP_PROXY_POOL_COUNT"]
    end

    subgraph Pool["代理池管理"]
        pool["ProxyIpPool"]
        load["load_proxies()"]
        validate["_is_valid_proxy()"]
        get["get_proxy()"]
        refresh["get_or_refresh_proxy()"]
    end

    subgraph Providers["代理提供商"]
        kuaidl["快代理<br/>KuaiDaiLiProxy"]
        wandou["万代理<br/>WanDouHttpProxy"]
        jishu["技术IP<br/>JiShuHttpProxy"]
    end

    subgraph Client["API客户端"]
        mixin["ProxyRefreshMixin"]
        request["request()"]
    end

    enable --> pool
    provider --> Providers
    count --> load
    pool --> load
    load --> validate
    validate --> Providers
    pool --> get
    pool --> refresh
    mixin --> refresh
    mixin --> Client
    request --> mixin

proxy/proxy_ip_pool.pyProxyIpPool 的关键行为值得逐条对应:

  • load_proxies():调用提供商的 get_proxy(ip_pool_count) 拉取 IP 列表,池容量由 IP_PROXY_POOL_COUNT 控制;
  • get_proxy():随机抽取并从池中移除该 IP(避免短期内重复使用),抽取带 @retry(stop=stop_after_attempt(3), wait=wait_fixed(1)) 重试;若开启校验,则用 httpx 经由该代理请求 https://echo.apifox.cn/,状态码 200 视为有效;
  • is_current_proxy_expired(buffer_seconds=30):默认提前 30 秒判定过期,短时效代理(如分钟级有效期)不会用到失效 IP;
  • get_or_refresh_proxy():供 ProxyRefreshMixin 每次请求前调用,未过期直接复用当前代理,过期则重新抽取;
  • 静态代理StaticProxyProviderproxy/proxy_ip_pool.py)直接解析 STATIC_PROXY_URL(形如 http://user:password@host:port),并将过期时间设为极大值、跳过有效性校验。

提供商注册表 IpProxyProviderproxy/proxy_ip_pool.py)当前映射了 kuaidailiwandouhttpstatic 三种;proxy/providers/ 目录下另存有 jishu_http_proxy.py 等提供商实现文件,从源码结构看属于可注册的扩展实现。未知提供商名会在 create_ip_pool() 中抛出明确的 ValueError 并列出合法选项。

6.2 登录流程

flowchart TB
    Start([开始登录]) --> CheckType{登录类型?}

    CheckType -->|qrcode| QR[显示二维码]
    QR --> WaitScan[等待扫描]
    WaitScan --> CheckQR{扫描成功?}
    CheckQR -->|是| SaveCookie[保存Cookie]
    CheckQR -->|否| WaitScan

    CheckType -->|phone| Phone[输入手机号]
    Phone --> SendCode[发送验证码]
    SendCode --> Slider{需要滑块?}
    Slider -->|是| DoSlider[滑动验证]
    DoSlider --> InputCode[输入验证码]
    Slider -->|否| InputCode
    InputCode --> Verify[验证登录]
    Verify --> SaveCookie

    CheckType -->|cookie| LoadCookie[加载已保存Cookie]
    LoadCookie --> VerifyCookie{Cookie有效?}
    VerifyCookie -->|是| SaveCookie
    VerifyCookie -->|否| Fail[登录失败]

    SaveCookie --> UpdateContext[更新浏览器上下文]
    UpdateContext --> End([登录完成])

三种登录方式对应 base/base_crawler.pyAbstractLogin 的抽象方法,由 cmd_arg/arg.pyLoginTypeEnum 枚举约束(qrcode | phone | cookie)。手机号登录的滑块验证依赖 tools/slider_util.py 实现;若登录时扫码始终失败,config/base_config.py 的注释建议将 HEADLESS 置为 False 打开浏览器窗口,手动完成滑动验证或手机验证。登录成功后若 SAVE_LOGIN_STATE = True,登录态会持久化到用户数据目录(USER_DATA_DIR,按平台名区分),下次启动可直接复用。

6.3 浏览器管理

flowchart LR
    subgraph Mode["启动模式"]
        standard["标准模式<br/>Playwright"]
        cdp["CDP模式<br/>Chrome DevTools"]
    end

    subgraph Standard["标准模式流程"]
        launch["chromium.launch()"]
        context["new_context()"]
        stealth["注入stealth.js"]
    end

    subgraph CDP["CDP模式流程"]
        detect["检测浏览器路径"]
        start["启动浏览器进程"]
        connect["connect_over_cdp()"]
        cdpContext["获取已有上下文"]
    end

    subgraph Features["特性"]
        f1["用户数据持久化"]
        f2["扩展和设置继承"]
        f3["反检测能力增强"]
    end

    standard --> Standard
    cdp --> CDP
    CDP --> Features

两种模式的配置开关集中在 config/base_config.py

配置项 默认值 说明
ENABLE_CDP_MODE True 是否使用本地 Chrome/Edge,经 CDP 协议控制,反检测能力更强
CDP_DEBUG_PORT 9222 调试端口,被占用时自动尝试下一个可用端口
CUSTOM_BROWSER_PATH "" 自定义浏览器路径,为空则自动检测 Chrome/Edge 安装位置
CDP_HEADLESS False CDP 模式无头开关;部分反检测功能在无头下可能失效
BROWSER_LAUNCH_TIMEOUT 60 浏览器启动超时(秒)
CDP_CONNECT_EXISTING True 连接用户已打开且启用远程调试的浏览器,反检测效果最佳
AUTO_CLOSE_BROWSER True 程序结束是否自动关闭浏览器

标准模式下的反检测脚本来自 libs/stealth.min.js,浏览器路径检测与进程拉起由 tools/browser_launcher.py、CDP 连接管理由 tools/cdp_browser.py 承担。

6.4 缓存系统

classDiagram
    class AbstractCache {
        <<abstract>>
        +get(key)* 获取缓存
        +set(key, value, expire)* 设置缓存
        +keys(pattern)* 获取所有键
    }

    class ExpiringLocalCache {
        -_cache: Dict
        -_expire_times: Dict
        +get(key)
        +set(key, value, expire_time)
        +keys(pattern)
        -_is_expired(key)
    }

    class RedisCache {
        -_client: Redis
        +get(key)
        +set(key, value, expire_time)
        +keys(pattern)
    }

    class CacheFactory {
        +create_cache(type) AbstractCache
    }

    AbstractCache <|-- ExpiringLocalCache
    AbstractCache <|-- RedisCache
    CacheFactory --> AbstractCache

工厂实现见 cache/cache_factory.pycache_type == 'memory' 返回 ExpiringLocalCache(进程内带过期时间的本地缓存),cache_type == 'redis' 返回 RedisCache,其余取值抛出 ValueError。类型常量 CACHE_TYPE_REDIS = "redis"CACHE_TYPE_MEMORY = "memory" 定义在 config/db_config.py,Redis 连接参数(主机、端口、密码、库号)同样支持环境变量覆盖。代理 IP 池等组件即通过该缓存体系共享代理状态。

7. 数据模型

7.1 ORM模型关系

erDiagram
    DouyinAweme {
        int id PK
        string aweme_id UK
        string aweme_type
        string title
        string desc
        int create_time
        int liked_count
        int collected_count
        int comment_count
        int share_count
        string user_id FK
        datetime add_ts
        datetime last_modify_ts
    }

    DouyinAwemeComment {
        int id PK
        string comment_id UK
        string aweme_id FK
        string content
        int create_time
        int sub_comment_count
        string user_id
        datetime add_ts
        datetime last_modify_ts
    }

    DyCreator {
        int id PK
        string user_id UK
        string nickname
        string avatar
        string desc
        int follower_count
        int total_favorited
        datetime add_ts
        datetime last_modify_ts
    }

    DouyinAweme ||--o{ DouyinAwemeComment : "has"
    DyCreator ||--o{ DouyinAweme : "creates"

ORM 模型统一定义在 database/models.py 中,会话管理由 database/db_session.py 提供。以抖音为例,内容表 DouyinAweme 与评论表 DouyinAwemeComment 均以业务 ID(aweme_id/comment_id)建唯一约束,创作者表为 DyCreator——存储层的 upsert 去重正是依赖这些唯一键。

7.2 各平台数据表

平台 内容表 评论表 创作者表
抖音 DouyinAweme DouyinAwemeComment DyCreator
小红书 XHSNote XHSNoteComment XHSCreator
快手 KuaishouVideo KuaishouVideoComment KsCreator
B站 BilibiliVideo BilibiliVideoComment BilibiliUpInfo
微博 WeiboNote WeiboNoteComment WeiboCreator
贴吧 TiebaNote TiebaNoteComment -
知乎 ZhihuContent ZhihuContentComment ZhihuCreator

贴吧没有创作者表(创作者模式走主页 URL 解析),各平台的 Pydantic 响应模型则分别定义在 model/ 下的 m_{platform}.py 中。

8. 配置系统

8.1 核心配置项

以下配置来自 config/base_config.py,括号内为当前仓库的实际默认值:

# config/base_config.py

# 平台选择
PLATFORM = "xhs"  # xhs | dy | ks | bili | wb | tieba | zhihu
XHS_INTERNATIONAL = False  # 是否使用海外版小红书 (rednote.com)

# 登录配置
KEYWORDS = "编程副业,编程兼职"  # 关键词,英文逗号分隔
LOGIN_TYPE = "qrcode"  # qrcode | phone | cookie
COOKIES = ""
SAVE_LOGIN_STATE = True

# 爬虫配置
CRAWLER_TYPE = "search"  # search | detail | creator
CRAWLER_MAX_NOTES_COUNT = 15      # 爬取内容数量上限
MAX_CONCURRENCY_NUM = 1           # 并发爬虫数
START_PAGE = 1                    # 起始页码
CRAWLER_MAX_SLEEP_SEC = 2         # 请求间隔

# 评论配置
ENABLE_GET_COMMENTS = True                 # 是否抓取一级评论
ENABLE_GET_SUB_COMMENTS = False            # 是否抓取二级评论
CRAWLER_MAX_COMMENTS_COUNT_SINGLENOTES = 10  # 单条内容一级评论数上限

# 媒体配置
ENABLE_GET_MEIDAS = False  # 是否抓取图片/视频资源

# 浏览器配置
HEADLESS = False
ENABLE_CDP_MODE = True       # 使用本地 Chrome/Edge 经 CDP 控制
CDP_DEBUG_PORT = 9222
CUSTOM_BROWSER_PATH = ""
CDP_HEADLESS = False
BROWSER_LAUNCH_TIMEOUT = 60
CDP_CONNECT_EXISTING = True  # 连接已开启远程调试的浏览器
AUTO_CLOSE_BROWSER = True

# 代理配置
ENABLE_IP_PROXY = False
IP_PROXY_POOL_COUNT = 2
IP_PROXY_PROVIDER_NAME = "kuaidaili"  # kuaidaili | wandouhttp | static
STATIC_PROXY_URL = ""  # static 模式下形如 http://user:password@host:port

# 存储配置
SAVE_DATA_OPTION = "jsonl"  # csv | db | json | jsonl | sqlite | mongodb | excel | postgres
SAVE_DATA_PATH = ""         # 为空则保存到 data 目录

# 词云配置
ENABLE_GET_WORDCLOUD = False
CUSTOM_WORDS = {"零几": "年份"}  # 自定义词组
STOP_WORDS_FILE = "./docs/hit_stopwords.txt"
FONT_PATH = "./docs/STZHONGS.TTF"

DISABLE_SSL_VERIFY = False  # 仅企业代理/中间人代理场景启用

文件末尾通过 from .xhs_config import * 等方式导入 7 个平台专属配置(config/base_config.py),例如各平台的 *_SPECIFIED_ID_LIST*_CREATOR_ID_LIST

8.2 数据库配置

config/db_config.py 中所有连接参数均支持环境变量覆盖(os.getenv 提供默认值):

# MySQL
MYSQL_DB_USER = os.getenv("MYSQL_DB_USER", "root")
MYSQL_DB_PWD = os.getenv("MYSQL_DB_PWD", "123456")
MYSQL_DB_HOST = os.getenv("MYSQL_DB_HOST", "localhost")
MYSQL_DB_PORT = os.getenv("MYSQL_DB_PORT", 3306)
MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME", "media_crawler")

# Redis
REDIS_DB_HOST = os.getenv("REDIS_DB_HOST", "127.0.0.1")
REDIS_DB_PWD = os.getenv("REDIS_DB_PWD", "123456")
REDIS_DB_PORT = os.getenv("REDIS_DB_PORT", 6379)
REDIS_DB_NUM = os.getenv("REDIS_DB_NUM", 0)
CACHE_TYPE_REDIS = "redis"
CACHE_TYPE_MEMORY = "memory"

# SQLite(路径固定为仓库内 database/sqlite_tables.db)
SQLITE_DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "database", "sqlite_tables.db")

# MongoDB
MONGODB_HOST = os.getenv("MONGODB_HOST", "localhost")
MONGODB_PORT = os.getenv("MONGODB_PORT", 27017)
MONGODB_USER = os.getenv("MONGODB_USER", "")
MONGODB_PWD = os.getenv("MONGODB_PWD", "")
MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "media_crawler")

# PostgreSQL
POSTGRES_DB_USER = os.getenv("POSTGRES_DB_USER", "postgres")
POSTGRES_DB_PWD = os.getenv("POSTGRES_DB_PWD", "123456")
POSTGRES_DB_HOST = os.getenv("POSTGRES_DB_HOST", "localhost")
POSTGRES_DB_PORT = os.getenv("POSTGRES_DB_PORT", 5432)
POSTGRES_DB_NAME = os.getenv("POSTGRES_DB_NAME", "media_crawler")

9. 工具模块

9.1 工具函数概览

模块 文件 主要功能
应用运行器 app_runner.py 信号处理、优雅退出、清理管理
浏览器启动 browser_launcher.py 检测浏览器路径、启动浏览器进程
CDP管理 cdp_browser.py CDP连接、浏览器上下文管理
爬虫工具 crawler_util.py 二维码识别、验证码处理、User-Agent
文件写入 async_file_writer.py 异步CSV/JSON写入、词云生成
滑块验证 slider_util.py 滑动验证码破解
时间工具 time_util.py 时间戳转换、日期处理

9.2 应用运行管理

flowchart TB
    Start([程序启动]) --> Run["run(app_main, app_cleanup)"]
    Run --> Main["执行 app_main()"]
    Main --> Running{运行中}

    Running -->|正常完成| Cleanup1["执行 app_cleanup()"]
    Running -->|SIGINT/SIGTERM| Signal["捕获信号"]

    Signal --> First{第一次信号?}
    First -->|是| Cleanup2["启动清理流程"]
    First -->|否| Force["强制退出"]

    Cleanup1 & Cleanup2 --> Cancel["取消其他任务"]
    Cancel --> Wait["等待任务完成<br/>(超时15秒)"]
    Wait --> End([程序退出])
    Force --> End

对应 tools/app_runner.pyrun() 入口:main.pycleanup_timeout_seconds=15.0 注册清理超时,并在首次中断时通过 on_first_interrupt=_force_stop 强制回收 CDP 浏览器进程,避免浏览器进程残留。

10. 模块依赖关系

flowchart TB
    subgraph Entry["入口层"]
        main["main.py"]
        config["config/"]
        cmdarg["cmd_arg/"]
    end

    subgraph Core["核心层"]
        base["base/base_crawler.py"]
        platforms["media_platform/*/"]
    end

    subgraph Client["客户端层"]
        client["*/client.py"]
        login["*/login.py"]
    end

    subgraph Storage["存储层"]
        store["store/"]
        database["database/"]
    end

    subgraph Infra["基础设施"]
        proxy["proxy/"]
        cache["cache/"]
        tools["tools/"]
    end

    subgraph External["外部依赖"]
        playwright["Playwright"]
        httpx["httpx"]
        sqlalchemy["SQLAlchemy"]
        motor["Motor/MongoDB"]
    end

    main --> config
    main --> cmdarg
    main --> Core

    Core --> base
    platforms --> base
    platforms --> Client

    client --> proxy
    client --> httpx
    login --> tools

    platforms --> Storage
    Storage --> sqlalchemy
    Storage --> motor

    client --> playwright
    tools --> playwright

    proxy --> cache

从导入关系看,cmd_arg/arg.py 在解析参数时直接覆写 config 模块的全局变量(cmd_arg/arg.py),因此命令行参数 > 配置文件的优先级通过"先加载 config、再用 CLI 值覆写"这一机制实现,这也是所有模块共享同一份 import config 的副作用式配置方案的体现。

11. 扩展指南

11.1 添加新平台

  1. media_platform/ 下创建新目录;
  2. 实现以下核心文件:
    • core.py - 继承 AbstractCrawler
    • client.py - 继承 AbstractApiClientProxyRefreshMixin
    • login.py - 继承 AbstractLogin
    • field.py - 定义平台枚举
  3. store/ 下创建对应存储目录
  4. main.pyCrawlerFactory.CRAWLERS 中注册

同时还需要:在 config/ 下新增 {platform}_config.py 并挂入 base_config.py 的 import 列表、在 cmd_arg/arg.pyPlatformEnum 中登记代号、在 database/models.py 中补充 ORM 表。

11.2 添加新存储方式

  1. store/ 下创建新的存储实现类;
  2. 继承 AbstractStore 基类;
  3. 实现 store_contentstore_commentstore_creator 方法;
  4. 在各平台的 StoreFactory.STORES 中注册

若要成为命令行合法选项,还需同步扩充 cmd_arg/arg.pySaveDataOptionEnum

11.3 添加新代理提供商

  1. proxy/providers/ 下创建新的代理类;
  2. 继承 BaseProxy 基类(接口为 proxy/base_proxy.py 中的 ProxyProvider);
  3. 实现 get_proxy(num) 方法;
  4. proxy/proxy_ip_pool.pyIpProxyProvider 注册表中登记,并在 ProviderNameEnum 中加入对应名称。

12. 快速参考

12.1 常用命令

# 启动爬虫(默认读取 config/base_config.py)
python main.py

# 指定平台
python main.py --platform xhs

# 指定登录方式
python main.py --lt qrcode

# 指定爬虫类型
python main.py --type search

# 指定关键词与数量
python main.py --platform dy --keywords "AI,大模型" --crawler_max_notes_count 30

# 指定存储方式(csv | db | json | jsonl | sqlite | mongodb | excel | postgres)
python main.py --save_data_option sqlite

# 初始化数据库表结构(sqlite | mysql | postgres)
python main.py --init_db sqlite

完整参数还包括 --start(起始页)、--get_comment / --get_sub_comment(布尔开关,支持 yes/true/t/y/1)、--headless--cookies--specified_id--creator_id--max_comments_count_singlenotes--max_concurrency_num--save_data_path--enable_ip_proxy--ip_proxy_pool_count--ip_proxy_provider_name--static_proxy_url 等(cmd_arg/arg.py)。所有参数的默认值均取自 config 模块,配置非法时会回退到安全默认值并给出警告(_coerce_enumcmd_arg/arg.py)。

12.2 关键文件路径

用途 文件路径
程序入口 main.py
核心配置 config/base_config.py
数据库配置 config/db_config.py
爬虫基类 base/base_crawler.py
命令行解析 cmd_arg/arg.py
ORM模型 database/models.py
数据库会话 database/db_session.py
MongoDB 基类 database/mongodb_store_base.py
代理池 proxy/proxy_ip_pool.py
代理刷新 Mixin proxy/proxy_mixin.py
缓存工厂 cache/cache_factory.py
CDP浏览器 tools/cdp_browser.py
平台示例存储实现 store/douyin/_store_impl.py

12.3 架构要点回顾

  • 分层 + 工厂:入口层经 CrawlerFactoryStoreFactory 两个工厂解耦平台/存储选择,config 全局变量是各层的共享契约;
  • 基类约定AbstractCrawler / AbstractApiClient / AbstractLogin / AbstractStore 四套抽象类定义了平台实现的完整契约,ProxyRefreshMixin 以 Mixin 方式横切所有客户端;
  • 反爬三件套:CDP 真实浏览器模式、代理 IP 池(含过期刷新与静态代理)、签名脚本(libs/ 与各平台 xhs_sign.py 等);
  • 可插拔存储:7+1 种存储方式统一收敛到 AbstractStore 三方法接口,数据库类存储自带基于唯一键的 upsert 去重。

掌握以上结构后,无论是排查"某平台为什么没有 CDP 实现"(回退逻辑在基类中),还是回答"换 MySQL 后重复数据如何避免"(upsert 在 _store_impl.py 中),都可以在仓库中找到明确的代码依据。

登录后查看全文
热门项目推荐
相关项目推荐