跳到正文

从 Python 使用 yt-dlp:内嵌 API、选项、钩子,以及在服务器中运行

作者 Tunelio 团队发布于

yt-dlp 是一个 Python 包,所以可以直接导入而不是通过 shell 调用:yt_dlp.YoutubeDL(opts).extract_info(url) 返回元数据,需要的话也返回文件。本文涵盖选项字典、不下载只获取格式、进度钩子、错误处理、让异步 Web 服务器保持响应、所需的 Docker 镜像 —— 以及为他人运行时每个视频的 CPU 成本。

安装

python3 -m pip install -U "yt-dlp[default]"
# 另外机器上需要 ffmpeg(合并/音频)和 Deno 之类的 JS 运行时(YouTube 挑战)

最小下载示例

import yt_dlp

opts = {
    "format": "bv*+ba/b",              # 最佳视频 + 最佳音频,合并(需要 ffmpeg)
    "merge_output_format": "mp4",
    "outtmpl": "downloads/%(title)s.%(ext)s",
    "noplaylist": True,
    "quiet": True,
}
with yt_dlp.YoutubeDL(opts) as ydl:
    ydl.download(["https://youtu.be/dQw4w9WgXcQ"])

每个命令行参数都有对应的字典键;映射关系记录在 yt_dlp/YoutubeDL.py(类的 docstring)和 yt_dlp/options.py 中。拿不准时,用你想要的参数加 --print-json 运行 CLI,再照抄选项名。

不下载,只取元数据和格式

import yt_dlp

with yt_dlp.YoutubeDL({"quiet": True, "skip_download": True}) as ydl:
    info = ydl.extract_info("https://youtu.be/dQw4w9WgXcQ", download=False)

print(info["title"], info["duration"])
for f in info["formats"]:
    if f.get("vcodec") != "none" and f.get("acodec") != "none":     # 渐进式(音视频在一个文件里)
        print(f["format_id"], f.get("height"), f.get("ext"), f.get("filesize_approx"))
best_audio = max((f for f in info["formats"] if f.get("vcodec") == "none"), key=lambda f: f.get("abr") or 0)
print("audio:", best_audio["format_id"], best_audio["url"][:60], "...")

extract_info 返回的 JSON 与 CLI 用 -j 打印的相同:带直接 googlevideo URL 的格式、缩略图、字幕、章节。这些 URL 是针对请求它们的 IP 和客户端签名的,大约六小时后过期 —— 请把它们交给同一台机器上的 ffmpeg 或下载器,而不是交给你的用户。

进度钩子与日志

def on_progress(d):
    if d["status"] == "downloading":
        print(d.get("_percent_str"), d.get("_speed_str"), end="\r")
    elif d["status"] == "finished":
        print("\n完成,后处理中:", d["filename"])

class QuietLogger:
    def debug(self, msg): pass
    def warning(self, msg): print("WARN", msg)
    def error(self, msg): print("ERR", msg)

opts = {"progress_hooks": [on_progress], "logger": QuietLogger(), "format": "bv*+ba/b"}

错误处理

from yt_dlp.utils import DownloadError, ExtractorError

try:
    with yt_dlp.YoutubeDL(opts) as ydl:
        info = ydl.extract_info(url, download=False)
except DownloadError as e:
    msg = str(e)
    if "Sign in to confirm" in msg:      # 机器人墙 → IP/cookies/PO token,见机器人验证指南
        ...
    elif "HTTP Error 403" in msg:       # 见 403 指南
        ...
    elif "Private video" in msg or "Video unavailable" in msg:
        ...
    raise

yt-dlp 几乎把一切都包在 DownloadError 里,原始信息在其中;按信息文本匹配即可。处理播放列表时设置 "ignoreerrors": True,这样一个失效条目不会中止整个运行(这类条目会以 None 返回)。

Cookies、代理及其他生产选项

opts = {
    "cookiefile": "/secrets/cookies.txt",           # Netscape 格式;只用备用账号
    "proxy": "socks5h://user:pass@exit.example:1080",
    "source_address": "0.0.0.0",                    # 强制 IPv4(即 -4 参数)
    "extractor_args": {"youtube": {"player_client": ["tv", "web"]}},
    "sleep_interval_requests": 1,
    "retries": 3,
    "socket_timeout": 30,
}

规则与命令行相同:cookies 来自备用账号的隐私窗口,用住宅出口而非数据中心出口,除非确定 IPv6 网段干净否则用 IPv4。cookies、代理和 403 指南分别解释了每一项。

在异步服务器内(FastAPI、aiohttp、Telegram 机器人)

import asyncio
from fastapi import FastAPI

app = FastAPI()
_sem = asyncio.Semaphore(4)   # 限制并发提取 —— 每次约 1 秒 CPU

def _extract(url: str) -> dict:
    with yt_dlp.YoutubeDL({"quiet": True, "skip_download": True}) as ydl:
        return ydl.extract_info(url, download=False)

@app.get("/info")
async def info(url: str):
    async with _sem:
        data = await asyncio.to_thread(_extract, url)   # 绝不要在事件循环上直接调用 yt-dlp
    return {"title": data["title"], "duration": data["duration"]}

yt-dlp 是同步且 CPU 密集的:在我们的服务器上,一次 YouTube 提取约消耗 1.2–1.3 秒 CPU,大部分花在运行播放器 JavaScript 上。在异步处理函数中直接调用会冻结所有其他请求;请在线程或进程池中运行并限制并发,否则服务在每秒几个请求时就会垮掉。

Docker

FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg curl unzip ca-certificates \
 && rm -rf /var/lib/apt/lists/* \
 && curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh
RUN pip install --no-cache-dir "yt-dlp[default]" fastapi uvicorn
COPY app.py /app/app.py
USER 1000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app"]

三个运行时依赖:yt-dlp、ffmpeg 和 JavaScript 运行时。至少每月重建镜像(见更新指南)—— 装着三个月前 yt-dlp 的容器在大多数视频上都会失败。以非 root 用户运行,并使用独立的缓存目录。

规模化时的成本与可靠性

  • CPU:现代核心上每次提取约 1.2–1.3 秒,每个请求都要付(播放器 JS 会被重新求值)。每秒十个请求光 yt-dlp 就需要十几个核心。
  • IP 信誉:一台从单一地址每天提取数百个视频的服务器会撞上机器人墙;最终你得运行代理和 PO token 服务。
  • 失效:YouTube 每次变动都是一次重新部署;原因见更新指南。
  • 带宽:你转发给用户的媒体会两次经过你的服务器。

这就是托管 API 存在的原因。Tunelio(我们自己的服务)负责提取并对每个请求返回下载 URL,你的 Python 只剩一次 HTTP 调用,容器里不需要 ffmpeg、运行时或代理。对笔记本上的脚本来说,上面的内嵌 API 才是正确的工具。新账号可获得 100 个免费额度。

来源

  • yt-dlp README —— “Embedding yt-dlp” 一节,以及 yt_dlp/YoutubeDL.py 中的选项参考。
  • yt-dlp wiki —— Extractor args(player_client)与 PO token 指南。
  • 我们自己的测量(Tunelio 提取集群,2026):每次提取的 CPU、数据中心 IP 的机器人墙比例。

常见问题

该用 subprocess 调用 yt-dlp CLI 还是直接导入?

导入更干净且能得到结构化数据;subprocess 能隔离崩溃和内存。无论哪种,都要在事件循环之外运行并限制并发。

能不下载就拿到直接的视频 URL 吗?

可以:extract_info(url, download=False) 返回每种格式及其 URL。它们绑定你的 IP/客户端,大约六小时后过期,所以要在服务端立即使用。

为什么 yt-dlp 运行时我的 FastAPI 应用会卡住?

yt-dlp 是阻塞且 CPU 密集的。用 asyncio.to_thread(或进程池)包装它,并用信号量限制并行提取数。

哪些选项键对应哪些参数?

大多数参数变成 snake_case 键(--merge-output-format → merge_output_format)。权威列表是 yt_dlp/YoutubeDL.py 中 YoutubeDL 类的 docstring。

相关指南