MediaCrawler 项目架构深度解析:爬虫基类体系、存储工厂与反爬基础设施实现
本文基于仓库内的 项目架构文档 展开,逐层剖析 MediaCrawler 这个多平台自媒体爬虫框架的系统架构:从入口层、爬虫基类体系、平台客户端层到存储工厂与代理/缓存等基础设施,并结合 main.py、base/base_crawler.py、proxy/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.py 中 CrawlerFactory.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.py 中
AbstractLogin的三个抽象方法); - 多种存储方式:CSV、JSON、JSONL、SQLite、MySQL、MongoDB、Excel,另含 PostgreSQL 选项(见 config/base_config.py 与 cmd_arg/arg.py 的
SaveDataOptionEnum); - 反爬虫对策: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 的源码,这套基类体系有几个值得注意的实现细节:
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)。
-
AbstractStore只强制要求store_content、store_comment、store_creator三个抽象方法;store_image/store_video被拆分为独立的AbstractStoreImage/AbstractStoreVideo,源码注释标明当前仅微博平台实现了媒体文件存储(base/base_crawler.py)。 -
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.py、playwright_sign.py、extractor.py 等平台特定逻辑文件(签名算法与数据提取);快手平台则额外携带 graphql/ 目录存放 GraphQL 查询语句,B站的 field.py 与贴吧的测试数据 test_data/ 也体现了各平台差异化实现。这种"标准文件 + 特殊实现"的组织方式正是扩展新平台时的模板。
4.4 三种爬虫模式
| 模式 | 配置值 | 功能描述 | 适用场景 |
|---|---|---|---|
| 搜索模式 | search |
根据关键词搜索内容 | 批量获取特定主题内容 |
| 详情模式 | detail |
获取指定ID的详情 | 精确获取已知内容 |
| 创作者模式 | creator |
获取创作者所有内容 | 追踪特定博主/UP主 |
detail 与 creator 模式分别由命令行参数 --specified_id、--creator_id 提供 ID 列表(支持完整 URL 或纯 ID,逗号分隔)。cmd_arg/arg.py 会按平台将解析结果写入对应平台配置,例如 config.XHS_SPECIFIED_NOTE_URL_LIST、config.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 采用追加写入,适合大规模增量爬取; - 数据库类存储(
DouyinDbStoreImplement,DouyinSqliteStoreImplement直接继承它)实现了 upsert 去重:先按业务唯一键(如aweme_id、comment_id)查询,不存在则新建并写入add_ts时间戳,存在则逐字段setattr更新后commit(store/douyin/_store_impl.py),这正是数据库存储"带去重功能"的底层依据; - MongoDB 存储基于 database/mongodb_store_base.py 封装的
MongoDBStoreBase,以collection_prefix="douyin"组织集合,通过save_or_update完成去重写入; - Excel 存储通过
ExcelStoreBase.get_instance(...)返回全局单例(利用__new__实现),配合 main.py 中flush_all()的收尾刷盘,保证程序退出前所有行数据完整落盘。
各平台存储实现集中在 store/{platform}/_store_impl.py 中,media_platform/{platform}/core.py 在构造爬虫时经由各平台的 StoreFactory 按 config.SAVE_DATA_OPTION 取得具体实例,存储方式的选择对爬取主流程完全透明。
5.3 存储方式对比
| 存储方式 | 配置值 | 优点 | 适用场景 |
|---|---|---|---|
| CSV | csv |
简单、通用 | 小规模数据、快速查看 |
| JSON | json |
结构完整、易解析 | API对接、数据交换 |
| JSONL | jsonl |
追加写入、性能好 | 大规模数据、增量爬取(默认) |
| SQLite | sqlite |
轻量、无需服务 | 本地开发、小型项目 |
| MySQL | db |
性能好、支持并发 | 生产环境、大规模数据 |
| MongoDB | mongodb |
灵活、易扩展 | 非结构化数据、快速迭代 |
| Excel | excel |
可视化、易分享 | 报告、数据分析 |
补充一点:当前仓库的 SaveDataOptionEnum(cmd_arg/arg.py)还支持 postgres,config/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.py 中 ProxyIpPool 的关键行为值得逐条对应:
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每次请求前调用,未过期直接复用当前代理,过期则重新抽取;- 静态代理:
StaticProxyProvider(proxy/proxy_ip_pool.py)直接解析STATIC_PROXY_URL(形如http://user:password@host:port),并将过期时间设为极大值、跳过有效性校验。
提供商注册表 IpProxyProvider(proxy/proxy_ip_pool.py)当前映射了 kuaidaili、wandouhttp、static 三种;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.py 中 AbstractLogin 的抽象方法,由 cmd_arg/arg.py 的 LoginTypeEnum 枚举约束(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.py:cache_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.py 的 run() 入口:main.py 以 cleanup_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 添加新平台
- 在
media_platform/下创建新目录; - 实现以下核心文件:
core.py- 继承AbstractCrawlerclient.py- 继承AbstractApiClient和ProxyRefreshMixinlogin.py- 继承AbstractLoginfield.py- 定义平台枚举
- 在
store/下创建对应存储目录 - 在 main.py 的
CrawlerFactory.CRAWLERS中注册
同时还需要:在 config/ 下新增 {platform}_config.py 并挂入 base_config.py 的 import 列表、在 cmd_arg/arg.py 的 PlatformEnum 中登记代号、在 database/models.py 中补充 ORM 表。
11.2 添加新存储方式
- 在
store/下创建新的存储实现类; - 继承
AbstractStore基类; - 实现
store_content、store_comment、store_creator方法; - 在各平台的
StoreFactory.STORES中注册
若要成为命令行合法选项,还需同步扩充 cmd_arg/arg.py 的 SaveDataOptionEnum。
11.3 添加新代理提供商
- 在
proxy/providers/下创建新的代理类; - 继承
BaseProxy基类(接口为 proxy/base_proxy.py 中的ProxyProvider); - 实现
get_proxy(num)方法; - 在 proxy/proxy_ip_pool.py 的
IpProxyProvider注册表中登记,并在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_enum,cmd_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 架构要点回顾
- 分层 + 工厂:入口层经
CrawlerFactory与StoreFactory两个工厂解耦平台/存储选择,config全局变量是各层的共享契约; - 基类约定:
AbstractCrawler/AbstractApiClient/AbstractLogin/AbstractStore四套抽象类定义了平台实现的完整契约,ProxyRefreshMixin以 Mixin 方式横切所有客户端; - 反爬三件套:CDP 真实浏览器模式、代理 IP 池(含过期刷新与静态代理)、签名脚本(
libs/与各平台xhs_sign.py等); - 可插拔存储:7+1 种存储方式统一收敛到
AbstractStore三方法接口,数据库类存储自带基于唯一键的 upsert 去重。
掌握以上结构后,无论是排查"某平台为什么没有 CDP 实现"(回退逻辑在基类中),还是回答"换 MySQL 后重复数据如何避免"(upsert 在 _store_impl.py 中),都可以在仓库中找到明确的代码依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00