☰
Python实战:M3U8分片下载与FFmpeg合并,一键保存智慧教育平台课程
2026/9/26 5:51:44 网站建设 项目流程

1. 这个工具到底解决了什么问题

国家中小学智慧教育平台上的课程资源,质量确实不错,覆盖了从小学到高中的主要学科,而且都是官方录制的完整课程。但用过的人都知道,它有一个非常让人头疼的限制:没有提供批量下载功能。你想把一节40分钟的课保存到本地,只能在线看,或者用一些不太靠谱的录屏方式,画质和音质都打折扣。

更麻烦的是,这个平台的视频采用了M3U8 分片传输的方式。简单来说,一个完整的视频被切成了几百上千个小的.ts文件,通过一个.m3u8索引文件来组织播放顺序。你打开浏览器开发者工具,看到的是密密麻麻的请求列表,手动一个个下载再合并,几乎是不可能完成的任务。

所以,smartedu-download这个项目的核心价值就一句话:把国家中小学智慧教育平台上的课程视频,一键下载到本地,自动合并成完整的 MP4 文件。它用 Python 写成,封装了从解析 M3U8 索引、批量下载 TS 分片、到调用 FFmpeg 合并输出的完整流程。对于家里网络不稳定、想给孩子反复看课、或者想把优质课程存档的家长和老师来说,这个工具非常实用。

这篇文章我会从零开始,把整个项目的设计思路、核心代码、实操步骤、以及我踩过的坑,全部拆开讲清楚。即使你之前没写过 Python,跟着走也能跑起来。

2. 整体设计思路与方案选型

2.1 为什么选择 Python 而不是其他语言

这个项目本质上是一个网络请求 + 文件处理 + 媒体合并的自动化脚本。Python 在这个场景下有天然优势:

  • requests 库处理 HTTP 请求极其简洁,几行代码就能完成带 Cookie、Referer 的请求
  • 多线程/多进程支持成熟,下载几百个 TS 分片时能显著提速
  • FFmpeg 的 Python 封装或者直接subprocess调用都很方便
  • 跨平台,Windows、macOS、Linux 都能跑

如果用 Go 或 Rust 写,性能会更好,但开发效率和代码可读性对普通用户不够友好。这个工具的目标用户是家长和老师,不是专业程序员,所以 Python 是最平衡的选择。

2.2 M3U8 下载的核心逻辑

M3U8 是 HLS(HTTP Live Streaming)协议使用的索引格式。一个典型的 M3U8 文件长这样:

#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.000000, segment_000.ts #EXTINF:10.000000, segment_001.ts #EXTINF:10.000000, segment_002.ts ... #EXT-X-ENDLIST

每一行#EXTINF后面跟着的是这个分片的时长,下一行是分片文件名。下载工具要做的就是:

  1. 请求 M3U8 文件,解析出所有 TS 分片的 URL
  2. 并发下载所有 TS 分片到本地临时目录
  3. 按顺序拼接所有 TS 分片
  4. 用 FFmpeg 把 TS 流转换为 MP4 格式

注意:有些 M3U8 文件是加密的,会有#EXT-X-KEY标签,需要先获取密钥才能解密。国家中小学智慧教育平台的资源目前大部分没有加密,但如果你遇到加密的源,需要额外处理密钥请求。

2.3 为什么需要 FFmpeg

TS 分片直接拼接得到的文件,虽然能用某些播放器打开,但兼容性很差。FFmpeg 的作用是:

  • 重新封装:把 TS 流封装成 MP4 容器,兼容所有播放器
  • 修复时间戳:分片拼接后时间戳可能不连续,FFmpeg 会自动处理
  • 转码(可选):如果源视频编码格式特殊,可以转成 H.264 + AAC 的通用格式

FFmpeg 的调用命令很简单:

ffmpeg -i input.ts -c copy -bsf:a aac_adtstoasc output.mp4

-c copy表示不重新编码,直接复制流,速度极快。-bsf:a aac_adtstoasc是处理 AAC 音频流的比特流过滤器,避免音频播放异常。

2.4 打包成 EXE 的考量

很多用户没有 Python 环境,所以项目提供了打包成 EXE 的方案。常用的工具有PyInstaller和Nuitka。PyInstaller 更简单,一行命令就能打包:

pyinstaller -F -w smartedu_download.py

-F表示打包成单个文件,-w表示不显示控制台窗口。但 PyInstaller 打包出来的 EXE 体积较大(通常 10-30MB),启动也稍慢。Nuitka 会把 Python 代码编译成 C,性能和体积都更优,但配置复杂一些。

3. 核心细节解析与实操要点

3.1 如何获取真实的 M3U8 地址

这是整个流程中最关键的一步。国家中小学智慧教育平台的视频播放页面,并不是直接把 M3U8 地址写在 HTML 里的,而是通过 JavaScript 动态加载。你需要:

  1. 打开课程播放页面
  2. 按 F12 打开开发者工具,切换到Network(网络)面板
  3. 筛选m3u8关键字
  4. 刷新页面,找到返回 M3U8 内容的请求
  5. 复制这个请求的 URL

实操心得:平台的 M3U8 地址通常带有时效性 Token,过期后需要重新获取。所以下载要尽快开始,不要拖太久。

3.2 请求头的伪装

直接请求 M3U8 地址可能会被拒绝,因为服务器会检查Referer和User-Agent。你需要带上:

headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://basic.smartedu.cn/", }

Referer告诉服务器你是从平台页面跳转过来的,User-Agent模拟浏览器。这两个字段缺一不可,否则大概率返回 403。

3.3 多线程下载的线程数选择

TS 分片数量可能从几十到上千不等。单线程下载太慢,但线程数也不是越多越好。我的经验是:

分片数量推荐线程数理由
< 1008-16分片少,线程太多反而增加调度开销
100-50016-32平衡速度和服务器压力
> 50032-64分片多,需要更高并发

线程数过高会导致服务器限流,甚至封 IP。建议从 16 开始测试,如果下载速度稳定,再逐步增加。

3.4 分片顺序的保证

多线程下载时,分片完成的顺序是随机的。但合并时必须按原始顺序拼接,否则视频会乱序。解决方案有两种:

  • 方案一:下载时用分片索引作为文件名,如0001.ts、0002.ts,合并时按文件名排序
  • 方案二:用一个列表记录分片顺序,下载完成后按列表顺序拼接

我推荐方案一,简单可靠,而且方便断点续传。

3.5 FFmpeg 的路径问题

FFmpeg 不是 Python 标准库,需要单独安装。Windows 用户下载ffmpeg.exe后,要么放到系统 PATH 里,要么在代码里指定绝对路径:

FFMPEG_PATH = r"C:\ffmpeg\bin\ffmpeg.exe"

常见坑:如果路径中有空格,subprocess调用时需要用引号包裹,或者用列表形式传参。我建议用列表形式,避免转义问题。

4. 完整实操流程与核心代码

4.1 环境准备

首先确保你的电脑上有 Python 3.8 以上版本。Windows 用户去 Python 官网下载安装包,安装时勾选Add Python to PATH。安装完成后,打开命令行验证:

python --version pip --version

然后安装依赖库:

pip install requests pip install tqdm

requests用于网络请求,tqdm用于显示下载进度条。FFmpeg 需要单独下载,解压后把bin目录添加到系统 PATH。

4.2 解析 M3U8 文件

import requests import re def parse_m3u8(m3u8_url, headers): resp = requests.get(m3u8_url, headers=headers, timeout=30) resp.raise_for_status() content = resp.text # 提取所有 TS 分片文件名 ts_list = re.findall(r'^[^#].*\.ts.*$', content, re.MULTILINE) # 处理相对路径 base_url = m3u8_url.rsplit('/', 1)[0] ts_urls = [] for ts in ts_list: ts = ts.strip() if ts.startswith('http'): ts_urls.append(ts) else: ts_urls.append(f"{base_url}/{ts}") return ts_urls

这段代码的核心是正则表达式^[^#].*\.ts.*$,它会匹配所有不以#开头且包含.ts的行。re.MULTILINE让^和$匹配每一行的开头和结尾。

4.3 多线程下载分片

import os from concurrent.futures import ThreadPoolExecutor, as_completed from tqdm import tqdm def download_segment(args): url, index, save_dir, headers = args filename = os.path.join(save_dir, f"{index:05d}.ts") # 断点续传:如果文件已存在且大小合理,跳过 if os.path.exists(filename) and os.path.getsize(filename) > 0: return filename for retry in range(3): try: resp = requests.get(url, headers=headers, timeout=60) resp.raise_for_status() with open(filename, 'wb') as f: f.write(resp.content) return filename except Exception as e: if retry == 2: raise e return filename def download_all(ts_urls, save_dir, headers, max_workers=16): os.makedirs(save_dir, exist_ok=True) tasks = [(url, i, save_dir, headers) for i, url in enumerate(ts_urls)] with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [executor.submit(download_segment, task) for task in tasks] for future in tqdm(as_completed(futures), total=len(futures), desc="下载分片"): future.result()

这里有几个关键点:

  • 断点续传:如果文件已存在且大小大于 0,直接跳过。这样中断后重新运行,不会重复下载
  • 重试机制:每个分片最多重试 3 次,避免网络抖动导致失败
  • 进度条:tqdm让用户看到下载进度,体验更好

4.4 合并分片并转 MP4

import subprocess def merge_segments(save_dir, output_file, ffmpeg_path="ffmpeg"): # 获取所有 TS 文件并按名称排序 ts_files = sorted([f for f in os.listdir(save_dir) if f.endswith('.ts')]) # 生成合并列表文件 list_file = os.path.join(save_dir, "filelist.txt") with open(list_file, 'w', encoding='utf-8') as f: for ts in ts_files: f.write(f"file '{ts}'\n") # 调用 FFmpeg 合并 cmd = [ ffmpeg_path, "-f", "concat", "-safe", "0", "-i", list_file, "-c", "copy", "-bsf:a", "aac_adtstoasc", output_file ] subprocess.run(cmd, cwd=save_dir, check=True) print(f"合并完成:{output_file}")

-f concat告诉 FFmpeg 使用拼接模式,-safe 0允许使用绝对路径,-c copy不重新编码。整个过程通常只需要几秒钟。

4.5 打包成 EXE

如果你想把脚本分享给没有 Python 环境的朋友,可以用 PyInstaller 打包:

pip install pyinstaller pyinstaller -F -w -i icon.ico smartedu_download.py

打包完成后,dist目录下会生成一个.exe文件。但要注意:

  • FFmpeg 不会被自动打包进去,需要单独放在同目录下
  • 如果代码里用了相对路径,打包后路径会变化,建议用os.path.dirname(sys.executable)获取 EXE 所在目录

实操心得:PyInstaller 打包的 EXE 启动时会解压到临时目录,杀毒软件可能会误报。建议给 EXE 添加数字签名,或者提前告知用户添加信任。

5. 常见问题与排查技巧实录

5.1 下载速度慢怎么办

问题现象:分片下载速度只有几十 KB/s,一个视频要下几个小时。

排查思路:

  1. 检查网络本身是否正常,尝试访问其他网站
  2. 降低线程数,可能是服务器限流
  3. 检查是否走了代理,某些代理会限制大文件下载
  4. 尝试更换 DNS,有些 CDN 节点解析慢

解决方案:我实测下来,线程数从 16 降到 8,速度反而更稳定。另外,把下载任务安排在非高峰时段(比如深夜),速度会明显提升。

5.2 FFmpeg 合并报错

问题现象:ffmpeg报错Invalid data found when processing input。

原因分析:通常是某个 TS 分片下载不完整或损坏。

解决方案:

  • 检查分片文件大小,异常小的文件(如 0 字节)就是损坏的
  • 删除损坏的分片,重新下载
  • 如果多个分片损坏,可能是网络问题,建议降低线程数重试

5.3 视频播放没有声音

问题现象:合并后的 MP4 能播放,但没有声音。

原因分析:AAC 音频流的 ADTS 头没有正确处理。

解决方案:确保 FFmpeg 命令中加了-bsf:a aac_adtstoasc。如果还是不行,尝试去掉-c copy,让 FFmpeg 重新编码音频:

ffmpeg -i input.ts -c:v copy -c:a aac -b:a 128k output.mp4

5.4 EXE 在别人电脑上打不开

问题现象:自己电脑上能运行的 EXE,发给别人后报错。

排查清单:

问题检查项解决方法
缺少 DLL是否在纯净系统测试用--add-data打包依赖
路径错误是否用了绝对路径改用sys.executable获取路径
杀毒拦截是否被 Windows Defender 拦截添加信任或签名
权限不足是否在 Program Files 目录改用用户目录

5.5 M3U8 地址失效

问题现象:昨天还能下载的地址,今天报 403。

原因分析:平台的 Token 有时效性,通常几小时到一天。

解决方案:重新从浏览器获取新的 M3U8 地址。建议获取后立即开始下载,不要拖延。

5.6 常见问题速查表

问题可能原因快速解决
403 Forbidden缺少 Referer补全请求头
分片下载失败网络抖动增加重试次数
合并后花屏分片顺序错乱检查文件名排序
音频不同步时间戳问题用 FFmpeg 重新封装
EXE 体积过大PyInstaller 打包改用 Nuitka 或 UPX 压缩
下载中断网络断开利用断点续传重新运行

6. 进阶优化与扩展思路

6.1 支持批量下载整个课程

单个视频下载只是基础,实际使用中更常见的是下载整个课程(几十个视频)。思路是:

  1. 解析课程页面的 HTML,提取所有视频的 ID
  2. 对每个视频 ID,调用平台的 API 获取 M3U8 地址
  3. 循环调用下载函数

这里的关键是找到平台的 API 接口。通过浏览器开发者工具,观察播放页面加载时的 XHR 请求,通常能找到返回视频信息的接口。

6.2 添加图形界面

命令行工具对普通用户不够友好。可以用tkinter或PyQt做一个简单的 GUI:

  • 输入框:粘贴 M3U8 地址
  • 选择目录:选择保存位置
  • 开始按钮:触发下载
  • 进度条:显示下载进度

tkinter是 Python 自带的,不需要额外安装,适合快速开发。

6.3 自动识别并下载

更进一步,可以做一个浏览器插件或者油猴脚本,在播放页面自动提取 M3U8 地址,然后调用本地工具下载。这样用户只需要点一下按钮,完全不需要手动复制地址。

6.4 处理加密的 M3U8

如果遇到加密的 M3U8,需要:

  1. 解析#EXT-X-KEY标签,获取密钥 URL
  2. 请求密钥 URL,得到解密密钥
  3. 用 AES-128 解密每个 TS 分片

Python 的pycryptodome库可以处理 AES 解密。这部分代码稍复杂,但原理不复杂。

7. 我踩过的坑和实操建议

第一个坑是Referer 缺失。我一开始只带了 User-Agent,结果一直返回 403。后来抓包对比浏览器请求,才发现少了 Referer。这个字段在平台的反盗链机制中很关键。

第二个坑是线程数过高导致 IP 被限。我一开始设了 64 线程,下载了大概 200 个分片后,所有请求都超时了。等了几分钟才恢复。后来改成 16 线程,稳定跑完整个视频。

第三个坑是FFmpeg 路径问题。我在代码里写的是ffmpeg,但 Windows 上如果 PATH 没配好,会报FileNotFoundError。后来改成配置文件读取,让用户可以自己指定路径。

第四个坑是打包后的路径问题。PyInstaller 打包后,__file__指向的是临时解压目录,不是 EXE 所在目录。用sys.executable才能拿到正确路径。

最后分享一个小技巧:下载前先请求第一个分片,确认能正常返回,再启动多线程下载。这样可以避免因为地址失效导致所有线程都失败。

这个工具后续还可以扩展成支持其他教育平台的下载,核心逻辑是一样的,只需要适配不同的 API 和请求头。如果你对 Python 爬虫和媒体处理感兴趣,这个项目是一个很好的练手素材。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询