最近帮朋友下载一个在线教育平台的视频课程时,发现这类网站普遍采用M3U8视频流配合AES-128加密的技术方案。这种方案既能保证视频传输效率,又能防止简单抓包下载。经过多次实战,我总结出一套完整的解决方案,下面就以某会计网校为例,手把手教你如何破解这个技术难题。
M3U8本质上是个播放列表文件,里面记录了视频的分片信息。当视频采用AES加密时,每个.ts分片都需要用密钥解密后才能播放。整个过程就像拼图游戏:先找到藏宝图(m3u8文件),再按图索骥收集碎片(ts分片),最后用钥匙(AES密钥)把碎片还原成完整图画。接下来我会用最通俗的语言,配合具体代码示例,带你走完整个流程。
工欲善其事必先利其器,我们需要准备以下工具:
安装加密库时要注意,Python有两个常用加密库:pycrypto和pycryptodome。前者已停止维护,推荐使用后者:
bash复制pip install pycryptodome requests
打开目标视频页面后,按F12调出开发者工具。在Network面板中过滤"m3u8"请求,通常能在XHR或Media标签下找到线索。以会计网校为例,视频地址往往藏在某个JSON响应中,需要分析网页源码中的JavaScript代码。这里有个小技巧:直接搜索"videoPath"这类关键词,能快速定位到视频信息。
获取到m3u8文件URL后,用requests下载内容分析:
python复制m3u8_url = "https://example.com/playlist.m3u8"
response = requests.get(m3u8_url)
content = response.text
典型的加密m3u8文件会包含如下关键信息:
code复制#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key.key"
#EXTINF:6.006000,
segment_000.ts
从m3u8文件中正则提取密钥URL后,需要特别注意:有些网站会对密钥进行Base64编码,而有些直接返回二进制数据。处理时要先尝试Base64解码:
python复制key_url = re.findall(r'URI="(.*?)"', content)[0]
key_response = requests.get(key_url)
key = base64.b64decode(key_response.content) # 尝试Base64解码
if len(key) != 16: # AES-128密钥长度应为16字节
key = key_response.content # 直接使用二进制数据
拿到密钥后,就可以逐个下载并解密ts分片了。这里有个关键点:AES-128-CBC模式需要初始化向量(IV)。很多网站使用全零IV,但最好检查m3u8文件中是否指定了IV:
python复制cipher = AES.new(key, AES.MODE_CBC, iv=bytes.fromhex('00000000000000000000000000000000'))
with open('encrypted.ts', 'rb') as f:
decrypted_data = cipher.decrypt(f.read())
# 注意:解密后可能需要去除PKCS7填充
pad = decrypted_data[-1]
decrypted_data = decrypted_data[:-pad]
逐个下载ts文件速度较慢,可以使用线程池加速。但要注意控制并发数,避免被封IP:
python复制from concurrent.futures import ThreadPoolExecutor
def download_ts(ts_url, index):
# 下载逻辑...
return index, content
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(download_ts, url, i) for i, url in enumerate(ts_urls)]
results = [f.result() for f in futures]
大视频下载可能中途失败,可以添加重试机制和断点续传功能。记录已下载的分片序号,下次运行时跳过:
python复制downloaded = set([int(f.split('.')[0]) for f in os.listdir() if f.endswith('.ts')])
for i, ts_url in enumerate(ts_urls):
if i in downloaded:
continue
# 下载逻辑...
虽然可以用copy /b *.ts output.mp4简单合并,但FFmpeg能处理得更专业:
bash复制ffmpeg -f concat -safe 0 -i <(for f in *.ts; do echo "file '$PWD/$f'"; done) -c copy output.mp4
如果不想安装FFmpeg,也可以用Python实现合并。注意要按序号顺序写入:
python复制with open('output.mp4', 'wb') as out:
for i in range(len(ts_urls)):
with open(f'{i}.ts', 'rb') as f:
out.write(f.read())
在实际操作中可能会遇到各种问题,这里分享几个典型问题的解决方案:
下面给出一个整合所有步骤的完整示例:
python复制import requests
import re
import base64
from Crypto.Cipher import AES
from concurrent.futures import ThreadPoolExecutor
import os
class M3U8Downloader:
def __init__(self, video_url):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
self.video_url = video_url
self.ts_urls = []
self.key = None
self.iv = b'0000000000000000'
def get_m3u8_url(self):
# 实现从网页源码解析m3u8地址的逻辑
pass
def process_m3u8(self):
m3u8_content = requests.get(self.m3u8_url, headers=self.headers).text
if '#EXT-X-KEY' in m3u8_content:
self._handle_encryption(m3u8_content)
self._parse_ts_urls(m3u8_content)
def _handle_encryption(self, content):
key_info = re.search(r'#EXT-X-KEY:METHOD=AES-128,URI="(.*?)"', content)
if key_info:
key_url = key_info.group(1)
if not key_url.startswith('http'):
key_url = self.base_url + key_url
key_response = requests.get(key_url)
self.key = base64.b64decode(key_response.content)
def download_ts(self, ts_url, index):
try:
response = requests.get(ts_url, headers=self.headers)
if self.key:
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
data = cipher.decrypt(response.content)
# 处理可能的填充
return index, data[-data[-1]:] if data[-1] <= 16 else data
return index, response.content
except Exception as e:
print(f"下载 {ts_url} 失败: {str(e)}")
return index, None
def run(self):
self.get_m3u8_url()
self.process_m3u8()
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(self.download_ts, url, i)
for i, url in enumerate(self.ts_urls)]
results = [f.result() for f in futures]
# 按顺序保存分片
for index, data in sorted(results, key=lambda x: x[0]):
if data:
with open(f"{index}.ts", 'wb') as f:
f.write(data)
# 合并文件
self.merge_files()
def merge_files(self):
# 实现合并逻辑
pass
这套方案经过多个在线教育平台实测有效,但要注意不同网站可能有细微差异,需要灵活调整。建议先从免费课程试手,熟练掌握后再处理其他场景。