最近在做内部培训平台的视频备份时,发现很多现成的“视频下载神器”要么失效,要么捆绑广告,要么下载下来的文件根本打不开。与其到处找工具碰运气,不如自己用 Python 写一个够用的下载器,既能按需定制下载逻辑,又能把请求头、断点续传、流媒体分片合并这些细节掌握清楚。
这篇文章会从视频播放原理讲起,逐步拆解视频真实地址的查询方法,再给出完整的 Python 下载代码,覆盖普通 MP4 直链和 M3U8 流媒体两种常见场景。无论是学习爬虫、做离线备份,还是纯粹想提升 Python 文件处理能力,这篇实战教程都能给你一套可复用的方案。
有一点必须提前说明:本文所有技术仅用于学习交流与合法授权的内容获取,请勿用于下载未授权或受版权保护的视频内容。
1. 为什么需要自己写“视频下载神器”
1.1 浏览器是怎么播放视频的
浏览器播放视频时,本质上是做了一件很简单的事:向服务器发送 HTTP 请求,拿到视频文件的字节流,然后交给视频解码器渲染。
常见的视频播放方式有两种:
- 直链播放:
<video>标签的src直接指向一个.mp4或.webm文件,浏览器直接请求该文件并播放。 - 流媒体播放:播放器通过
.m3u8索引文件获取一串.ts分片地址,逐个请求分片并顺序播放,看起来就像在播一个完整视频。
第一种情况,下载视频只需拿到直链 URL,用代码请求并保存。第二种情况,则要解析 M3U8 文件、多线程或循环下载分片、最后合并。
1.2 为什么现成下载工具总是不好用
市面上的下载工具非常多,但实际用起来总有各种限制:
- 图形界面工具经常内置广告弹窗,下载大文件时还会限速,或者强制引导用户开通会员;
- 浏览器插件版本的下载器,面对 M3U8 播放策略时容易抓不到完整地址,部分站点对分片地址做了动态签名,插件直接下载会导致 403;
- 部分下载器把“下载”做成了在线解析中转,视频地址解析要经过第三方服务器,既慢又存在隐私风险。
自己写下载器,最大的优势是逻辑透明、依赖可控,遇到问题也能直接从代码层面排查,而不是被黑盒工具卡住。
1.3 自己写下载器需要掌握什么
用 Python 写一个视频下载器,核心技能点如下:
- HTTP 请求与响应处理。
- 请求头构造,尤其是 User-Agent、Referer、Cookie。
- 文件流写入与断点续传。
- M3U8 文件格式解析。
- 多线程下载与文件合并。
- 配合 FFmpeg 处理加密流或合并转码。
这些技能单独拿出来都是 Python 开发中的常见能力,组合起来就是一个实用的下载工具。下面我们从环境准备开始,一步一步搭建。
2. 环境准备与基础工具
本文实战部分使用 Python 和 FFmpeg,建议在本地开发环境提前准备好。
2.1 Python 环境
需要 Python 3.7 及以上版本,主要使用标准库urllib和第三方库requests。
在终端中执行:
python --version pip --version如果你的环境没有安装requests,执行:
pip install requestsrequests是我们后续发起 HTTP 请求的主要库,它比标准库urllib更简洁,处理请求头、超时、Cookie 都非常方便。
2.2 FFmpeg
FFmpeg 是一个强大的音视频处理工具,在本项目中有两个作用:
- 合并 M3U8 下载下来的 TS 分片文件。
- 将某些格式的视频转码为 MP4。
FFmpeg 的安装方式根据系统不同有所区别:
# macOS brew install ffmpeg # Ubuntu / Debian sudo apt update && sudo apt install ffmpeg # Windows # 从官网下载 release 版本并配置 PATH 环境变量安装完成后验证:
ffmpeg -version如果能看到版本信息,说明 FFmpeg 已经就绪。
2.3 requests 和 FFmpeg 的分工
| 能力 | 使用工具 | 说明 |
|---|---|---|
| 下载普通文件 | Python requests | 请求视频直链并写入本地文件 |
| 下载 M3U8 分片 | Python requests | 循环请求 ts 分片,保存为本地临时文件 |
| 合并分片 | FFmpeg | 使用 concat 协议或 demuxer 合并 TS 文件 |
| 转码封装 | FFmpeg | 将 TS 转 MP4,或将音频视频合并 |
也可以选择纯 Python 方式合并 TS 分片,但 FFmpeg 处理边界情况(如时间戳错乱、编码不一致)更可靠,所以本文推荐使用 FFmpeg 完成合并环节。
3. 找到视频真实地址:从网页到直链
在写下载代码之前,必须先解决一个问题:怎么获取视频的真实下载地址?
3.1 开发者工具查看网络请求
打开 Chrome 浏览器,进入目标视频页面,按下F12打开开发者工具,切换到 Network 面板。
点击视频播放按钮,然后在 Network 面板顶部的筛选器中输入media,可以看到视频流请求:
- 如果是直链,会出现一个
type: media的请求,URL 后缀通常是.mp4。 - 如果是 M3U8 流,会看到一个
type: xhr或type: manifest的请求,URL 后缀是.m3u8。
点开这个请求,在 Headers 页面可以查看完整请求 URL 和请求头信息。这些信息非常关键,尤其是User-Agent、Referer和Cookie。
3.2 为什么直接在浏览器访问视频 URL 会 403
很多视频站点开启了防盗链机制。当你直接把视频 URL 复制到浏览器地址栏访问时,请求头中缺少来源网站的Referer信息,服务器就会拒绝请求,返回 403。
因此,在下载代码中,我们需要模拟浏览器的请求头,将User-Agent和Referer等字段原样带上。
一个常见的请求头构造如下:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0", "Referer": "https://example-video-site.com/", }具体取值要从开发者工具中复制,不能臆造。
3.3 直链与 M3U8 的选择
视频页面中可能会出现多个 media 请求,按以下规则判断:
- 如果后缀是
.mp4且响应体直接是视频数据,说明这是普通直链。 - 如果后缀是
.m3u8,说明需要通过 M3U8 文件继续请求分片。
对于 M3U8 流,还需要进一步查看 M3U8 文本内容。以text/plain或application/vnd.apple.mpegurl打开的请求,响应内容形如:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, segment_000.ts #EXTINF:10.0, segment_001.ts其中每一行#EXTINF后面跟着的就是分片文件名,需要拼接 M3U8 文件的目录作为完整 URL。
3.4 真实地址“失效”的原因
有时候地址看起来有效,但下载到一半就断了,或者下载到的文件无法播放。常见原因:
- 视频地址带动态签名,签名过期后 URL 失效;
- 服务器限制了单 IP 的并发连接数;
- 下载请求未携带正确的 Range 头,导致断点续传失败;
- M3U8 分片 URL 是相对路径,拼接错误导致 404。
这些问题我们会在后面的代码和排错部分逐一处理。
4. 实战一:下载普通 MP4 直链视频
我们先从最简单的情况开始,实现一个功能完整、代码精简的 MP4 下载器。
4.1 最小可运行版本
创建文件download_mp4.py,代码如下:
import requests def download_mp4(url, save_path, headers=None): """ 下载普通 MP4 文件 :param url: 视频直链 :param save_path: 本地保存路径 :param headers: 请求头,可选 """ if headers is None: headers = {} # 流式请求,避免一次性加载到内存 resp = requests.get(url, headers=headers, stream=True, timeout=30) # 检查响应状态 if resp.status_code != 200: print(f"请求失败,状态码: {resp.status_code}") return False # 获取文件总大小,用于显示进度 total_size = int(resp.headers.get("Content-Length", 0)) downloaded = 0 with open(save_path, "wb") as f: for chunk in resp.iter_content(chunk_size=1024 * 1024): if chunk: f.write(chunk) downloaded += len(chunk) if total_size > 0: percent = downloaded / total_size * 100 print(f"下载进度: {percent:.2f}%", end="\r") print(f"\n下载完成: {save_path}") return True if __name__ == "__main__": url = "https://example.com/video.mp4" save_path = "video.mp4" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } download_mp4(url, save_path, headers)代码说明:
stream=True表示以流式方式读取响应,不会一次性把整个视频加载到内存,适合下载大文件。resp.iter_content每次迭代读取一个指定大小的块,循环写入本地文件。- 通过
Content-Length计算下载进度,注意有些服务器不会返回该字段,所以要做total_size > 0判断。
用这段代码配合开发者工具中获取到的直链和请求头,即可下载大部分普通 MP4 视频。
4.2 添加断点续传支持
如果视频文件较大,下载过程中网络中断会导致前功尽弃。我们可以通过 HTTP 的Range请求头实现断点续传。
Range: bytes=1024-表示从第 1024 字节开始继续下载。
改进后的代码如下:
import os import requests def download_mp4_resume(url, save_path, headers=None): """ 支持断点续传的 MP4 下载 """ if headers is None: headers = {} # 如果文件已存在,记录已下载大小 downloaded_size = 0 if os.path.exists(save_path): downloaded_size = os.path.getsize(save_path) # 从断点处开始请求 resume_headers = headers.copy() if downloaded_size > 0: resume_headers["Range"] = f"bytes={downloaded_size}-" resp = requests.get(url, headers=resume_headers, stream=True, timeout=30) # 如果是 206,说明服务器支持断点续传;如果返回 200,说明服务器忽略了 Range if resp.status_code not in (200, 206): print(f"请求失败,状态码: {resp.status_code}") return False total_size = int(resp.headers.get("Content-Length", 0)) + downloaded_size mode = "ab" if downloaded_size > 0 else "wb" with open(save_path, mode) as f: for chunk in resp.iter_content(chunk_size=1024 * 1024): if chunk: f.write(chunk) downloaded_size += len(chunk) if total_size > 0: percent = downloaded_size / total_size * 100 print(f"下载进度: {percent:.2f}%", end="\r") print(f"\n下载完成: {save_path}") return True这里有两个关键点:
- 使用
"ab"模式打开文件,表示追加写入,避免覆盖已下载内容。 - 判断服务器响应状态码,如果服务器不支持 Range,会返回
200而不是206,此时需要重新从零开始下载(上面的代码保留了这种兼容,后续可以根据实际需求补充逻辑)。
4.3 完整调用示例
if __name__ == "__main__": video_url = "https://example.com/video.mp4" save_path = "downloads/demo.mp4" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://example.com/", } os.makedirs("downloads", exist_ok=True) download_mp4_resume(video_url, save_path, headers)5. 实战二:下载 M3U8 流媒体视频
M3U8 是一种基于 HTTP 的流媒体传输方案,常用于直播和长视频点播。它的核心思路是:把一个完整视频切成很多小的 TS 分片文件,播放器按顺序请求播放。我们要做的,就是把所有分片下载下来,再合并成一个完整视频。
5.1 M3U8 文件结构解析
M3U8 本质上是一个文本文件,常见的字段有:
| 字段 | 含义 |
|---|---|
#EXTM3U | 文件头,表示这是一个 M3U8 文件 |
#EXT-X-VERSION | 协议版本号 |
#EXT-X-TARGETDURATION | 单个分片最大时长 |
#EXT-X-MEDIA-SEQUENCE | 分片序号起点 |
#EXTINF | 分片时长,下一行是分片地址 |
#EXT-X-KEY | 加密信息,如果存在则说明分片被 AES 加密 |
#EXT-X-ENDLIST | 文件结束标记,无此标记可能是直播流 |
一个典型的 M3U8 文件:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, https://example.com/segments/segment_000.ts #EXTINF:10.0, https://example.com/segments/segment_001.ts #EXT-X-ENDLIST如果分片地址是相对路径,需要与 M3U8 地址的目录部分拼接。例如 M3U8 地址是https://example.com/path/index.m3u8,分片地址是segment_000.ts,则完整地址是https://example.com/path/segment_000.ts。
5.2 下载 M3U8 分片并合并
创建一个新文件download_m3u8.py,实现完整的下载流程:
import os import re import requests from urllib.parse import urljoin def download_m3u8(m3u8_url, save_path, headers=None): """ 下载 M3U8 流媒体视频 :param m3u8_url: M3U8 地址 :param save_path: 最终保存的视频文件路径 :param headers: 请求头 """ if headers is None: headers = {} # 1. 请求 M3U8 文件 resp = requests.get(m3u8_url, headers=headers, timeout=30) if resp.status_code != 200: print(f"M3U8 文件请求失败,状态码: {resp.status_code}") return False m3u8_content = resp.text # 2. 解析所有分片地址 segment_urls = [] for line in m3u8_content.splitlines(): line = line.strip() # 跳过注释行 if line.startswith("#"): continue if line: # 拼接绝对地址 segment_url = urljoin(m3u8_url, line) segment_urls.append(segment_url) if not segment_urls: print("未找到任何分片地址,请检查 M3U8 内容") return False print(f"发现 {len(segment_urls)} 个分片") # 3. 创建临时目录保存分片 temp_dir = save_path + "_parts" os.makedirs(temp_dir, exist_ok=True) # 4. 循环下载分片 for i, segment_url in enumerate(segment_urls): segment_path = os.path.join(temp_dir, f"segment_{i:04d}.ts") try: seg_resp = requests.get(segment_url, headers=headers, timeout=30) if seg_resp.status_code == 200: with open(segment_path, "wb") as f: f.write(seg_resp.content) if (i + 1) % 20 == 0 or i + 1 == len(segment_urls): print(f"分片进度: {i + 1}/{len(segment_urls)}") else: print(f"分片 {i} 下载失败,状态码: {seg_resp.status_code}") return False except requests.RequestException as e: print(f"分片 {i} 下载异常: {e}") return False print("所有分片下载完成,开始合并...") # 5. 使用 FFmpeg 合并分片 concat_file = os.path.join(temp_dir, "concat.txt") with open(concat_file, "w") as f: for i in range(len(segment_urls)): segment_path = os.path.join(temp_dir, f"segment_{i:04d}.ts") f.write(f"file '{segment_path}'\n") ffmpeg_cmd = ( f"ffmpeg -f concat -safe 0 -i {concat_file} " f"-c copy -y {save_path}" ) result = os.system(ffmpeg_cmd) if result == 0: print(f"合并完成: {save_path}") # 清理临时文件 import shutil shutil.rmtree(temp_dir) return True else: print("FFmpeg 合并失败,临时文件保留在:", temp_dir) return False if __name__ == "__main__": m3u8_url = "https://example.com/path/index.m3u8" save_path = "output_video.mp4" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } download_m3u8(m3u8_url, save_path, headers)这段代码的核心流程:
- 请求 M3U8 文件,读取文本内容。
- 按行解析,跳过
#开头的注释行,得到分片地址。 - 使用
urljoin正确处理相对路径和绝对路径。 - 循环下载每个分片,按序号保存在临时目录中。
- 生成 FFmpeg concat 列表文件,执行合并命令。
FFmpeg 文件名中的中文或特殊字符可能导致问题,如果遇到合并失败,可以先把分片复制到纯英文路径再合并。
5.3 使用多线程加速分片下载
分片数量较多时,逐个下载速度很慢。我们可以用 Python 的ThreadPoolExecutor实现并发下载。
from concurrent.futures import ThreadPoolExecutor, as_completed def download_one_segment(segment_url, save_path): """下载单个分片""" try: resp = requests.get(segment_url, timeout=30) if resp.status_code == 200: with open(save_path, "wb") as f: f.write(resp.content) return True except requests.RequestException as e: print(f"分片下载异常: {segment_url} -> {e}") return False def download_segments_concurrent(segment_urls, temp_dir, headers=None, max_workers=5): """并发下载全部分片""" tasks = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: for i, segment_url in enumerate(segment_urls): save_path = os.path.join(temp_dir, f"segment_{i:04d}.ts") future = executor.submit(download_one_segment, segment_url, save_path) tasks.append((i, future)) failed = 0 for i, future in tasks: if not future.result(): failed += 1 if (i + 1) % 50 == 0: print(f"已提交 {i + 1}/{len(segment_urls)} 个分片任务") return failed == 0使用多线程时要注意:并发数不要太大,一般 5 到 10 个线程足够。过大容易触发服务器限流,反而导致部分分片下载失败。
将主流程中的下载循环替换为:
success = download_segments_concurrent(segment_urls, temp_dir, headers, max_workers=8) if not success: print("部分分片下载失败,请检查网络后重试") return False5.4 关于 M3U8 加密分片
部分视频源的 M3U8 文件中会有#EXT-X-KEY字段,说明分片使用了 AES-128 加密。这种情况下,还需要解析密钥并解密分片。
# 从 M3U8 内容中提取密钥地址 key_match = re.search(r'#EXT-X-KEY:METHOD=AES-128,URI="([^"]+)"', m3u8_content) if key_match: key_url = urljoin(m3u8_url, key_match.group(1)) key_resp = requests.get(key_url, headers=headers, timeout=30) key = key_resp.content解密分片时,使用pycryptodome库的 AES 模块:
from Crypto.Cipher import AES cipher = AES.new(key, AES.MODE_CBC, key) # IV 默认等于 key decrypted = cipher.decrypt(segment_content)注意:很多站点的 IV 并不是默认值,而是写在#EXT-X-KEY字段中,需要仔细解析。实际项目如果遇到加密流,直接解密分片再合并会复杂一些,更推荐直接用 FFmpeg 处理:
ffmpeg -allowed_extensions ALL -i "https://example.com/index.m3u8" -c copy output.mp4FFmpeg 会自动处理 AES 解密逻辑。
6. 处理防盗链:请求头与 Cookie 实战
6.1 防盗链的原理
服务器通过检查Referer字段判断请求来源。如果请求头中的Referer不是预期站点,服务器直接拒绝,返回 403。
6.2 构造完整请求头
下载视频时,建议把开发者工具中的关键请求头都带上:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0", "Referer": "https://example-video-site.com/", "Origin": "https://example-video-site.com", "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", }不要一次性把所有请求头都搬过来,重点是User-Agent、Referer和Cookie三个字段。
6.3 处理需要登录的视频
部分视频需要登录后才能播放。此时需要在请求头中携带登录后的 Cookie 信息:
cookies = { "session_id": "xxxxxxxx", "auth_token": "yyyyyyyy", } resp = requests.get(url, headers=headers, cookies=cookies, stream=True)在实际项目里,更推荐从浏览器的开发者工具中直接复制Cookie字符串,然后转换成字典:
cookie_str = "session_id=xxxxxxxx; auth_token=yyyyyyyy" cookie_dict = dict(item.split("=", 1) for item in cookie_str.split("; ") if "=" in item)6.4 动态签名 URL 的处理思路
有些播放地址会带signature、token、expires等参数,地址会过期。这种地址通常是从网页的xhr请求中动态获取的。
处理思路:
- 先用 Python 请求网页或接口,获取视频播放地址。
- 再用获取到的播放地址去下载视频。
- 如果播放地址有效期很短,需要缩短从获取到下载的时间间隔。
示例思路:
# 1. 获取播放地址接口 api_url = "https://example-video-site.com/api/video/playinfo" payload = {"video_id": "123456"} resp = requests.post(api_url, json=payload, headers=headers) video_url = resp.json()["data"]["video_url"] # 2. 立即下载 download_mp4_resume(video_url, "output.mp4", headers)这种接口的结构每个站点都不一样,需要结合具体网页的 Network 请求分析,没有统一的固定代码。
7. 常见问题与排查思路
在下载过程中,最常遇到的就是下面这几种问题。这里整理成表格,方便快速对照排查:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 请求返回 403 | 缺少 Referer 或 User-Agent,被防盗链拦截 | 从开发者工具补齐请求头,尤其是 Referer |
| 下载到几十 KB 就结束 | 请求到的内容是错误提示页或 JSON,不是真实视频 | 用文本编辑器打开文件检查内容,确认 URL 是否正确 |
| M3U8 合并失败 | 分片路径拼接错误,或分片不是 TS 格式 | 检查分片 URL 是否 200,手工下载一个分片用 FFmpeg 探测 |
| FFmpeg 找不到文件 | concat 文件中路径含中文或特殊字符 | 使用纯英文临时目录,或调整 concat 文件路径格式 |
| 下载速度很慢 | 单线程下载,或服务器限速 | 使用多线程并发下载分片,控制线程数在 5-10 个 |
| M3U8 请求成功但无分片 | 文件内容可能是嵌套 M3U8,或者接口返回了动态内容 | 打印 M3U8 文本内容,查看是否有#EXT-X-STREAM-INF,嵌套 M3U8 需要二次解析 |
| 下载完成后视频无法播放 | 下载过程中缺分片,或合并命令不对 | 检查分片下载成功率,重新合并,或改用-c copy参数 |
| 播放地址过期 | 动态签名 URL 有效期太短 | 缩短解析和下载间隔,将获取地址与下载合并到一个脚本中 |
| 请求报 SSL 证书错误 | 服务器证书不受信任 | 在测试环境可临时关闭验证,生产环境建议更新证书库 |
7.1 嵌套 M3U8 怎么处理
M3U8 文件可能是多级结构。顶层 M3U8 里不是分片,而是多个不同清晰度的子 M3U8 地址:
#EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,RESOLUTION=1280x720 https://example.com/path/720p/index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360 https://example.com/path/360p/index.m3u8遇到这种情况,最简单的处理方式是选择分辨率最高的子 M3U8,递归解析:
if "#EXT-X-STREAM-INF" in m3u8_content: # 找到最后一个 STREAM-INF 后的 URL lines = m3u8_content.strip().splitlines() # 从后往前找第一个非注释行,作为子 M3U8 地址 child_url = None for line in reversed(lines): line = line.strip() if line and not line.startswith("#"): child_url = urljoin(m3u8_url, line) break if child_url: m3u8_url = child_url resp = requests.get(m3u8_url, headers=headers, timeout=30) m3u8_content = resp.text7.2 断点续传导致文件损坏
断点续传有一个隐藏陷阱:如果服务器返回的Content-Length和本地已下载文件的长度不可靠,会导致文件拼接出错。
排查建议:
- 记录 URL、文件总大小、断点位置等日志,方便复现。
- 下载完成后校验文件大小是否等于
Content-Length。 - 如果服务器不支持 Range,直接改为重头下载。
8. 最佳实践与工程建议
8.1 明确使用边界
写视频下载器的技术本身是中性的,但使用时必须注意边界:
- 只能下载自己有权限下载的内容,例如开源视频、教学资料、公司内部培训视频等。
- 不要批量下载他人版权内容,不要用于商业分发。
- 涉及登录态 Cookie 时,不要随意泄露到公开仓库。
在代码的 README 或注释中加入合法使用声明,既是工程习惯,也是保护自己的方式。
8.2 控制并发与频率
对目标服务器要设置合理的请求频率,避免造成服务器压力过大。推荐做法:
- 普通 MP4 下载使用单线程即可。
- M3U8 分片并发控制在 5-10。
- 使用
time.sleep()在分片请求之间增加短暂延迟,尤其是面对服务器限流时。 - 设置
requests超时时间,避免线程长期卡住。
8.3 日志与错误处理
下载大文件时最怕中断后不知道卡在哪里。建立完善的日志输出:
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler()] )每次分片下载、文件合并、异常捕获都通过logging输出,方便排查。
8.4 使用临时目录与清理机制
分片文件占用的磁盘空间可能很大,建议:
- 下载过程中使用独立临时目录。
- 合并完成后立即清理临时文件。
- 异常退出时也要在
finally中尽量清理,避免垃圾文件堆积。
try: download_segments(segment_urls) merge_video() finally: shutil.rmtree(temp_dir, ignore_errors=True)8.5 封装成命令行工具
为了方便复用,可以把下载逻辑封装成命令行工具,使用argparse接收参数:
import argparse parser = argparse.ArgumentParser(description="视频下载工具") parser.add_argument("--url", required=True, help="视频地址或 M3U8 地址") parser.add_argument("--output", default="output.mp4", help="输出文件路径") parser.add_argument("--referer", default="", help="Referer 请求头") parser.add_argument("--concurrent", type=int, default=5, help="并发下载数") args = parser.parse_args()这样使用时只需要一条命令:
python video_downloader.py --url "https://example.com/index.m3u8" --output demo.mp4 --concurrent 88.6 更多可靠的开源工具
如果你不想从零搭建,也可以直接使用开源社区成熟的命令行工具,它们大多内部封装了 M3U8 解析、分片下载、解密、合并等能力。
以常见的yt-dlp和ffmpeg组合为例,可以直接处理大量网站的视频下载场景。不过这类工具同样要遵守目标网站的使用协议,且更新速度较快,使用时建议从官方仓库获取最新版本。
即使使用现成工具,理解底层下载和合并原理仍然很有价值,因为遇到问题时要能判断是工具问题还是视频源问题。
9. 总结
通过这篇文章,你实际上已经掌握了开发一个视频下载工具所需的核心知识:
- 视频网页的请求过程,以及如何通过开发者工具获取真实地址。
- 普通 MP4 直链的下载与断点续传。
- M3U8 流媒体文件解析、分片下载、并发加速与 FFmpeg 合并。
- 防盗链、Cookie 和动态签名地址的处理思路。
- 常见报错的排查方法。
下一步可以继续学习的方向包括:更完善的命令行 UI 封装、数据库记录下载任务、定时批量下载任务调度、以及如何给工具增加自定义文件命名规则。如果对流媒体协议感兴趣,还可以深入研究 HLS 加密机制、DASH 协议和 FFmpeg 的高级滤镜用法。
请在动手练习时始终把“授权”和“版权”放在第一位。技术没有对错,但使用技术的人需要清楚边界。
如果这篇文章对你有帮助,可以收藏备用,也欢迎在评论区分享你在实际下载过程中遇到的坑。