从 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:
...
raiseyt-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。