最近遇到一个需求:朋友发了一个爱奇艺分享链接,要把视频下载下来存档。
以前这种事 yt-dlp 一行命令搞定,这次折腾了两小时才发现——常用的几条路全堵死了。最后靠 Playwright 浏览器拦截才拿到视频流地址。把整个过程记下来,给遇到同样问题的人省点时间。
先说结论
截至 2026 年 6 月,爱奇艺视频下载的现状:
| 方案 | 状态 | 说明 |
|---|---|---|
| yt-dlp iqiyi 提取器 | ❌ 失效 | 报 “Can’t find any video” |
| 直接 curl dash API | ❌ 失效 | 返回 “Time expired”,缺浏览器认证参数 |
| mixer API | ❌ 失效 | 404 Not Found |
| Playwright 拦截 dash 响应 | ✅ 可用 | 让浏览器帮你带认证,拦截响应拿 m3u8 |

下面按时间顺序讲踩坑过程,着急的可以直接跳到”最终方案”那节。
第一坑:yt-dlp 爱奇艺提取器失效
拿到分享链接后第一反应是 yt-dlp:
yt-dlp -F "https://m.iqiyi.com/mp/sharePlay.html?tvid=8518700492290400&..."
# ERROR: [iqiyi] Can't find any video
从分享链接里提取 tvid,拼标准 URL 也试了:
yt-dlp -F "http://www.iqiyi.com/v_2bql2y1y6fg.html"
# 同样报错
升级 yt-dlp 到最新版(2026.06.09),结果一样。查了一下源码,yt-dlp 的 iqiyi.py 提取器依赖的页面元素和 API 结构已经变了,短期内不太可能修复。
第二坑:curl 直调 dash API
换思路,直接调爱奇艺的播放流 API。先用 baseinfo 接口拿到视频元信息(这个接口是公开的,不需要认证):
curl -s "https://pcw-api.iqiyi.com/video/video/baseinfo/8518700492290400"
返回里有 tvid、vid、播放地址等关键字段。拿到 vid 后试调 dash 接口:
curl -s "https://cache.video.iqiyi.com/dash?tvid=8518700492290400&vid=844482a856d1c5fbec039430383d02dc&..." \
-H "User-Agent: Mozilla/5.0 ..."
结果:
{"msg": "Time expired", "code": "A00020"}
即使用当前时间戳替换 tm 参数也一样。原因是 dash API 还需要 k_uid(浏览器指纹)、dfp(设备指纹)、authKey(签名)这几个参数,全由爱奇艺播放器在浏览器里动态生成,纯 HTTP 请求伪造不了。
试了 mixer API(mixer.video.iqiyi.com),直接 404。
最终方案:Playwright 拦截
思路很简单——既然 dash API 需要浏览器环境里的认证参数,那就让浏览器来发起请求,我们只负责从浏览器的网络请求里”截获”响应数据。
原理
爱奇艺播放器加载时,会自动发起 cache.video.iqiyi.com/dash 请求。浏览器自带完整的认证参数(k_uid、dfp、authKey),服务器返回的响应里包含完整的 HLS m3u8 playlist——每个 TS 分片的 URL 都带好了签名。
Playwright 的 page.on("response", ...) 可以监听所有网络响应,包括跨域请求。我们要做的就是:加载页面 → 等播放器发请求 → 捕获 dash 响应 → 解析 m3u8。

步骤 1:拦截 dash 响应
import asyncio, json
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
dash_responses = []
async def on_response(response):
if 'cache.video.iqiyi.com/dash' in response.url:
try:
body = await response.json()
dash_responses.append(body)
except:
pass
page.on("response", on_response)
# 用短视频页面触发播放器加载
await page.goto(
"https://www.iqiyi.com/shortvideo/?tvid=8518700492290400",
timeout=20000, wait_until="domcontentloaded"
)
await asyncio.sleep(8) # 等 dash 请求完成
await browser.close()
for i, resp in enumerate(dash_responses):
with open(f'/tmp/iqiyi_dash_{i}.json', 'w') as f:
json.dump(resp, f, ensure_ascii=False, indent=2)
print(f"Captured {len(dash_responses)} dash responses")
asyncio.run(main())
几个要点:
- URL 构造:
https://www.iqiyi.com/shortvideo/?tvid=<TVID>,分享链接会重定向到这个地址 - headless=True 就行:不需要有头浏览器
- 等待 8 秒:播放器加载和 dash 请求需要时间,太短会抓不到
- dash 可能返回两次:第一次可能只有空 vid,第二次才是完整响应。脚本用数组全部捕获,后面选有效那个
步骤 2:解析响应,提取 m3u8
dash 响应的结构:
data
├── ctl.bid: 500 (最高可用画质)
└── program.video[]
├── [0] bid=500, 720x1272 竖屏, ~72MB, m3u8="..."
├── [1] bid=300, 较低画质, m3u8="" (空)
└── [2] bid=200, ~17.5MB
选最高画质(bid 最大)的流,提取 m3u8 字段:
with open('/tmp/iqiyi_dash_0.json') as f:
data = json.load(f)
videos = data['data']['program']['video']
# 取最高画质,跳过 m3u8 为空的流
best = max(
(v for v in videos if v.get('m3u8')),
key=lambda v: v.get('bid', 0)
)
print(f"Resolution: {best['scrsz']}") # 720x1272
print(f"Size: {best['mp4Size'] / 1024 / 1024:.1f} MB") # 72.5 MB
print(f"Duration: {best['duration']}s") # 452
with open('/tmp/iqiyi_playlist.m3u8', 'w') as f:
f.write(best['m3u8'])
m3u8 内容是一个标准 HLS playlist,包含 97 个 TS 分片,每个约 5 秒,分片 URL 带认证参数(qd_tm、qd_p、qd_k、qd_sc)。

步骤 3:用 N_m3u8DL-RE 下载
拿到 m3u8 文件后,用 N_m3u8DL-RE 下载(也可以用 ffmpeg,但 N_m3u8DL-RE 支持多线程,快很多):
N_m3u8DL-RE "/tmp/iqiyi_playlist.m3u8" \
--save-dir ./downloads \
--save-name "iqiyi_video" \
--thread-count 8 \
-H "Referer: https://www.iqiyi.com/" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
--del-after-done
实测 97 个分片,8 线程约 3 秒下完,输出 72.5 MB 的 mp4 文件。
验证下载结果
ffprobe -v error -show_entries format=duration,size,bit_rate \
-show_entries stream=width,height,codec_name \
-of default=noprint_wrappers=1 output.mp4
确认视频完整:H.264 + AAC,720×1272 竖屏,452 秒,码率 1,345 kbps。
一体化脚本
把上面三步合成一个脚本,接受 tvid 参数,一步到位:
#!/usr/bin/env python3
"""爱奇艺视频下载:Playwright 拦截 dash API → 提取 m3u8 → 下载"""
import asyncio, json, subprocess, sys
from playwright.async_api import async_playwright
async def capture_m3u8(tvid: str) -> str:
"""用 Playwright 拦截 dash 响应,返回 m3u8 内容"""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
dash_data = []
async def on_response(response):
if 'cache.video.iqiyi.com/dash' in response.url:
try:
dash_data.append(await response.json())
except:
pass
page.on("response", on_response)
await page.goto(
f"https://www.iqiyi.com/shortvideo/?tvid={tvid}",
timeout=20000, wait_until="domcontentloaded"
)
await asyncio.sleep(8)
await browser.close()
for resp in dash_data:
try:
videos = resp['data']['program']['video']
best = max(
(v for v in videos if v.get('m3u8')),
key=lambda v: v.get('bid', 0)
)
return best['m3u8']
except (KeyError, ValueError):
continue
raise RuntimeError("未捕获到有效的 dash 响应")
def download_m3u8(m3u8_content: str, save_dir: str, save_name: str):
"""用 N_m3u8DL-RE 下载 m3u8"""
import tempfile, os
m3u8_path = os.path.join(save_dir, f"{save_name}.m3u8")
with open(m3u8_path, 'w') as f:
f.write(m3u8_content)
subprocess.run([
"N_m3u8DL-RE", m3u8_path,
"--save-dir", save_dir,
"--save-name", save_name,
"--thread-count", "8",
"-H", "Referer: https://www.iqiyi.com/",
"-H", "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"--del-after-done"
], check=True)
async def main():
tvid = sys.argv[1] if len(sys.argv) > 1 else "8518700492290400"
print(f"正在拦截 tvid={tvid} 的 dash 响应...")
m3u8 = await capture_m3u8(tvid)
print(f"获取到 m3u8,长度 {len(m3u8)} 字节")
save_dir = "./downloads"
import os
os.makedirs(save_dir, exist_ok=True)
print("开始下载...")
download_m3u8(m3u8, save_dir, f"iqiyi_{tvid}")
print("下载完成")
asyncio.run(main())
使用方式:
# 需要先安装 Playwright 浏览器
pip install playwright && playwright install chromium
# 下载指定 tvid 的视频
python3 iqiyi_download.py 8518700492290400
踩坑备忘
整理几个容易踩的坑:
1. VIP 视频需要登录态
上面的方法只能下载免费内容。如果视频是 VIP 专属,Playwright 需要先注入爱奇艺的登录 cookie。可以手动在浏览器里登录后导出 cookie,然后用 page.context.add_cookies() 注入。
2. 分享链接不能直接访问
爱奇艺的分享页面(m.iqiyi.com/mp/sharePlay.html)检测 User-Agent,非微信浏览器返回 404。所以不能直接用 Playwright 访问分享链接,需要先通过 baseinfo API 拿到 tvid,再构造短视频页 URL。
3. m3u8 中的 TS 分片 URL 有时效性
带签名的分片链接过一段时间会失效。拿到 m3u8 后要尽快下载,不要隔太久。
4. 画质取决于源文件
UGC(用户上传)视频的最高画质通常只有 720p,这是爱奇艺对非专业内容的限制,不是工具的问题。
5. N_m3u8DL-RE 不在 PATH 里
如果你用 Docker 容器运行,N_m3u8DL-RE 可能没装或者不在 PATH 里。要么把二进制放到 /usr/local/bin/,要么在脚本里用绝对路径。
总结
爱奇艺视频下载在 2026 年基本只剩 Playwright 拦截这一条路。核心思路就是”浏览器帮你带认证”——你不需要知道 k_uid、dfp 这些参数怎么生成,让播放器自己请求,你只管截获响应里的 m3u8。
整套方案依赖两个工具:Playwright(截获 dash 响应)和 N_m3u8DL-RE(下载 m3u8)。前者 pip install 就行,后者在 GitHub Release 页面下载对应平台的二进制。
如果你也遇到 yt-dlp 失效的情况,不妨试试这个方案。