大部分人看视频的路径是:打开网站或 App,点播放,完事。但如果你想离线缓存、收藏老剧、或者把视频存到 NAS 统一管理,就会发现流媒体播放器并不提供”保存到本地”这个按钮。
因为视频走的不是 mp4 直链,而是 m3u8 协议——视频被切成几十上百个 ts 小片段,播放器一边拉一边播。想下载,就得先理解这套机制,然后走完一条完整的链路:搜索资源 → 发现 m3u8 → 多源对比 → 下载 → 后处理。
下面把这 5 个环节拆开说。

搜索:不止百度
大多数视频下载的第一步不是下载,是找资源。常见途径有两条。
CMS 采集站搜索。很多视频站背后用的是 TVBox 的采集系统,播放源都是同一套接口。只要找到任意一个接入 TVBox 的站点,就能用它提供的搜索 API 跨站查资源。搜索脚本的开源实现很多,基本逻辑是:加载 sources 列表 → 并发请求(15 线程,10 秒超时)→ 去重 → 返回结果。网络请求记得设置 trust_env=False 或 --noproxy '*' 绕过系统代理,否则 CDN 容易拒连。
直接访问视频聚合站。像 meijukankan、mjvod 这类站点,直接搜剧集名就能找到播放页。它们的 m3u8 链接一般通过三种方式嵌入页面:
- base64 变量:
var now=base64decode("...") - JSON 对象:
var player_aaaa={"flag":"play",...},m3u8 在link字段 - 页面直出:m3u8 URL 直接出现在 HTML 源码里
提取方法不复杂——如果页面结构固定,甚至可以直接用 Python 正则全局匹配 index\.m3u8。需要注意的是,JSON 对象的 link 字段有时带反斜杠转义,取到后要做一次 unescape。
选源:不是所有源都值得下
找到播放页之后,页面上一般有多个播放源(vfrom=0,1,2,3…)。不要取第一个可用源就停——不同源的画质差异可能很大,720p 和 4K 都能叫”可用”。
我的做法是遍历每个 vfrom,拿到对应的 m3u8 链接后:
# 请求 Master Playlist 看分辨率和带宽信息
curl -s "$m3u8_url" | grep -i 'RESOLUTION\|BANDWIDTH'
# 取一个 ts 分片测速度
ts_url=$(curl -s "$m3u8_url" | grep -v '^#' | head -1)
ts_full="${m3u8_url%/*}/$ts_url"
time curl -s -o /dev/null -w '%{speed_download}' "$ts_full"
速度基准:> 500 KB/s 可用,100~500 KB/s 慢但能接受,< 100 KB/s 直接放弃。实测经验是,同一个站点不同源的带宽差距可以到 20 倍以上——快的源 1.5~2.8 MB/s,慢的连 100 KB/s 都跑不到。
下载:三种方式,一种首选
拿到可用的 m3u8 链接后,有三种方式把视频抓下来。

N_m3u8DL-RE(首选)。这是目前最好用的命令行 HLS 下载器。多线程、支持 AES-128 解密、自动合并为 mp4,一次搞定。基本用法:
N_m3u8DL-RE "https://example.com/path/index.m3u8" \
--save-dir ./downloads \
--save-name "video_name" \
--thread-count 8 \
-H "Referer: https://www.example.com/" \
--del-after-done
几个有用的参数:--thread-count 控制线程数(默认 2,推荐 8~16),--http-request-timeout 设长一点(60 秒以上),对需要 Referer 的源加 -H 头。如果你在 Linux 下处理大文件(1000+ 分片),下载前先执行 ulimit -n 4096,否则会遇到 Too many open files。
实际测试,一部 98 分钟的 1080p 电影(1052 个分段),8 线程下载 + AES 解密 + ffmpeg 合并,总共花了 11 分钟。效率可以接受。
ffmpeg(备选)。如果你不想装任何额外工具,ffmpeg 就行:
ffmpeg -y -user_agent "Mozilla/5.0 ..." \
-i "https://example.com/path/index.m3u8" \
-c copy output.mp4
优点是系统自带,缺点是单线程、不支持续传、无法限速。适合小文件或临时场景。
手动 ts 合并(fallback)。下载 m3u8 → 提取所有 ts 片段 → 逐个 curl → ffmpeg concat 合并。这个路径走一遍只是为了理解协议原理,日常没必要这么折腾。
后处理:验证、去广告、归档
下载完不要直接不管。我一般做三件事。
验证完整性。用 ffprobe 查时长和文件大小:
ffprobe -v error -show_entries format=duration,size \
-of default=noprint_wrappers=1 output.mp4
一集 60 分钟的 1080p 电视剧大约 1-2GB。如果文件只有 60-70MB,说明下载中间被截断了,重下。
去广告。国内流媒体源经常在视频里插赌博广告。如果是黑边广告(letterbox),用 ffmpeg 的 cropdetect 可以自动检测画面变化区域辅助定位。如果是全帧硬切广告,需要黑帧检测或 ASR 转写,这部分比较麻烦,不是所有源都需要处理。
归档。单次下载丢下载目录就行,长期收藏就按「剧名/集数」的目录结构存好。
进阶:自动追更
如果你在追连载剧,可以写个简单的 cron 任务自动检查新集数:
# 1. 爬取剧集页,获取最新集数
latest=$(curl -s "https://example.com/show/{id}.html" |
grep -oP '至第\d+集' | grep -oP '\d+')
# 2. 对比已下载记录
record_file="/data/.剧名.latest"
record=0; [ -f "$record_file" ] && record=$(cat "$record_file")
# 3. 有新集则下载并更新记录
if [ "$latest" -gt "$record" ]; then
# 遍历 vfrom → 速度测试 → N_m3u8DL-RE 下载
echo "$latest" > "$record_file"
fi
这个方案的问题是对每个剧都需要单独设置一个 cron 任务。想更省事,可以直接用 Python 写一个通用调度器,从配置文件读剧集列表,循环检查更新。但说实话,手动触发比自动调度放心——跑坏了也不会积压一堆断片。
踩坑记录
这 9 个坑是我实际下载过程中踩过的,列出来供参考。
-
代理导致 CDN 拒连。部分 CDN(比如 yzzy.play-cdn6.com)会检测并拒绝代理流量。症状是 curl HTTPS 超时或被拒。用
trust_env=False或--noproxy '*'直连可解。 -
HTTP 代理不支持 HTTPS。如果你用 9090 HTTP 代理抓 HTTPS 链接,会报 exit 35。常见 mihomo/Clash 的 HTTP 端口不支持隧道,改用 SOCKS5(7890)或直连。
-
Too many open files。1000+ 分片下载时 Linux 默认 fd 限制不够。下载前先 ulimit -n 4096。
-
声明分辨率与实际不符。m3u8 的 BANDWIDTH 和 RESOLUTION 可以伪造,我遇到过标 1080p 但实际只有 576p 的源。下完后用 ffprobe 验一下。
-
部分站点防盗链。加
Referer头基本都能解决。B 站必须带Referer: https://www.bilibili.com/,否则返回 403。 -
磁力下载需直连。BT 协议走代理会对端感知到异常 IP,速度极慢甚至连不上。下载磁力时关掉代理环境变量和 TUN 模式。
-
HTTP/2 PROTOCOL_ERROR。部分站点的 WAF 会拦截非浏览器流量。降级到 HTTP/1.1(加
--http1.1或用 Python requests 的默认行为)可以绕过。 -
追更漏下。如果下某一集时失败但记录已经更新,这集就永远丢了。解决方法是记录成功下载的集数,而不是”最新集数”。
-
旧源撤下。发布数周后的剧集,播放源可能已经撤下。所以追更要趁早,不要等全集完结再一次性下。
一条完整的链路
从找资源到本地保存,核心就这几步:搜得到 → 找得对 → 下得动 → 存得好。上面这些操作听起来不少,但熟手走一遍一条链接不过几分钟。关键在于每个环节都有替代方案——搜索有多种途径,选源有多维对比,下载有三条路径递进。哪个环节卡住,切到备选方案就行。
我对这件事的理解是:在线流媒体给了你播放的便利,但没给你拥有的自由。想本地保存的人,本质上是在拿动手时间换拥有权。