网页视频下载实战:m3u8、ts分片与AES-128解密全解析
发布时间:2026/9/26 1:45:48来源:尧图网络
1. 网页视频下载实战的核心链路拆解1.1 为什么m3u8是绕不开的第一道坎做过网页视频下载的人都有一个共识直接右键另存为的时代早就过去了。现在打开开发者工具的Network面板筛选Media或者XHR十有八九会看到一个后缀是.m3u8的请求。这个文件本身不是视频它是一份索引清单用纯文本记录了一串.ts分片文件的地址。你可以把它理解成一本菜谱的目录页——告诉你这道菜由哪些步骤组成但真正的内容在后面的每一页里。m3u8的本质是HLSHTTP Live Streaming协议的播放列表格式。它的设计初衷是自适应码率同一个视频源可以生成多套不同清晰度的m3u8播放器根据当前网速自动切换。这个机制对观看体验是好事但对下载来说就多了一层判断——你得先确定自己要下的是哪一档清晰度再去拿对应的索引文件。我一般会先看m3u8的内容结构。用浏览器直接打开那个链接或者用curl拉下来看一眼curl -s https://example.com/video/index.m3u8 -o index.m3u8 cat index.m3u8如果看到的是#EXT-X-STREAM-INF开头的多行记录说明这是主播放列表master playlist里面列出了不同码率的子播放列表地址。如果看到的是#EXTINF后面跟着一串.ts文件名那就是媒体播放列表media playlist可以直接进入下一步。注意有些站点的m3u8链接带有时间戳签名或者token参数过期后会返回403。遇到这种情况需要重新从页面抓取最新的链接不要死磕旧地址。1.2 ts分片到底是什么为什么不能直接改后缀很多人第一次接触ts文件时会想不就是视频分片吗把后缀改成mp4不就行了实测下来单个ts分片改后缀确实能播放但只能播那几秒钟的内容。因为ts是MPEG-TSTransport Stream格式它的设计目标是在不可靠信道上传输每个分片都自带独立的包头和时钟信息。多个ts分片之间需要按顺序拼接并且要处理时间戳连续性问题才能形成完整的视频流。ts分片的命名通常是有规律的比如video_000.ts、video_001.ts或者带时间戳的segment_1609459200.ts。在m3u8索引里它们的顺序就是播放顺序。有些站点会对ts分片做额外的处理比如在文件头插入自定义数据、对分片进行二次压缩甚至把真正的视频数据藏在图片文件里。这些情况需要具体分析但大多数常规站点的ts分片就是标准的MPEG-TS流。我踩过的一个坑是某些站点的ts分片虽然能下载但每个分片开头有一段空白数据直接合并后播放器会卡在开头几秒。后来发现是分片里包含了ID3元数据标签需要用ffmpeg的-map参数过滤掉非视频流。这个后面在合并环节会详细说。1.3 AES-128加密的识别与判断当m3u8索引里出现#EXT-X-KEY:METHODAES-128,URI...这一行时说明ts分片被加密了。这是HLS协议支持的标准加密方式密钥通过一个独立的URI提供。AES-128指的是使用128位密钥的AES加密算法在HLS场景下通常采用CBC模式每个分片有独立的IV初始化向量。判断是否加密很简单看m3u8里有没有#EXT-X-KEY标签。如果有就需要先拿到密钥文件。密钥的URI可能是相对路径也可能是绝对路径如果是相对路径需要相对于m3u8文件的URL来拼接。比如m3u8在https://example.com/video/index.m3u8密钥URI是key.bin那完整地址就是https://example.com/video/key.bin。密钥文件通常是一个16字节的二进制文件。用xxd或者hexdump看一眼curl -s https://example.com/video/key.bin -o key.bin xxd key.bin如果看到的是16个字节的十六进制数据那就是标准的AES-128密钥。有些站点会对密钥做额外保护比如要求特定的Referer头或者Cookie才能下载这时候需要在请求时带上对应的头部信息。提示AES-128的IV通常有两种来源——一种是m3u8里明确指定的IV0x...另一种是默认使用分片的序列号作为IV。后者在解密时容易出错因为序列号的字节序和填充方式需要严格按规范处理。2. 工具选型与环境准备2.1 ffmpeg为什么是首选以及它的安装方式处理m3u8下载和ts合并ffmpeg几乎是最省心的选择。它原生支持HLS协议能直接读取m3u8索引、自动下载ts分片、处理AES-128解密、合并输出为mp4。一条命令就能完成从索引到成品的全流程不需要手动写脚本拼接。ffmpeg的安装方式取决于操作系统。Windows用户可以去官网下载编译好的压缩包解压后把bin目录加到系统PATH里。macOS用户用Homebrew最方便brew install ffmpegLinux用户根据发行版选择包管理器# Ubuntu/Debian sudo apt install ffmpeg # CentOS/RHEL sudo yum install ffmpeg安装完成后验证一下ffmpeg -version能正常输出版本信息就说明环境没问题。我建议用较新的版本因为旧版本对某些HLS特性的支持不完整比如#EXT-X-MAP标签的处理。2.2 直接下载与手动下载的取舍ffmpeg直接处理m3u8是最省事的方案但并不是所有场景都适用。如果站点有严格的反爬机制比如检测请求频率、校验Cookie、或者对ts分片做了额外的混淆ffmpeg可能会失败。这时候就需要手动下载ts分片再用ffmpeg合并。手动下载的好处是可控性强。你可以自己控制请求间隔、自定义请求头、对每个分片做单独处理。缺点是工作量大尤其是分片数量多的时候。我一般会先尝试ffmpeg直接处理如果失败再切换到手动模式。手动下载需要准备一个分片地址列表。可以从m3u8文件里提取也可以用脚本解析。提取出来的地址通常是相对路径需要拼接成完整URL。我习惯用Python写个小脚本来做这件事因为后续的解密和合并也需要脚本配合。2.3 辅助工具清单除了ffmpeg还有几个工具在实际操作中很有用curl/wget下载m3u8索引、密钥文件、ts分片。curl的优势是能灵活设置请求头wget的优势是支持断点续传。Python写自动化脚本处理批量下载、解密、合并。requests库处理HTTP请求pycryptodome库处理AES解密。xxd/hexdump查看密钥文件的二进制内容确认密钥长度和格式。VLC/PotPlayer验证下载的ts分片是否能正常播放快速定位问题。浏览器开发者工具抓取m3u8地址、分析请求头、查看网络请求的详细信息。这些工具的组合使用能覆盖绝大多数m3u8下载场景。我个人的习惯是先用浏览器开发者工具定位m3u8地址用curl验证可访问性然后用ffmpeg尝试直接下载失败则切换到Python脚本手动处理。3. 全链路实操从m3u8定位到ts合并3.1 定位m3u8地址的三种方法第一种方法最直接打开浏览器开发者工具切换到Network面板刷新页面开始播放视频在筛选框里输入m3u8。通常会出现一个或多个请求找到返回内容为m3u8格式的那个。如果页面用的是HLS播放器这个请求一定会出现。第二种方法适合m3u8地址被隐藏的情况在Elements面板里搜索m3u8关键词有时候地址会写在JavaScript变量或者>curl -I https://example.com/video/index.m3u8看返回的状态码和Content-Type。正常应该是200Content-Type可能是application/vnd.apple.mpegurl或者application/x-mpegURL。如果是403说明需要带上特定的请求头比如Referer或者User-Agent。3.2 解析m3u8索引与提取ts地址拿到m3u8文件后先看它的结构。一个典型的媒体播放列表长这样#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-KEY:METHODAES-128,URIkey.bin,IV0x00000000000000000000000000000000 #EXTINF:10.000000, segment_000.ts #EXTINF:10.000000, segment_001.ts #EXTINF:10.000000, segment_002.ts #EXT-X-ENDLIST#EXTINF后面的数字是分片时长下一行是分片文件名。#EXT-X-KEY说明有加密URI是密钥地址IV是初始化向量。#EXT-X-ENDLIST表示这是完整的播放列表不是直播流。提取ts地址可以用简单的文本处理import re with open(index.m3u8, r) as f: content f.read() # 提取ts文件名 ts_files re.findall(r^([^#\n].*\.ts.*)$, content, re.MULTILINE) # 提取密钥信息 key_info re.search(r#EXT-X-KEY:METHODAES-128,URI([^])(?:,IV0x([0-9a-fA-F]))?, content) if key_info: key_uri key_info.group(1) iv key_info.group(2) if key_info.group(2) else None print(f密钥地址: {key_uri}) print(fIV: {iv})这段代码能提取出ts文件名列表和密钥信息。如果m3u8里没有#EXT-X-KEY说明没有加密可以直接进入下载环节。3.3 下载密钥与ts分片密钥的下载需要注意请求头。有些站点会校验Referer需要带上m3u8所在页面的地址curl -H Referer: https://example.com/video/page \ -H User-Agent: Mozilla/5.0 ... \ https://example.com/video/key.bin -o key.bints分片的下载类似但数量多建议用脚本批量处理。下面是一个Python脚本示例import requests import os import time base_url https://example.com/video/ ts_files [segment_000.ts, segment_001.ts, segment_002.ts] headers { Referer: https://example.com/video/page, User-Agent: Mozilla/5.0 ... } os.makedirs(ts_files, exist_okTrue) for i, ts in enumerate(ts_files): url base_url ts filepath os.path.join(ts_files, ts) if os.path.exists(filepath): print(f已存在跳过: {ts}) continue try: resp requests.get(url, headersheaders, timeout30) resp.raise_for_status() with open(filepath, wb) as f: f.write(resp.content) print(f下载完成: {ts} ({len(resp.content)} bytes)) except Exception as e: print(f下载失败: {ts} - {e}) time.sleep(0.5) # 控制请求频率这个脚本做了几件事检查文件是否已存在支持断点续传、设置请求头、控制请求间隔避免触发反爬、记录下载状态。time.sleep(0.5)是我实测下来比较稳妥的间隔太快容易被封太慢效率低。注意如果分片数量超过100个建议分批下载每批之间休息几秒。有些站点会对短时间内的大量请求做限流返回429状态码。3.4 AES-128解密的两种实现方式如果ts分片被加密了下载下来的文件是密文直接合并会得到无法播放的结果。解密需要密钥和IV。用Python的pycryptodome库实现from Crypto.Cipher import AES from Crypto.Util.Padding import unpad def decrypt_ts(encrypted_data, key, iv): cipher AES.new(key, AES.MODE_CBC, iv) decrypted cipher.decrypt(encrypted_data) # 去除PKCS7填充 try: decrypted unpad(decrypted, AES.block_size) except ValueError: pass # 某些分片可能没有填充 return decrypted # 读取密钥 with open(key.bin, rb) as f: key f.read() # IV处理如果m3u8里指定了IV用指定的否则用分片序列号 # 序列号是分片在播放列表中的索引从0开始 def get_iv(sequence_num): # IV是16字节序列号放在最后8字节大端序 return sequence_num.to_bytes(16, byteorderbig) # 解密单个分片 with open(ts_files/segment_000.ts, rb) as f: encrypted f.read() iv get_iv(0) # 第一个分片 decrypted decrypt_ts(encrypted, key, iv) with open(ts_files/segment_000_dec.ts, wb) as f: f.write(decrypted)这里有个关键点IV的生成方式。如果m3u8里明确写了IV0x...就用那个值。如果没有写就按HLS规范用分片序列号生成。序列号是分片在播放列表中的位置从0开始计数。生成IV时要把序列号转成16字节的大端序字节串。另一种方式是直接用ffmpeg解密。ffmpeg能自动读取m3u8里的密钥信息并解密不需要手动处理ffmpeg -headers Referer: https://example.com/video/page\r\n \ -i https://example.com/video/index.m3u8 \ -c copy output.mp4-headers参数用来传递请求头-c copy表示直接复制流不重新编码。如果m3u8里的密钥地址是相对路径ffmpeg会自动拼接。这种方式最省事但前提是ffmpeg能正常访问密钥和分片。3.5 ts合并的三种方案对比解密完成后需要把所有ts分片合并成一个完整的视频文件。有三种常用方案方案一ffmpeg concat协议创建一个文本文件列出所有ts分片的路径file ts_files/segment_000_dec.ts file ts_files/segment_001_dec.ts file ts_files/segment_002_dec.ts然后用ffmpeg合并ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4-safe 0允许使用绝对路径-c copy直接复制流。这种方式速度快不重新编码画质无损。方案二直接二进制拼接把所有ts文件的二进制内容按顺序拼接cat ts_files/*_dec.ts merged.ts然后转成mp4ffmpeg -i merged.ts -c copy output.mp4这种方式最简单但要求ts分片的时间戳是连续的。如果分片来自不同的编码会话时间戳可能不连续导致播放器卡顿。方案三ffmpeg直接处理m3u8如果ffmpeg能直接访问m3u8和密钥一条命令搞定ffmpeg -i https://example.com/video/index.m3u8 -c copy output.mp4这是最省事的方案适合没有反爬机制的站点。三种方案的对比方案优点缺点适用场景ffmpeg concat速度快无损需要先生成文件列表分片已下载到本地二进制拼接最简单时间戳可能不连续分片时间戳连续ffmpeg直接处理一条命令依赖网络和反爬无严格反爬的站点我一般优先用方案三失败则用方案一。方案二只在特殊情况下使用比如分片数量极少且确认时间戳连续。3.6 完整脚本从m3u8到mp4的一键处理把上面的步骤整合成一个完整的Python脚本import requests import re import os import subprocess from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from urllib.parse import urljoin def download_m3u8(url, headers): resp requests.get(url, headersheaders, timeout30) resp.raise_for_status() return resp.text def parse_m3u8(content, base_url): ts_files re.findall(r^([^#\n].*\.ts.*)$, content, re.MULTILINE) key_info re.search(r#EXT-X-KEY:METHODAES-128,URI([^])(?:,IV0x([0-9a-fA-F]))?, content) key_uri None iv None if key_info: key_uri urljoin(base_url, key_info.group(1)) iv key_info.group(2) if key_info.group(2) else None return ts_files, key_uri, iv def download_file(url, filepath, headers): if os.path.exists(filepath): return True try: resp requests.get(url, headersheaders, timeout30) resp.raise_for_status() with open(filepath, wb) as f: f.write(resp.content) return True except Exception as e: print(f下载失败: {url} - {e}) return False def decrypt_ts(encrypted_data, key, iv): cipher AES.new(key, AES.MODE_CBC, iv) decrypted cipher.decrypt(encrypted_data) try: decrypted unpad(decrypted, AES.block_size) except ValueError: pass return decrypted def main(): m3u8_url https://example.com/video/index.m3u8 headers { Referer: https://example.com/video/page, User-Agent: Mozilla/5.0 ... } base_url m3u8_url.rsplit(/, 1)[0] / # 下载并解析m3u8 content download_m3u8(m3u8_url, headers) ts_files, key_uri, iv_hex parse_m3u8(content, base_url) # 下载密钥 key None if key_uri: key_path key.bin download_file(key_uri, key_path, headers) with open(key_path, rb) as f: key f.read() # 下载ts分片 os.makedirs(ts_files, exist_okTrue) for i, ts in enumerate(ts_files): ts_url urljoin(base_url, ts) ts_path os.path.join(ts_files, ts) download_file(ts_url, ts_path, headers) # 解密 if key: for i, ts in enumerate(ts_files): ts_path os.path.join(ts_files, ts) with open(ts_path, rb) as f: encrypted f.read() if iv_hex: iv bytes.fromhex(iv_hex) else: iv i.to_bytes(16, byteorderbig) decrypted decrypt_ts(encrypted, key, iv) with open(ts_path, wb) as f: f.write(decrypted) # 生成文件列表 with open(filelist.txt, w) as f: for ts in ts_files: f.write(ffile ts_files/{ts}\n) # 合并 subprocess.run([ ffmpeg, -f, concat, -safe, 0, -i, filelist.txt, -c, copy, output.mp4 ]) print(处理完成: output.mp4) if __name__ __main__: main()这个脚本覆盖了从m3u8下载到最终合并的全流程。实际使用时需要根据站点的具体情况调整请求头和URL。4. 常见问题与排查技巧实录4.1 下载的ts分片无法播放这是最常见的问题。可能的原因有几种分片本身是加密的但没有解密、分片下载不完整、分片格式不是标准的MPEG-TS。排查步骤先用ffprobe检查分片信息ffprobe -v error -show_format -show_streams segment_000.ts如果输出显示Invalid data found when processing input说明文件格式有问题。可能是加密数据也可能是下载不完整。对比文件大小和预期大小如果明显偏小重新下载。如果ffprobe能识别出视频流但播放花屏可能是解密时IV用错了。检查m3u8里是否有IV0x...如果有就用指定的IV没有就用序列号生成。4.2 ffmpeg报错Invalid argument或Protocol not found这类错误通常和请求头有关。有些站点要求特定的Referer或User-Agentffmpeg默认不带这些头会被服务器拒绝。解决方法是用-headers参数ffmpeg -headers Referer: https://example.com/page\r\nUser-Agent: Mozilla/5.0 ...\r\n \ -i https://example.com/video/index.m3u8 \ -c copy output.mp4注意\r\n的写法多个头之间用\r\n分隔末尾也要有\r\n。这个细节很容易忽略导致请求头没有正确传递。如果报错Protocol not found检查ffmpeg是否编译了HLS支持ffmpeg -protocols | grep hls如果没有输出说明当前ffmpeg版本不支持HLS需要重新安装完整版。4.3 合并后的视频音画不同步音画不同步通常是因为ts分片的时间戳不连续。HLS规范要求分片的时间戳连续但有些站点的分片来自不同的编码会话时间戳会重置。解决方法是用ffmpeg重新编码让ffmpeg重新计算时间戳ffmpeg -f concat -safe 0 -i filelist.txt -c:v libx264 -c:a aac output.mp4重新编码会损失一些画质但能解决时间戳问题。如果不想重新编码可以尝试用-fflags genpts参数让ffmpeg生成新的时间戳ffmpeg -fflags genpts -f concat -safe 0 -i filelist.txt -c copy output.mp4这个参数的效果取决于具体的时间戳问题不一定都能解决。4.4 密钥下载返回403密钥地址通常需要和m3u8相同的请求头。如果m3u8能下载但密钥返回403检查以下几点Referer是否正确、Cookie是否缺失、密钥地址是否需要拼接完整URL。有些站点的密钥地址是动态生成的每次请求m3u8时密钥地址都会变。这种情况下需要先请求m3u8获取最新的密钥地址再立即下载密钥不能使用缓存的旧地址。还有一种情况是密钥地址需要POST请求而不是GET。用浏览器开发者工具看密钥请求的方法和参数照着模拟。4.5 常见问题速查表问题现象可能原因解决方法ts分片无法播放未解密/下载不完整检查密钥和IV重新下载ffmpeg报Invalid argument缺少请求头用-headers参数补充合并后音画不同步时间戳不连续重新编码或加-fflags genpts密钥返回403请求头不匹配补充Referer/Cookie下载速度慢请求频率限制增加间隔分批下载合并后文件过大码率过高用-crf参数重新编码压缩m3u8地址过期签名/token失效重新从页面抓取分片数量不对m3u8不完整检查是否有#EXT-X-ENDLIST提示遇到问题时先用最小化测试——只下载一个分片单独解密单独播放。确认单个分片没问题后再处理批量。这样能快速定位问题出在哪个环节。4.6 几个容易忽略的细节第一个细节是m3u8的编码。有些m3u8文件是UTF-8 BOM格式用普通文本编辑器打开可能看到乱码。Python读取时用encodingutf-8-sig能自动处理BOM。第二个细节是ts分片的扩展名。有些站点不用.ts后缀而是用.jpg、.png甚至没有后缀。这种情况下不能靠后缀判断要看m3u8里的实际文件名。ffmpeg的concat协议不关心后缀只要内容是MPEG-TS就能处理。第三个细节是并发下载。用多线程下载能显著提升速度但要注意控制并发数。我一般用3-5个线程太多容易被封。Python的concurrent.futures模块能方便地实现并发下载。第四个细节是磁盘空间。一个小时的1080p视频ts分片加起来可能有1-2GB。下载前确认磁盘空间充足避免下到一半空间不足。5. 进阶技巧与效率提升5.1 用ffmpeg直接录制直播流如果是直播场景m3u8是动态更新的没有#EXT-X-ENDLIST标签。这种情况下不能用常规的下载合并方式需要用ffmpeg直接录制ffmpeg -headers Referer: https://example.com/page\r\n \ -i https://example.com/live/index.m3u8 \ -c copy -t 3600 live_output.mp4-t 3600表示录制3600秒不指定则一直录制直到手动停止。直播流的m3u8会不断更新ffmpeg会自动跟踪新的分片。5.2 批量处理多个视频如果需要下载多个视频可以把上面的脚本改造成批量处理。核心思路是把m3u8地址列表读进来循环处理每个视频。注意每个视频的输出文件名要区分开避免覆盖。我一般会用一个配置文件来管理任务tasks [ {name: video1, url: https://example.com/video1/index.m3u8, referer: https://example.com/video1/page}, {name: video2, url: https://example.com/video2/index.m3u8, referer: https://example.com/video2/page}, ]然后循环处理每个任务独立目录互不干扰。5.3 画质选择与码率判断主播放列表里通常有多个清晰度选项用#EXT-X-STREAM-INF标签标识BANDWIDTH属性表示码率。选择哪个清晰度取决于需求存档选最高码率移动端观看选中低码率。解析主播放列表的示例import re with open(master.m3u8, r) as f: content f.read() streams re.findall(r#EXT-X-STREAM-INF:BANDWIDTH(\d).*?\n(.*?\.m3u8), content) for bandwidth, url in streams: print(f码率: {int(bandwidth)//1000} kbps, 地址: {url})根据码率排序选择需要的清晰度对应的子播放列表地址再进入下载流程。5.4 处理特殊加密场景除了标准的AES-128有些站点会用非标准加密方式比如自定义的密钥派生、分片级别的不同密钥、或者对密钥本身再做一层加密。这些情况需要具体分析没有通用方案。我遇到过的几种变体密钥不是16字节而是32字节AES-256、IV不是标准生成方式而是固定值、分片头部有额外的加密标记。处理这些变体需要结合具体的m3u8内容和分片二进制分析用xxd查看分片头部对比加密前后的差异推断加密方式。注意分析加密方式时先用小样本测试。下载一个分片尝试不同的解密参数看哪个能解出可播放的内容。不要一次性处理所有分片避免浪费时间。5.5 性能优化建议下载环节的瓶颈通常在网络IO。用并发下载能提升速度但要注意控制并发数。我实测下来3-5个并发线程是比较稳妥的选择既能提升速度又不容易触发反爬。解密环节的瓶颈在CPU。AES-128解密的计算量不大但如果分片数量多累积起来也需要时间。可以用多进程加速Python的multiprocessing模块能利用多核CPU。合并环节的瓶颈在磁盘IO。-c copy模式不重新编码速度很快主要时间花在读写文件上。如果分片在机械硬盘上合并大文件时可能比较慢。用SSD能明显改善。5.6 合法合规使用提醒最后说一个容易被忽略但很重要的问题下载网页视频涉及版权和使用条款。实际操作中建议只下载自己有权限访问的内容比如自己上传的视频、公开授权的教育资源、或者明确允许下载的素材。不要用于商业传播或侵犯他人权益的场景。这个边界需要自己把握技术本身是中性的关键在于怎么用。我在实际使用中总结下来m3u8下载的核心难点不在技术本身而在于对具体站点行为的理解和适配。每个站点的反爬策略、加密方式、请求头要求都可能不同需要具体问题具体分析。掌握上面的基础流程后遇到新站点时先观察、再测试、最后批量处理基本能覆盖大多数场景。
网站建设高端定制企业官网