抖音视频批量下载方案
环境与工具
| 工具 | 用途 | 安装方式 |
|---|---|---|
| Python 3.11+ | 主控脚本语言 | 系统预装 |
websockets (Python) |
与 Chrome CDP 通信 | pip install websockets -i https://pypi.tuna.tsinghua.edu.cn/simple |
| Chrome 浏览器 | 提供登录态和视频页面渲染 | 系统预装,需以调试模式启动 |
ffmpeg |
合并分离的音视频流 | 从 BtbN/FFmpeg-Builds 下载解压 |
用清华 pip 镜像可大幅提速。
Chrome 启动方式(关键)
必须以远程调试端口启动,否则无法通过 CDP 连接:
Stop-Process -Name chrome -Force -ErrorAction SilentlyContinue
Start-Sleep 2
Start-Process "C:\Program Files\Google\Chrome\Application\chrome.exe" `
-ArgumentList "--remote-debugging-port=9222", `
"--user-data-dir=$env:LOCALAPPDATA\Google\Chrome\User Data", `
"about:blank"
要点:
- 必须先用
Stop-Process杀掉所有 Chrome 进程再启动,否则调试端口不会生效。 --user-data-dir必须指向用户的真实 Chrome profile,这样登录态(包括社媒助手等扩展)才能加载。- 验证端口是否就绪:
Invoke-WebRequest http://127.0.0.1:9222/json/version
下载流程
1. 获取视频列表
从抖音收藏夹提取的所有视频链接,保存到 douyin-link.md,一行一个 URL,格式如:
https://www.douyin.com/video/7630049695850646827
https://www.douyin.com/video/7658605034971598107
2. 核心原理
抖音 PC 端视频使用 DASH(Dynamic Adaptive Streaming over HTTP)技术,将视频分为纯视频流和纯音频流两个独立文件,存放在 douyinvod.com CDN 上。这两个流的 URL 带有签名 token,需要在浏览器会话中实时捕获。
3. Python 脚本流程
每段视频分三步完成:
Step 1:通过 CDP 导航到视频页面并捕获音视频流 URL
import asyncio, json, urllib.request, websockets
async def get_video_urls(video_page_url, ws_url):
async with websockets.connect(ws_url, max_size=2**30) as ws:
# 启用 Network 监控
await ws.send(json.dumps({"id":1, "method":"Network.enable"}))
await ws.recv()
# 导航到目标视频
await ws.send(json.dumps({"id":2, "method":"Page.navigate",
"params":{"url": video_page_url}}))
captured = []
start = asyncio.get_event_loop().time()
while asyncio.get_event_loop().time() - start < 12:
msg = await asyncio.wait_for(ws.recv(), timeout=2)
data = json.loads(msg)
if "method" in data and data["method"] == "Network.responseReceived":
resp = data["params"]["response"]
url = resp.get("url", "")
if "douyinvod.com" in url and "video" in resp.get("mimeType", ""):
captured.append(url)
if len(captured) >= 2:
break
return captured
- 通过
Page.navigate导航,利用 Chrome 的登录态。 - 监听
Network.responseReceived事件,过滤出douyinvod.com且 MIME 类型为video/*的请求。 - 通常每个视频有 2 个流:一个带
audio标识(音频流),一个带video标识(视频流)。部分短视频只有 1 个流(音视频已合并)。
Step 2:用 urllib 下载音视频流
def download(url, path, retries=3):
for i in range(retries):
try:
req = urllib.request.Request(url)
req.add_header("Referer", "https://www.douyin.com/")
req.add_header("User-Agent", "Mozilla/5.0 ...")
with urllib.request.urlopen(req, timeout=60) as resp:
content = resp.read()
with open(path, "wb") as f:
f.write(content)
return True
except Exception as e:
if i < retries - 1:
time.sleep(2)
return False
要点:
- 必须带上
Referer: https://www.douyin.com/,否则 CDN 会拒绝请求。 User-Agent必须模拟浏览器。- 捕获的 URL token 有效期很短(几分钟),必须马上使用。
- 加 3 次重试 +
IncompleteRead容错,避免网络波动导致失败。
Step 3:用 ffmpeg 合并音视频流
ffmpeg -i video1_video.mp4 -i video1_audio.mp4 `
-c copy -map 0:v:0 -map 1:a:0 -shortest video1.mp4 -y
参数说明:
-c copy:不重新编码,直接拷贝流(速度快)。-map 0:v:0:取第一个输入的视频轨。-map 1:a:0:取第二个输入的音频轨。-shortest:以较短的流为准截断(避免音视频不同步)。-y:覆盖已有文件。
获取 CDP WebSocket URL
def get_ws_url():
with urllib.request.urlopen("http://127.0.0.1:9222/json") as resp:
pages = json.loads(resp.read())
for p in pages:
if "douyin.com" in p.get("url", ""):
return p["webSocketDebuggerUrl"]
return pages[0]["webSocketDebuggerUrl"]
验证 CDP 连接
Invoke-WebRequest "http://127.0.0.1:9222/json/version" -UseBasicParsing
成功返回 JSON,包含 webSocketDebuggerUrl 字段即表示 CDP 可用。
完整的单视频下载脚本示例
文件:dl_douyin.py
import asyncio, json, urllib.request, os, time, websockets
CHROME_CDP = "http://127.0.0.1:9222"
OUT_DIR = r"D:\product\news\douyin-videos"
os.makedirs(OUT_DIR, exist_ok=True)
# ... 上述 get_ws_url / download / get_video_urls 函数 ...
async def process_video(url, prefix, ws_url):
"""下载单个视频的音视频流"""
streams = await get_video_urls(url, ws_url)
audio_url = next((s for s in streams if "audio" in s), None)
video_url = next((s for s in streams if "audio" not in s), None)
if audio_url and video_url:
# 分离的音视频流:分别下载后合并
audio_path = os.path.join(OUT_DIR, f"{prefix}_audio.mp4")
video_path = os.path.join(OUT_DIR, f"{prefix}_video.mp4")
download(audio_url, audio_path)
download(video_url, video_path)
# 用 ffmpeg 合并
out_path = os.path.join(OUT_DIR, f"{prefix}.mp4")
os.system(f'ffmpeg -i "{video_path}" -i "{audio_path}" -c copy -map 0:v:0 -map 1:a:0 -shortest "{out_path}" -y')
os.remove(audio_path)
os.remove(video_path)
elif video_url:
# 已合并的视频流:直接下载
download(video_url, os.path.join(OUT_DIR, f"{prefix}.mp4"))
async def main():
ws_url = get_ws_url()
await process_video("https://www.douyin.com/video/7630049695850646827", "video1", ws_url)
常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
CDP 连接被拒 ECONNREFUSED |
Chrome 未以调试模式启动,或 Chrome 未完全关闭就重启 | Stop-Process -Name chrome -Force 后再启动 |
IncompleteRead |
网络波动导致下载中断 | 加重试逻辑(3次),每次间隔2秒 |
Fresh cookies are needed |
yt-dlp 提取不到有效 cookies | 放弃 yt-dlp,改用 CDP + urllib 直接下载 |
| 只有一个流(无音频) | 短视频可能音视频未分离 | 检查 ffprobe 输出,如果已含音轨则直接使用 |
| token 过期 | CDN URL 有效期短 | 必须在捕获后 立即 下载,不要提前批量收集 |