Python实战:解密AES-128加密的m3u8视频流完整指南

📅 2026/7/1 22:49:33
Python实战:解密AES-128加密的m3u8视频流完整指南
1. 项目概述当爬虫撞上加密视频流做爬虫的尤其是搞视频下载的迟早会遇到m3u8这个玩意儿。它本质上就是个播放列表文件告诉你一个视频被切成了多少个小片段ts文件以及这些片段在哪里。这本来是个好事方便流媒体做自适应码率但对爬虫来说麻烦就来了——很多时候这些ts片段是被加密的最常见的就是AES-128加密。你辛辛苦苦把一堆ts文件爬下来结果发现全是乱码播放器根本认不出来那种感觉就像拼图少了一块关键信息。这个项目要解决的就是这个问题。我们将用Python从零开始完整走一遍“发现加密 - 获取密钥 - 解密ts文件 - 合并成完整视频”的全过程。这不仅仅是调用一个库那么简单更重要的是理解m3u8协议的结构、AES加密在流媒体中的应用方式以及如何用编程思维自动化处理整个链条。无论是想下载一些公开课程视频做离线学习还是分析视频流结构这个技能都相当实用。2. 核心原理与协议拆解2.1 m3u8文件结构深度解析m3u8文件是HLSHTTP Live Streaming协议的核心它是一个基于文本的播放列表。理解它的结构是破解加密的第一步。一个典型的加密m3u8文件内容如下#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-KEY:METHODAES-128,URIhttps://example.com/key.key,IV0x1234567890abcdef1234567890abcdef #EXTINF:9.009, segment0.ts #EXTINF:9.009, segment1.ts #EXTINF:5.005, segment2.ts #EXT-X-ENDLIST我们来逐行拆解关键标签#EXTM3U文件头表明这是一个M3U播放列表。#EXT-X-VERSION指定HLS协议版本版本3及以上支持更复杂的加密和编码。#EXT-X-KEY这是整个加密破解的命门所在。它定义了解密方法。METHODAES-128指明加密算法为AES-128这是目前流媒体最常用的加密方式。URI...密钥Key的获取地址。这个URI可能是一个直接的https://链接也可能是一个相对路径。爬虫需要从这里下载密钥文件。这个文件通常是一个16字节128位的二进制文件。IV0x...初始化向量Initialization Vector。AES加密除了需要密钥还需要IV来确保即使相同明文加密出的密文也不同增强安全性。如果这里没有提供IV通常默认使用媒体序列号EXT-X-MEDIA-SEQUENCE作为IV这在早期HLS中很常见。IV也是16字节。注意#EXT-X-KEY标签可能出现在文件开头作用于其后所有片段也可能出现在某个片段之前只作用于其后片段直到下一个#EXT-X-KEY标签出现。这意味着一个m3u8列表里可能存在多次密钥轮换处理时需要动态跟踪当前的解密密钥和IV。#EXTINF表示下一个媒体片段的持续时间秒。segment0.ts媒体片段文件名。爬虫需要根据这个名称或结合URI如果它是相对路径去构造完整的下载链接。#EXT-X-ENDLIST表示播放列表结束这是一个VOD视频点播文件。如果没有这个标签则可能是直播流列表会不断更新。2.2 AES-128在HLS中的加密模式HLS规范中使用的AES-128特指AES-128 CBCCipher Block Chaining模式。理解这一点至关重要因为它直接决定了我们解密时调用的API参数。CBC模式每个明文块在加密前会先与前一个密文块进行异或操作。对于第一个块则使用IV初始化向量进行异或。这意味着解密时我们同样需要密钥和正确的IV。PKCS7填充由于AES是块加密算法要求明文长度是16字节的倍数。ts文件不一定满足因此加密前会对最后一个块进行PKCS7填充。幸运的是Python的cryptography或pycryptodome库在CBC模式下解密时会自动处理去除填充我们一般无需手动干预。密钥长度AES-128的密钥必须是16字节。从URI下载下来的那个.key文件理论上就应该是16字节。如果下载下来的文件长度不对那可能是经过了Base64编码或其他处理需要先解码。为什么是AES-128而不是更安全的AES-256这主要是历史兼容性和性能权衡的结果。HLS协议诞生较早AES-128在当时被认为是安全且高效的足以应对流媒体内容的版权保护需求。对于爬虫而言我们不需要破解AES算法那是几乎不可能的我们的核心任务是“找到并拿到那个密钥”。3. 实战环境搭建与工具选型3.1 Python库的选择与考量处理这个任务我们需要几个核心库requests用于网络请求下载m3u8文件、ts片段和密钥。它的接口简单直观是Python爬虫的标配。cryptography或pycryptodome用于AES解密。两者都是优秀的加密库。pycryptodome是久经考验的pycrypto库的维护分支API对于处理这种原始字节的加解密非常直接。cryptography更现代被许多大型项目使用安全性有保障。选择建议对于这个项目两者皆可。本文示例将使用pycryptodome因为它AES.new(key, mode, iv)的API非常清晰。安装命令pip install pycryptodome requestsre / json用于解析m3u8文本内容提取关键信息。正则表达式re是处理这类文本的利器。concurrent.futures或asyncio/aiohttp用于加速ts片段的并发下载。考虑到新手友好度我们将使用concurrent.futures中的ThreadPoolExecutor它接口简单能有效利用网络I/O的等待时间。3.2 开发环境与调试工具IDE/编辑器VSCode、PyCharm均可。确保配置好Python环境。网络调试工具浏览器开发者工具F12的“网络”Network选项卡是你的眼睛。播放一个m3u8视频在这里你能清晰地看到m3u8文件请求、一个个ts文件的请求以及关键的key.key文件的请求。你可以直接复制这些请求的URL、请求头Headers用于编写爬虫代码。二进制查看工具可选如hexdump或VSCode的Hex Editor插件。当你下载密钥或ts文件后可以用它查看文件头确认是否是二进制数据或者检查文件长度。一个关键的实操心得在开始写爬虫代码前务必先用浏览器或播放器如VLC成功播放一次目标视频。这能证明视频流本身是可访问的并且你能在开发者工具里捕获到所有必要的请求信息。如果播放器都播不了那很可能是服务器做了更复杂的验证如Referer、User-Agent、Cookie甚至Token爬虫的难度会指数级上升。4. 核心代码实现与分步解析4.1 第一步解析m3u8文件提取密钥信息这是整个流程的指挥中心。我们需要编写一个函数输入m3u8文件的URL或内容输出一个结构化的信息包含ts片段列表和对应的解密信息。import re import requests def parse_m3u8(m3u8_url): 解析m3u8文件提取ts片段链接和加密信息。 返回 (ts_url_list, key_info) key_info 为字典 {method: AES-128, uri: ..., iv: ...(十六进制字符串或None)} headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(m3u8_url, headersheaders) resp.raise_for_status() content resp.text ts_url_list [] key_info {method: None, uri: None, iv: None} base_url /.join(m3u8_url.split(/)[:-1]) / # 用于处理相对路径 lines content.split(\n) i 0 while i len(lines): line lines[i].strip() if line.startswith(#EXT-X-KEY): # 解析 METHOD, URI, IV # 示例: #EXT-X-KEY:METHODAES-128,URIkey.key,IV0x123... method_match re.search(rMETHOD([^,]), line) uri_match re.search(rURI([^]), line) iv_match re.search(rIV([^,]), line) if method_match: key_info[method] method_match.group(1) if uri_match: key_uri uri_match.group(1) # 处理URI如果是相对路径则拼接基础URL if key_uri.startswith(http): key_info[uri] key_uri else: key_info[uri] base_url key_uri if iv_match: iv_str iv_match.group(1) # IV可能是 0x... 格式需要去除0x并转换为字节 if iv_str.startswith(0x): key_info[iv] iv_str[2:] # 先保存十六进制字符串后续转换 else: # 有时IV是十进制数字字符串需要特殊处理较少见 key_info[iv] iv_str elif line.startswith(#EXTINF): # 下一行就是ts文件名 i 1 ts_line lines[i].strip() if ts_line and not ts_line.startswith(#): if ts_line.startswith(http): ts_url ts_line else: ts_url base_url ts_line ts_url_list.append(ts_url) i 1 return ts_url_list, key_info代码解析与注意事项base_url的构造这是处理相对路径的关键。假设m3u8地址是https://example.com/video/playlist.m3u8那么base_url就是https://example.com/video/。对于URIkey.key拼接后就是https://example.com/video/key.key。IV的处理代码中先将IV的十六进制字符串保存下来。因为IV在解密时需要转换为字节对象这个转换我们放在下载密钥后统一进行。错误处理这里只做了最简单的raise_for_status()实际项目中需要增加重试、超时、日志等逻辑。4.2 第二步下载并处理密钥与IV获取到密钥URI和IV信息后我们需要下载密钥并将IV字符串转换为可用的字节。from Crypto.Util.Padding import pad, unpad from Crypto.Cipher import AES import codecs def download_and_prepare_key(key_info): 根据key_info下载密钥并准备解密所需的key和iv字节对象。 返回: (key_bytes, iv_bytes) if key_info[method] ! AES-128: raise ValueError(f不支持的加密方法: {key_info[method]}) # 1. 下载密钥 key_url key_info[uri] resp requests.get(key_url) resp.raise_for_status() key_content resp.content # 注意这里是.content获取二进制数据 # 密钥文件通常是16字节。有时可能是base64编码的文本需要判断。 if len(key_content) 16: key_bytes key_content else: # 尝试解码为base64 try: # 如果服务器返回的是文本形式的base64 key_text resp.text.strip() key_bytes codecs.decode(key_text.encode(), base64) except: # 如果还不是可能就需要根据实际情况处理这里先抛出异常 raise ValueError(无法识别的密钥格式长度不是16字节且非标准Base64) # 2. 处理IV iv_hex key_info.get(iv) if iv_hex: # 将十六进制字符串转换为字节 # 确保字符串长度为3216字节*2不足补0 iv_hex iv_hex.zfill(32) iv_bytes bytes.fromhex(iv_hex) else: # 如果m3u8中没有指定IV则默认使用媒体序列号EXT-X-MEDIA-SEQUENCE作为IV。 # 更常见的HLS实现是使用全0的IV。 # 根据HLS规范当没有IV时应使用加密片段的序列号一个32位无符号整数作为IV。 # 但在实际爬虫中遇到无IV且使用序列号的情况较少。很多简单加密直接使用全0 IV。 # 这里我们提供一个常见处理先尝试全0 IV。 print(警告m3u8中未指定IV尝试使用全零IV。如果解密失败请检查是否需要使用片段索引作为IV。) iv_bytes b\x00 * 16 # 验证长度 if len(key_bytes) ! 16: raise ValueError(f密钥长度错误: {len(key_bytes)} 字节应为16字节) if len(iv_bytes) ! 16: raise ValueError(fIV长度错误: {len(iv_bytes)} 字节应为16字节) return key_bytes, iv_bytes关键点与避坑指南密钥格式这是最容易出错的地方。90%的情况下.key文件就是16字节的二进制数据。但有些服务器会返回Base64编码的文本。代码中做了简单判断先看长度是否为16如果不是尝试Base64解码。如果还不行就需要你手动检查下载下来的文件内容用文本编辑器或print(repr(key_content[:50]))然后调整处理逻辑。IV缺失如果m3u8里没有IV情况就复杂一些。根据HLS规范应使用媒体序列号#EXT-X-MEDIA-SEQUENCE作为IV但很多实现图省事直接用全0。我们的代码先尝试全0。如果解密出来的视频开头花屏或完全无法播放大概率是IV不对。此时你需要检查m3u8文件是否有#EXT-X-MEDIA-SEQUENCE标签。如果有尝试用该序列号转换为8字节十六进制并左补零至32位作为IV。如果还不行可能需要研究特定网站的实现或者用已知的第一个ts片段明文如果有的话去反推IV。4.3 第三步并发下载TS片段直接串行下载几十上百个ts文件会非常慢。我们需要使用并发。import os from concurrent.futures import ThreadPoolExecutor, as_completed def download_ts_segments(ts_url_list, download_dir./ts_files, max_workers10): 并发下载所有ts片段到指定目录。 返回下载成功的本地文件路径列表。 if not os.path.exists(download_dir): os.makedirs(download_dir) downloaded_files [] headers {User-Agent: Mozilla/5.0 ...} def download_one(ts_url, index): try: filename os.path.join(download_dir, fsegment_{index:04d}.ts) resp requests.get(ts_url, headersheaders, timeout30) resp.raise_for_status() with open(filename, wb) as f: f.write(resp.content) print(f[OK] 下载完成: {filename}) return filename except Exception as e: print(f[Failed] 下载失败 {ts_url}: {e}) return None with ThreadPoolExecutor(max_workersmax_workers) as executor: # 提交所有下载任务 future_to_index {executor.submit(download_one, url, idx): idx for idx, url in enumerate(ts_url_list)} for future in as_completed(future_to_index): result future.result() if result: downloaded_files.append(result) # 按索引排序确保后续合并顺序正确 downloaded_files.sort() return downloaded_files实操心得max_workers不宜设置过大通常10-20即可。过大可能导致对方服务器拒绝连接或本地网络拥堵。一定要为每个ts文件按顺序命名如segment_0000.ts因为后续解密和合并必须严格按照m3u8中的顺序进行。加入超时(timeout)和重试机制是生产级代码必备的这里为简化省略但你应当实现它例如使用tenacity库。4.4 第四步解密TS片段这是最核心的一步使用下载好的密钥和IV对每一个加密的ts文件进行AES-128-CBC解密。def decrypt_ts_file(encrypted_ts_path, decrypted_ts_path, key_bytes, iv_bytes): 解密单个ts文件。 with open(encrypted_ts_path, rb) as f: ciphertext f.read() # 创建AES解密器 cipher AES.new(key_bytes, AES.MODE_CBC, iviv_bytes) try: plaintext cipher.decrypt(ciphertext) # 注意CBC解密后可能需要去除PKCS7填充。但ts文件加密时可能整个文件或每个188/192字节的包是块对齐的。 # 一个更稳妥的做法先解密如果最后一块解密后无法播放再尝试unpad。 # 对于TS流通常不需要unpad直接写入。 except Exception as e: print(f解密过程出错 {encrypted_ts_path}: {e}) return False with open(decrypted_ts_path, wb) as f: f.write(plaintext) return True def batch_decrypt_ts(downloaded_files, key_bytes, iv_bytes, decrypted_dir./decrypted_files): 批量解密所有ts文件。 if not os.path.exists(decrypted_dir): os.makedirs(decrypted_dir) decrypted_files [] for idx, enc_file in enumerate(downloaded_files): dec_file os.path.join(decrypted_dir, fdecrypted_{idx:04d}.ts) print(f正在解密 {enc_file} - {dec_file}) if decrypt_ts_file(enc_file, dec_file, key_bytes, iv_bytes): decrypted_files.append(dec_file) else: print(f解密失败: {enc_file}) # 可以选择退出或跳过 return decrypted_files关于填充Padding的深度解析 这里是一个巨大的坑。理论上CBC模式需要处理PKCS7填充。但TS流有其特殊性TS文件是MPEG-2 Transport Stream的容器其基本单位是188字节的包有时是192字节含4字节时间戳。加密通常是以这些“包”为单位进行的而不是整个文件。因此加密后的ts文件长度很可能本身就是16字节的倍数末尾没有填充。如果对整个文件调用unpad可能会因为末尾几个字节恰好构成合法的PKCS7填充而被错误去除导致文件损坏。最佳实践先直接解密并保存不进行unpad。然后用播放器如VLC或ffmpeg尝试播放解密后的单个ts文件。如果能播放说明没问题如果最后一点无法播放或报错再考虑对解密后的数据尝试unpad。代码中我们采用了保守策略直接写入解密数据。4.5 第五步合并与最终输出解密后的ts文件只是视频片段我们需要将它们合并成一个完整的视频文件如MP4。def merge_ts_to_mp4(decrypted_files, output_pathoutput.mp4): 将解密后的ts文件合并为mp4。 使用ffmpeg进行合并能处理编码问题更可靠。 # 方法一简单二进制合并仅当所有ts编码完全相同时可行 # with open(output_path, wb) as outfile: # for fname in decrypted_files: # with open(fname, rb) as infile: # outfile.write(infile.read()) # print(f已合并至: {output_path}) # return output_path # 方法二使用ffmpeg推荐 # 首先创建一个包含所有文件路径的文本文件 list_file file_list.txt with open(list_file, w, encodingutf-8) as f: for file in decrypted_files: # ffmpeg要求路径使用正斜杠且最好用引号括起来避免空格问题 file_path file.replace(\\, /) f.write(ffile {file_path}\n) import subprocess # 构建ffmpeg命令 # -f concat: 指定concat分离器 # -safe 0: 允许使用绝对路径 # -c copy: 流复制模式不重新编码速度极快 cmd [ ffmpeg, -f, concat, -safe, 0, -i, list_file, -c, copy, output_path ] try: print(f正在使用ffmpeg合并文件...) subprocess.run(cmd, checkTrue, capture_outputTrue, textTrue) print(f合并成功: {output_path}) # 清理临时列表文件 os.remove(list_file) return output_path except subprocess.CalledProcessError as e: print(fffmpeg合并失败: {e.stderr}) os.remove(list_file) return None except FileNotFoundError: print(错误未找到ffmpeg。请先安装ffmpeg并确保其在系统PATH中。) print(可以前往 https://ffmpeg.org/download.html 下载安装。) os.remove(list_file) return None为什么强烈推荐使用ffmpeg合并编码处理即使ts文件都是H.264AAC其参数如SPS/PPS也可能有细微差别。直接二进制拼接可能导致播放器只在第一个片段找到编码头后续片段无法解码。ffmpeg的concat协议能正确处理这些元数据。容器转换-c copy参数进行“流复制”不进行耗时的重新编码只是将视频和音频流从TS容器重新封装到MP4容器速度飞快且无损。容错性ffmpeg能处理一些不规范的ts流而直接合并对文件要求非常严格。5. 完整流程串联与主函数现在我们把所有步骤串联起来形成一个完整的脚本。def main(m3u8_url, output_videofinal_video.mp4): print(步骤1: 解析m3u8文件...) ts_url_list, key_info parse_m3u8(m3u8_url) if not key_info[method]: print(提示该m3u8流未加密可直接下载合并。) # 这里可以走非加密的下载流程省略... return elif key_info[method] ! AES-128: print(f暂不支持的解密方法: {key_info[method]}) return print(f发现加密: {key_info}) print(步骤2: 下载并准备密钥...) key_bytes, iv_bytes download_and_prepare_key(key_info) print(f密钥长度: {len(key_bytes)} IV: {iv_bytes.hex()}) print(步骤3: 并发下载TS片段...) downloaded_files download_ts_segments(ts_url_list, download_dir./ts_temp) print(f共下载 {len(downloaded_files)}/{len(ts_url_list)} 个片段。) print(步骤4: 批量解密TS片段...) decrypted_files batch_decrypt_ts(downloaded_files, key_bytes, iv_bytes, decrypted_dir./decrypted_temp) print(步骤5: 合并为MP4...) final_video merge_ts_to_mp4(decrypted_files, output_pathoutput_video) if final_video: print(f\n 视频下载并解密完成: {final_video}) # 可选清理临时文件 # import shutil # shutil.rmtree(./ts_temp) # shutil.rmtree(./decrypted_temp) else: print(\n❌ 处理失败。) if __name__ __main__: # 使用示例将下面的URL替换为实际的m3u8地址 # 请务必遵守相关法律法规和网站的使用条款仅用于学习测试。 test_m3u8_url https://example.com/path/to/your/playlist.m3u8 main(test_m3u8_url, output_videomy_decrypted_video.mp4)6. 常见问题排查与进阶技巧6.1 问题排查清单问题现象可能原因排查步骤与解决方案requests下载失败403/404服务器检查请求头如User-Agent,Referer,Origin。1. 复制浏览器中成功请求的Headers尤其是User-Agent和Referer到爬虫代码中。2. 有些网站需要Cookie或特定的Authorization头需要模拟登录或从浏览器复制。密钥下载后长度不是16字节密钥可能是Base64编码的文本或经过了其他包装。1.print(repr(key_content[:50]))查看内容。如果像bABCDEFG\\n可能是文本。2. 尝试key_bytes base64.b64decode(key_content)。3. 如果看起来像JSON如b{key:...}则需要用json.loads()解析后再提取并解码。解密后的视频花屏、绿屏或无法播放IV不正确是最常见的原因。1. 确认m3u8中是否有IV。没有则尝试全0 IV。2. 如果全0 IV不行检查#EXT-X-MEDIA-SEQUENCE。假设序列号是0IV应为32位十六进制00000000000000000000000000000000。序列号是1则为00000000000000000000000000000001。3.终极调试法找一个已知的、未加密的ts片段如果有的话和加密的对应片段用工具或脚本尝试暴力测试几种常见的IV生成方式。解密过程报ValueError: Invalid padding bytes解密后尝试unpad时末尾字节不符合PKCS7规范。大概率不需要unpad。注释掉解密函数中的unpad代码直接写入解密后的数据。TS流通常块对齐。合并后的视频只有声音没有画面或只有开头能播TS片段直接二进制合并编码头信息有问题。必须使用ffmpeg合并。使用-c copy参数进行流复制式合并不要用cat命令或简单的文件写入拼接。下载速度慢单线程下载。使用ThreadPoolExecutor并发下载如代码所示。注意控制并发数(max_workers)避免被封IP。中途某个ts片段下载失败网络波动或服务器临时问题。实现重试机制。可以为download_one函数添加重试逻辑例如使用tenacity库或在函数内用for _ in range(3): try ... except ...。6.2 进阶技巧与优化处理动态变化的m3u8直播流直播流的m3u8文件没有#EXT-X-ENDLIST且内容会不断更新。爬虫需要循环请求m3u8解析出新的ts片段并下载。注意处理#EXT-X-MEDIA-SEQUENCE的递增和旧片段的清理。多码率自适应流选择主m3u8可能包含多个子m3u8链接不同分辨率/码率。你需要先解析主列表然后选择其中一个子列表如分辨率最高的进行上述流程。断点续传与状态保存对于长视频可以将已下载的ts片段索引记录到文件或数据库。重启脚本时先检查本地已有文件跳过已下载的实现断点续传。更健壮的密钥获取有些网站的密钥URI是动态生成的每次访问都不同甚至需要携带一个有时间限制的Token。你需要分析浏览器中获取密钥的完整请求链可能涉及额外的API请求和参数计算。使用专业库对于复杂的HLS流处理可以考虑使用m3u8这个Python库来解析m3u8文件它能更规范地处理各种标签和属性。6.3 法律与道德边界最后必须强调技术是一把双刃剑。遵守robots.txt在爬取前检查目标网站的robots.txt文件尊重网站管理员的意愿。控制请求频率在代码中增加延时如time.sleep(0.5)避免对目标服务器造成过大压力影响其正常服务。高并发请求是导致IP被封锁的主要原因。明确用途本技术仅用于学习网络协议、安全知识以及下载你拥有观看权限的视频用于个人离线观看如已购买的网络课程。严禁用于盗版、侵犯版权或其他非法用途。尊重版权破解加密技术是为了理解原理不应成为侵犯内容创作者权益的工具。整个流程走下来你会发现破解m3u8的AES-128加密核心不在于密码学攻防而在于对HTTP协议、HLS规范以及目标网站具体实现的细致分析。从抓包分析请求到处理各种边缘情况如奇怪的密钥格式、缺失的IV每一步都需要耐心和严谨的调试。把这个流程吃透你不仅能下载视频更能深刻理解现代流媒体传输与保护的基本逻辑。