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 的做法(可插拔提取器范式):

  1. 定一个统一基类 InfoExtractor(定好"每个网站必须能干嘛");
  2. 每个网站一个独立子类(只写"这个网站怎么解析");
  3. 启动时用一个注册表收集所有子类,按 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.pyInfoExtractor_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/
作者
clxstart
发布于
2026-09-07
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。