1131 字
约 3 分钟
1
怎么让一个程序支持「几百个长得不一样的网站」——可插拔提取器范式
① 怎么让一个程序支持「几百个长得不一样的网站」——可插拔提取器范式
目标:学一个能直接用在自己项目里的软件模式。yt-dlp 用这套范式支持了 1800+ 网站——这是它最值钱、可迁移性最强的设计。
一、问题的本质
你要对接 10 个、100 个外部源(网站/API),每个长得不一样:
| 源 | 返回的样子 |
|---|---|
| 源A | HTML 网页,媒体在 <video> 标签 |
| 源B | 一个 JSON API,URL 藏在深层字段 |
| 源C | 要先执行一段 JS 才能拿到地址 |
| 源D | 需要登录 Cookie 才给链接 |
最烂的写法:一个巨大的函数,if site==A ... elif site==B ... 写几百个分支。加一个网站就改一次主函数,越改越乱。
yt-dlp 的做法(可插拔提取器范式):
- 定一个统一基类
InfoExtractor(定好"每个网站必须能干嘛"); - 每个网站一个独立子类(只写"这个网站怎么解析");
- 启动时用一个注册表收集所有子类,按 URL 自动派发。
加新网站 = 新增一个文件 + 新增一个类,主程序一行都不用改。
二、范式长什么样(概念图)
┌──────────────────────────┐
│ InfoExtractor(基类) │ ← 定契约:只规定框架
│ - _VALID_URL │ 每个子类都要实现
│ - _real_extract(url) │
└──────────┬───────────────┘
继承 │
┌────────┬───────────┼────────────┬───────────┐
▼ ▼ ▼ ▼ ▼
SiteAIE SiteBIE SiteCIE ... GenericIE
(类) (类) (类) (兜底类,.*)
每个子类:
- 声明 _VALID_URL(我匹配哪些链接)
- 实现 _real_extract(这个网站怎么把 URL 变成统一结构)
new site = 新增一个子类。旧的、不相关的类完全不动 → 开闭原则(对扩展开放、对修改关闭)。
三、两个关键机制(可迁移的精华)
① 模板方法 Template Method
基类定义"总执行流程",子类只填"变化的细节"。
# 基类定义"流程骨架",子类只补 _real_extract
class InfoExtractor:
def extract(self, url):
self.initialize() # 公共:登录/日志
return self._real_extract(url) # 变化的部分,交给子类
def _real_extract(self, url):
raise NotImplementedError # 子类必须实现
class SiteAIE(InfoExtractor):
def _real_extract(self, url): # 只写"A 网站怎么解析"
return {..., 'formats': [...]}
[!tip] 你不用改基类的
extract(),因为"初始化的顺序、日志格式"每个网站都一样;你只改_real_extract()这个"填空点"。这就是模板方法:固定骨架 + 可变填物。
② 注册表 + 按 URL 派发
一个全局字典 {网站名: 提取器类},用 URL 匹配 _VALID_URL 找出该用哪个。
# 伪代码:核心派发逻辑
for name, cls in registry.items():
if matches(cls._VALID_URL, url): # URL 命中这个网站的规则
return cls().extract(url) # 就让这个网站的提取器处理
[!important] 兜底:最后一个
GenericIE用.*匹配一切 → 任何没被显式识别的 URL 都有处理,不会崩。"不可能穷举所有情况"时的兜底设计,很值得借鉴。
四、怎么抄到自己项目里(3 步)
第 1 步:定基类(接口)
class Source(ABC):
_match: str = None # 识别条件(对应 _VALID_URL)
@abstractmethod
def fetch(self, key): ... # 子类实现:这个源怎么拿数据
@abstractmethod
def normalize(self, raw): ... # 子类实现:转成统一结构
第 2 步:每个源一个子类
class NewsSource(Source):
_match = 'news.example'
def fetch(self, key): return requests.get(...).json()
def normalize(self, raw): return {'title': raw['t'], 'body': raw['b']}
第 3 步:注册表派发
REGISTRY = {cls._match: cls for cls in Source.__subclasses__()}
def get_source(link):
for k, cls in REGISTRY.items():
if k in link: return cls()
return GenericSource() # 兜底
以后接新网站:写一个新的子类,注册表自动收编,主流程不用改。
五、与这篇文档相关的源码位置
- 基类与契约:
yt_dlp/extractor/common.py(InfoExtractor,_VALID_URL在 :580+,模板方法extract在 :755,抽象_real_extract在 :828) - 注册表顺序:
yt_dlp/extractor/extractors.py(Youtube 优先、GenericIE 兜底) - 完整论述见学习库 → [[Extractor-Subsystem]]、[[Pattern-Template-Method-写法示例|模板方法]]
六、一句话带走
面对"多个长得不同的外部源",不要 if-else 堆死,要:一个统一接口 + 每个源一个实现 + 一张注册表自动派发 + 一个兜底。这就是 yt-dlp 支持 1800+ 网站的根本原因,也是你以后对接任何多源系统能直接抄的范式。
怎么让一个程序支持「几百个长得不一样的网站」——可插拔提取器范式
http://clxhxhhr.top/posts/517/ 评论
0 条
还没有评论,先写一条吧。