第一次接触m3u8文件时,我完全被那些以#EXT开头的奇怪标签搞懵了。后来才发现,这其实就是个播放列表文件,就像我们去餐厅点餐时的菜单一样。HLS(HTTP Live Streaming)协议把完整的视频切成很多小片段(.ts文件),然后通过m3u8这个"菜单"告诉播放器应该按什么顺序"上菜"。
m3u8文件分为两种常见类型:
我遇到过最头疼的情况是某些平台会动态更换密钥,这时候就需要实时获取最新的密钥信息。有一次为了下载一个教学视频,我花了整整一晚上研究如何自动获取和更新密钥,最后发现其实只要在请求头里加上Referer字段就能解决。
工欲善其事,必先利其器。我建议使用Python 3.8+版本,太老的版本可能会遇到依赖问题。先创建一个干净的虚拟环境:
bash复制python -m venv m3u8_env
source m3u8_env/bin/activate # Linux/Mac
m3u8_env\Scripts\activate # Windows
然后安装核心依赖库:
bash复制pip install m3u8 requests pycryptodome
这里有个坑要注意:pycryptodome是加密解密的核心库,但有些系统可能已经安装了老版本的pycrypto,这会导致冲突。如果遇到解密失败的问题,可以先卸载旧版本:
bash复制pip uninstall pycrypto
使用m3u8库解析文件比想象中简单很多。我最常用的几种场景:
场景1:直接解析URL
python复制import m3u8
playlist = m3u8.load('http://example.com/playlist.m3u8')
场景2:处理本地文件
python复制with open('local.m3u8', 'r') as f:
content = f.read()
playlist = m3u8.loads(content)
场景3:处理加密内容
python复制if playlist.keys and playlist.keys[0]:
key_info = playlist.keys[0]
print(f"加密方式: {key_info.method}")
print(f"密钥地址: {key_info.uri}")
print(f"初始向量: {key_info.iv}")
我经常用这个技巧批量检查视频是否加密。有时候会遇到URI是相对路径的情况,这时候需要手动拼接基础URL:
python复制base_url = 'http://example.com/path/'
key_url = base_url + key_info.uri.split('/')[-1]
下载大量小文件最怕速度慢和连接断开。经过多次测试,我总结出三种可靠方案:
方案1:单线程+重试机制
python复制def download_ts(url, retry=3):
for i in range(retry):
try:
resp = requests.get(url, timeout=10)
return resp.content
except Exception as e:
print(f"第{i+1}次尝试失败: {e}")
return None
方案2:多线程加速
python复制from concurrent.futures import ThreadPoolExecutor
def batch_download(urls):
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(download_ts, urls))
return results
方案3:异步IO(适合高并发)
python复制import aiohttp
import asyncio
async def async_download(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.read()
async def main(urls):
tasks = [async_download(url) for url in urls]
return await asyncio.gather(*tasks)
实测下来,对于100个以上的小文件,方案3的速度能比方案1快10倍以上。但要注意服务器可能会限制并发数,这时候需要适当控制并发量。
遇到加密视频别慌,按照这个步骤来:
python复制key_url = playlist.keys[0].uri
key_content = requests.get(key_url).content
python复制from Crypto.Cipher import AES
def decrypt_ts(encrypted_data, key, iv=None):
cipher = AES.new(key, AES.MODE_CBC, iv=iv if iv else key)
return cipher.decrypt(encrypted_data)
python复制def remove_padding(data, block_size=16):
padding_len = data[-1]
return data[:-padding_len]
我遇到过最棘手的情况是IV值动态变化,这时候需要从每个片段的EXT-X-KEY标签中实时获取IV值。解决方案是重写m3u8的解析逻辑,但这已经属于进阶技巧了。
合并文件看似简单,但有几个坑我踩过:
坑1:文件顺序错乱
解决方法是用数字前缀命名:
python复制for i, segment in enumerate(playlist.segments):
filename = f"{i:05d}.ts" # 00001.ts格式
坑2:内存溢出
大视频应该分块写入:
python复制with open('output.mp4', 'wb') as out_f:
for ts_file in sorted(glob.glob('*.ts')):
with open(ts_file, 'rb') as in_f:
while chunk := in_f.read(1024*1024): # 每次1MB
out_f.write(chunk)
坑3:格式不兼容
有时候直接合并的MP4无法播放,需要转码:
bash复制ffmpeg -i input.ts -c copy output.mp4
下面是我在实际项目中打磨过的代码框架:
python复制import os
import requests
import m3u8
from Crypto.Cipher import AES
from concurrent.futures import ThreadPoolExecutor
class M3U8Downloader:
def __init__(self, url, output='output.mp4'):
self.base_url = self._get_base_url(url)
self.output = output
self.temp_dir = 'temp_ts'
os.makedirs(self.temp_dir, exist_ok=True)
def _get_base_url(self, url):
return '/'.join(url.split('/')[:-1]) + '/'
def download_ts(self, segment, index):
ts_url = segment.uri if segment.uri.startswith('http') else self.base_url + segment.uri
try:
content = requests.get(ts_url, timeout=10).content
if segment.key and segment.key.method == 'AES-128':
key = requests.get(segment.key.uri).content
iv = segment.key.iv or key
content = AES.new(key, AES.MODE_CBC, iv).decrypt(content)
path = os.path.join(self.temp_dir, f"{index:05d}.ts")
with open(path, 'wb') as f:
f.write(content)
return True
except Exception as e:
print(f"下载失败 {ts_url}: {e}")
return False
def run(self):
playlist = m3u8.load(self.base_url + 'playlist.m3u8')
with ThreadPoolExecutor(max_workers=10) as executor:
tasks = [executor.submit(self.download_ts, seg, i)
for i, seg in enumerate(playlist.segments)]
[task.result() for task in tasks]
self._merge_files()
def _merge_files(self):
with open(self.output, 'wb') as out_f:
for ts in sorted(os.listdir(self.temp_dir)):
with open(os.path.join(self.temp_dir, ts), 'rb') as in_f:
out_f.write(in_f.read())
if __name__ == '__main__':
downloader = M3U8Downloader('http://example.com/path/playlist.m3u8')
downloader.run()
这个框架的特点是:
问题1:下载速度慢
python复制headers = {
'User-Agent': 'Mozilla/5.0',
'Referer': 'http://original-site.com/'
}
问题2:解密失败
问题3:合并后无法播放
bash复制ffmpeg -i output.mp4
bash复制ffmpeg -i broken.mp4 -c:v libx264 -c:a aac fixed.mp4
问题4:403禁止访问
记得在处理任何视频时都要遵守版权规定,只下载有权限访问的内容。我曾经因为一个项目需要分析教育视频,特意联系了版权方获得了书面授权。技术很强大,但我们要用在正确的地方。