夸克缓存视频合并:Python脚本实现分片转MP4的完整方案
夸克App缓存视频这事儿估计不少人都碰到过明明把视频缓存下来了结果想导出、想剪辑、想换个播放器看却发现缓存文件是一堆乱七八糟的小分片根本没法直接用。网上搜了一圈要么是教你用文件管理器改后缀名要么是让你下各种奇怪的“全能提取工具”实际用下来没几个靠谱的。我自己也在这个坑里折腾了好几次后来干脆写了个小脚本把合并流程自动化了用着还算顺手今天就把整个思路和实现过程拆开聊一聊给有同样需求的朋友一条能直接走通的路。先说清楚这个工具到底是干嘛的。夸克网盘客户端在缓存视频时并不像本地播放器一样生成一个完整的视频文件而是把视频切成大量小片段同时配上一个索引文件记录顺序和加密信息。这种设计对流媒体播放很友好但对普通用户来说就是一个黑盒缓存完只能在夸克里看拿不出来、用不了。我写的这个合并小工具做的就是一件事——把这些分片重新拼装成完整的MP4文件让你真正拥有这份缓存。这篇文章比较适合三类人看被夸克缓存文件折磨过的普通用户、想自己写点小工具但不知道从哪下手的编程新手以及想了解视频文件封装原理的技术爱好者。整个项目只需要一台电脑、一个夸克缓存好的视频不需要任何高级设备。1. 夸克缓存视频的存储结构先搞清楚敌人长什么样1.1 缓存目录与文件格式夸克网盘的缓存文件一般存在Android设备的内部存储里路径大致是/sdcard/Android/data/com.quark.browser/cache/下面不同版本可能略有差异。如果你是用电脑版夸克缓存的路径通常会配置在安装目录或者用户目录下的特定文件夹。实际进去看会发现缓存下来的内容并不是一整个视频而是很多的小文件。以我手头这个项目为例缓存目录里面有几百个无后缀名的小文件大小从几KB到几百KB不等按数字编号排列类似1031、1032、1033这样。除了这些分片还有一个.m3u8或者自定义格式的索引文件里面记录了视频流的分片顺序、时长、分辨率等信息。这是典型的HLS流媒体缓存结构夸克只不过是把它从网络上下载到了本地。理解了这一点合并的思路就清晰了把索引文件里记录顺序的分片按顺序拼接起来。但实际操作中远没有想象中那么简单因为夸克对索引文件做过改动直接用通用工具解析往往会失败。这也是为什么用VLC播放器直接打开本地缓存目录经常只能播出一小段或者根本放不出来的原因。1.2 常见缓存格式m3u8与自定义格式的区别标准的HLS流媒体使用m3u8文件作为索引里面明文写着每个分片的URL和时长。按理说拿到了m3u8逐行下载分片再合并就行了。但夸克客户端做了几层改动第一层是分片的加密。很多视频分片是加了密的m3u8里会有#EXT-X-KEY标签标明加密方式和密钥地址。夸克会把密钥文件也缓存到本地但文件名和路径被重命名过不能直接对应上。第二层是索引文件格式的改动。我测试过几个不同时期版本夸克的缓存索引文件有的还是标准m3u8有的就已经换成了夸克自定义的格式用文本编辑器打开能看到一些可读的字段但结构完全不是标准HLS。这时候就需要针对性解析。第三层是分片本身的处理。普通HLS的分片是ts格式可以直接拼接。但我发现夸克缓存的部分分片去掉了ts文件头0x47同步字节直接拼起来会导致播放器识别不了。所以合并之后还需要做一次修复把缺失的文件头补回去这就需要用ffmpeg之类的工具做转封装。1.3 直接改后缀和普通拼接方案为什么不靠谱网上最常见的教程是让你把缓存文件夹里的小文件改后缀名改成.ts或者.mpg然后就能播了。这个办法对某些老版本的缓存确实有效但我实测下来成功率很低原因就在上一节说的那些改动上。我拿一个缓存好的剧集做测试缓存目录里有几百个分片直接全选改后缀名成.ts后用播放器打开有大概30%的分片能正常播放但无法连续播放完整个视频有一半左右的分片提示“无法解码”或者画面花屏部分分片能达到完美播放效果但想把这些“完美分片”挑出来重新拼接工作量比写个合并工具还大普通拼接方案也就是直接用命令行copy /b把所有分片二进制合并成一个文件会遇到的问题是合并出来的文件有声音没画面或者画面卡在某一帧不动过一会儿又突然跳转。这是因为分片之间缺少正确的时间戳PTS/DTS信息直接把二进制数据倒在一起播放器无法正确解码。所以靠谱的方案一定是在解析索引文件的前提下按顺序合并分片再用ffmpeg做一次转封装修复时间戳。这意味着必须写代码不能靠简单粗暴的文件操作完成。2. 技术方案选型为什么Python加ffmpeg是最优解2.1 语言与工具选择对比解决这个问题的工具有很多种选择我用过的不下三种方案简单聊聊各自优劣。第一种是用批处理脚本Windows下的.bat或Linux下的.sh。这个方案最轻量不需要装任何额外环境写个简单的for循环就能把所有分片copy /b合并。优点是门槛低缺点是处理不了索引解析、文件头修复这些稍微复杂的逻辑。一旦遇到夸克改了缓存格式脚本就废了完全不可维护。第二种是用Java或C#写图形界面工具。优点是用户友好可以做拖拽操作和进度条适合分发给别人用。缺点还是那个开发成本高而且一旦夸克更新格式修改逻辑要重新编译打包迭代效率太低。第三种就是我最终选用的方案Python脚本配合ffmpeg。Python做文件扫描、索引解析、顺序编排这类逻辑活儿特别顺手代码量少、改起来快。ffmpeg作为业界标准的音视频处理工具负责最后的转封装和修复可靠性和兼容性都有保障。2.2 核心设计思路与流程拆解整个合并流程拆成五个环节第一步是定位缓存目录。用户输入或者程序自动扫描夸克的缓存路径找到目标视频对应的文件夹。第二步是识别分片文件。把文件夹下所有参与视频编码的分片文件列出来按文件名中的序号排序。这一步要排除掉索引文件、临时文件和其它非视频数据。第三步是解析索引文件。读取m3u8或夸克自定义格式的索引提取分片顺序、时长、密钥信息。这是整个流程里技术含量最高的一步各种异常情况都在这里处理。第四步是合并分片。按正确的顺序把分片数据用二进制模式拼接成一个文件。如果分片有加密需要先解密再拼接或者把密钥信息传给ffmpeg让它处理。第五步是修复并输出。用ffmpeg把拼接好的文件重新封装成MP4格式统一编码参数修复时间戳让它变成任何播放器都能正常识别的标准视频文件。2.3 为什么不用VLC自带的合并功能VLC播放器本身有一个“使用滤镜”里的合并功能能把多个视频文件按列表拼接。之前热词里也提到了“vlc视频合并”确实有不少人推荐这种做法。我用VLC试过一次效果很一般一是VLC合并对输入文件的编码一致性要求很高夸克缓存下来的ts分片虽然整体编码一致但文件头缺失会导致VLC判定失败二是VLC合并出来的是ts流文件不是MP4兼容性大打折扣三是VLC的合并是重新编码速度很慢一个小时的视频可能要处理二十分钟而ffmpeg做转封装只需要几秒钟。3. 核心代码实现与关键参数解读3.1 第一步扫描缓存目录识别分片文件这一步的核心是找出目标视频的缓存目录并列出所有的分片文件。我的做法是让用户在命令行传入缓存根目录程序自动搜索里面包含分片文件的文件夹。import os import re import sys def find_cache_dirs(root_path): 扫描缓存根目录找到所有包含视频分片的文件夹 cache_dirs [] for dirpath, dirnames, filenames in os.walk(root_path): # 夸克缓存分片通常是纯数字文件名且数量大于10 slice_count 0 for f in filenames: if re.match(r^\d$, f): slice_count 1 if slice_count 10: cache_dirs.append((dirpath, slice_count)) return cache_dirs if __name__ __main__: root sys.argv[1] if len(sys.argv) 1 else ./ dirs find_cache_dirs(root) for d, count in dirs: print(f{d} 分片数: {count})这里的关键点是用正则^\d$匹配纯数字文件名。为什么强调纯数字因为夸克的缓存分片命名规则就是纯数字递增如果有些版本改成带后缀的命名就需要根据实际情况调整正则表达式。另外我设置了一个阈值分片数大于等于10才认为是视频缓存目录。这是为了避免把日志目录或者配置文件目录误判成视频。当然短视频缓存可能分片数量少于10个这个阈值可以根据需要调整。另外一个容易被忽略的细节是os.walk遍历目录默认会进入所有子目录如果缓存根目录特别大比如用户把整个数据目录都指定为根目录遍历会很慢。实测下来遇到几千个文件时Python的os.walk也要好几秒。优化方案是用os.scandir替代部分逻辑或者限定搜索深度。3.2 第二步解析索引文件还原播放顺序识别出缓存目录后下一步就是找索引文件并解析。标准的m3u8文件用文本方式读取逐行解析即可def parse_m3u8(index_path): 解析m3u8索引文件提取分片顺序和密钥信息 segments [] key_info None with open(index_path, r, encodingutf-8) as f: lines f.readlines() i 0 while i len(lines): line lines[i].strip() if line.startswith(#EXT-X-KEY): # 解析密钥信息例如 # #EXT-X-KEY:METHODAES-128,URIkey.key,IV0x... parts line.replace(#EXT-X-KEY:, ) items dict(item.split(, 1) for item in parts.split(,)) key_info { method: items.get(METHOD, ), uri: items.get(URI, ).strip(), iv: items.get(IV, ) } elif line.startswith(#EXTINF:): # 下一行就是分片文件名 duration line.split(:)[1].rstrip(,) if i 1 len(lines): seg_file lines[i 1].strip() segments.append({ file: seg_file, duration: float(duration) }) i 1 # 跳过文件名行 i 1 return segments, key_info但现实情况往往更复杂。我遇到过几类特殊情况第一类是索引文件不是UTF-8编码。夸克在Windows版本上缓存的索引文件可能是GBK编码直接用utf-8读取会报错。我在代码里加了一层编码检测用chardet先检测编码再按检测结果读取。第二类是索引文件里混合了相对路径和绝对路径。有的索引文件里写的是file:///storage/emulated/0/...这种绝对路径有的只写分片文件名。解析的时候需要做路径变换统一变成当前缓存目录下的实际文件路径。第三类是最麻烦的夸克自定义格式。这种格式不能用m3u8解析逻辑处理需要单独写一个解析函数。我采取的策略是先快速判断文件头部如果前几行里含有#EXTM3U就走标准m3u8解析否则走自定义格式解析。自定义格式的字段不固定我用的方法是正则匹配所有看起来像文件名的字符串再按数字排序这样即使字段含义变了也能凑合提取出分片列表。3.3 第三步分片合并与密钥处理拿到分片列表后合并本身就是一个二进制复制操作。但有两类情况需要预处理第一类是没有加密的分片。这种最简单直接按顺序读取每个分片文件写入目标文件即可。第二类是AES-128加密的分片。需要先读取密钥文件用密钥对每个分片做解密再把解密后的数据写入目标文件。Python的pycryptodome库可以很好的完成这个工作from Crypto.Cipher import AES def decrypt_segment(seg_path, key, iv): AES-128解密单个分片 with open(seg_path, rb) as f: data f.read() cipher AES.new(key, AES.MODE_CBC, iv) return cipher.decrypt(data) def merge_segments(segments, output_path, key_infoNone): 按顺序合并所有分片 with open(output_path, wb) as out: for seg in segments: seg_path seg[file] # 确保分片文件存在 if not os.path.exists(seg_path): print(f[警告] 分片缺失: {seg_path}) continue with open(seg_path, rb) as f: data f.read() if key_info and key_info.get(method) AES-128: key_path os.path.join(os.path.dirname(seg_path), key_info[uri]) with open(key_path, rb) as kf: key kf.read() # IV要么在索引文件里给出要么默认用分片序号 iv bytes.fromhex(key_info.get(iv, 0)[-32:]) if key_info.get(iv) else (0).to_bytes(16, big) data decrypt_segment(seg_path, key, iv) out.write(data)这里有个非常坑的地方是IV的默认值。标准HLS规范里如果没写IV默认用分片序号作为IV序号从0开始递增。但我实测夸克缓存的加密视频有的分片序号不是从0开始而是从1开始。如果密钥和序号对不上解密出来的数据就是花屏。我的解决方式是在解析m3u8时记录每个分片在播放列表中的实际序号把它作为IV传入解密函数而不是用循环里的索引。另外一个细节是iv字段的转换。m3u8里的IV通常写成IV0x9c7a...这种16字节的十六进制数前面带0x前缀。解析时需要去掉前缀再按字节流转成bytes类型否则CBC模式的IV长度不匹配直接报错。3.4 第四步ffmpeg修复与重封装分片合并完成后得到的还是一个ts流文件里面可能有缺失的文件头也可能有时间戳不连续的问题。直接改名成mp4是播不了的必须用ffmpeg重新封装。import subprocess def fix_and_remux(input_file, output_file): 用ffmpeg修复ts流并重新封装为mp4 cmd [ ffmpeg, -y, -i, input_file, -c, copy, # 不重新编码只复制音视频流 -bsf:a, aac_adtstoasc, # 修复AAC音频流格式 -movflags, faststart, # 让MP4支持边下载边播放 output_file ] result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: raise RuntimeError(fffmpeg处理失败: {result.stderr})关键参数说明-c copy表示流复制模式不做重新编码。这样速度快、画质无损失。如果加了重新编码一个2GB的缓存视频可能要转半小时而-c copy模式只需几秒到几十秒。-bsf:a aac_adtstoasc是处理AAC音频的必备参数。夸克缓存视频的音频流通常是AAC格式以ADTS容器封装在ts里。转成MP4时需要把ADTS头转换成AudioSpecificConfig格式否则播放器会识别不了音频流。网上很多合并教程不提这个参数导致转出来的视频没有声音就是这个原因。-movflags faststart会把MP4文件的元数据moov box移动到文件头部这样视频不必完整下载也能播放。对于网络播放和手机端播放器特别友好。不加这个参数有的播放器要等文件全部加载完才能开始播放体验很差。3.5 命令行参数设计脚本写好后使用方式遵循简单直接的原则只暴露两个核心参数输入缓存目录和输出文件名。python merge_quark_cache.py --input ./quark_cache/视频文件夹 --output ./merged_video.mp4可选的还有几个参数--keep-ts保留中间产生的ts文件方便排查问题--no-fix跳过ffmpeg修复调试用--max-depth限制目录扫描深度。这些参数都是我在实际使用中陆续加的一开始只写了最简单的版本后来遇到各种情况才补全了这些开关。4. 实操演练从夸克缓存到MP4的完整流程4.1 环境准备与安装依赖在本地环境配置Python3和ffmpeg。以Windows为例Python直接从官网下载安装包安装时勾选“Add Python to PATH”选项避免后续命令行找不到python命令。ffmpeg的安装相对麻烦一点因为官网只提供源码包。我建议用满足构建版本去gyan.dev下载Windows构建版解压后把bin目录加到系统环境变量的PATH里。Linux用户直接用包管理器安装sudo apt install ffmpeg。装完后在命令行验证一下python --version ffmpeg -version看到版本号输出就说明环境没问题。Python依赖库只有两个pycryptodome处理AES解密和chardet检测文件编码一条pip命令搞定pip install pycryptodome chardet4.2 实际缓存目录扫描和合并我第一次跑这个脚本时用了一个已经缓存好的1080P电影做测试。缓存目录结构比较复杂最外层是夸克的应用数据根目录里面套了好几层子目录。我把合并脚本放到这个根目录下然后指定这个根目录作为输入参数。程序输出如下$ python merge_quark_cache.py --input ./quark_cache --output ./movie.mp4 [信息] 扫描到 3 个候选缓存目录: ./quark_cache/abc123/video_789 分片数: 286 ./quark_cache/def456/video_234 分片数: 318 ./quark_cache/ghi789/video_111 分片数: 45 [提示] 自动选择分片数最多的目录: ./quark_cache/def456/video_234 [信息] 解析索引文件: index.m3u8 [信息] 索引中共有 318 个分片无加密 [信息] 开始合并分片... [信息] 合并完成临时文件大小: 2.3GB [信息] 调用ffmpeg修复并转封装... [信息] 完成输出文件: ./movie.mp4 (2.1GB)整个过程耗时大约1分钟。分片合并是纯文件复制速度取决于磁盘性能实测大约每秒500MB左右。ffmpeg转封装基本是秒级操作318个分片加起来的实际视频时长约2小时转封装时间约10秒。这里自动选择分片数最多的目录是一个小技巧。通常分片数最多的目录就是体积最大、时长最长的视频。但这个方法不完美如果缓存里有多个视频分片数接近就需要逐个查看。为了应对这种情况我在脚本里加了一个--select参数支持手动指定目录路径而不是自动选择。4.3 合并产物的播放兼容性测试合并完成的MP4文件我用了几种不同的播放器和平台做了兼容性验证VLC桌面版正常播放拖动进度条流畅音画同步正常手机系统自带播放器正常播放快进快退都正常网页端HTML5播放器正常播放得益于faststart参数页面打开后可以立即播放不用等缓冲视频剪辑软件剪映/PR正常导入剪辑流畅有一个特殊情况需要提一下如果原视频编码是H.265HEVC转封装后的MP4在旧一点的设备上可能播不了因为硬件不支持H.265硬解码。这种问题没法通过转封装解决只能重新编码但那就需要牺牲画质和速度了。我建议普通用户先试试能不能播不能播再考虑重编码方案。5. 合并过程中的高频问题与排查技巧5.1 缓存文件路径太深Windows报路径过长怎么办夸克的缓存目录层级本来就深再加上Android存储映射路径经常出现路径超过260字符的情况。Windows的老版本文件系统API对路径长度有限制Python的os模块直接操作就会报错。解决办法有两种一是开启Windows的长路径支持在注册表里设置LongPathsEnabled为1然后重启。这个方案一劳永逸但不是所有版本Windows都支持。二是用Python的\\\\?\\前缀把所有路径转换成UNC格式绕过Win32 API的长度限制。注意这里不是反斜杠转义的问题是真的要在路径前面加\\\\?\\前缀。我实际测试下来大部分情况用第二个方案就能解决。代码改动也很小写一个路径转换函数在所有open操作前调用即可。5.2 分片文件不完整合并时提示找不到文件缓存过程中如果网络不稳定或者用户手动停止了下载缓存目录里的分片就不是完整的318个可能只有214个。这时候直接合并会得到一个“残疾”视频播放到缺失分片的位置就会卡住或者跳帧。我的做法是合并前先检查分片文件的连续性把缺失的序号报出来让用户决定是继续合并还是重新缓存。代码实现上就是用正则提取所有分片序号排序后和理论上的完整序号列表做差集。还有一个更隐蔽的问题是分片文件大小异常。有的分片只有几十字节明显不是一个正常的视频分片正常至少也要几KB。这种可能是下载时被截断的产物。我在合并时会跳过大小小于1KB的分片并输出警告信息。5.3 合并后的视频音画不同步声音正常画面卡顿这个问题的根源是时间戳不连续也是直接二进制合并最常见的副作用。虽然ffmpeg转封装能修复一部分时间戳问题但当分片序列里有大量缺失时ffmpeg也修不好。排查思路是先用ffprobe查看输出文件的流信息ffprobe -show_streams output.mp4重点看视频流和音频流的start_time和duration。如果音频流和视频流的起始时间差超过0.5秒就会出现橙音画不同步。这种情况最有效的解决方式是放弃流复制模式改用重新编码ffmpeg -i merged.ts -c:v libx264 -c:a aac output.mp4代价是处理时间大幅增加但能彻底解决时间戳错乱问题。适用于对画质要求不那么极致、对播放体验要求高的场景。5.4 夸克版本升级后缓存格式变化脚本失效这是最让人头疼的情况。夸克客户端的缓存格式并不是完全稳定的大版本更新后缓存目录结构、分片命名、索引文件格式都可能变动。我遇到过的一次变化是分片从1031这种纯数字改成了v_1031带前缀形式我的脚本扫描正则直接失效。应对这种问题的策略是写一个容错性更强的扫描逻辑不再只匹配纯数字文件名而是匹配^\w_\d$或者数字加字母的混合命名。同时索引文件的解析也增加了更多的编码兼容和格式兼容处理。但如果夸克改了加密方式那就真的无能为力了需要重新分析新格式的索引结构和密钥逻辑。好在目前我接触到的最新版夸克缓存格式还能兼容但谁也不能保证未来不会变。这也是我把代码开源、把解析逻辑做模块化的原因真的遇到格式更新改起来能快点。5.5 合并完成后文件能播但总时长和原视频对不上这个问题的原因主要是夸克缓存时做过分片合并。有些原本是分开的多个分段视频比如剧集的上下集或广告分段缓存在同一个目录下m3u8索引里可能会有多个播放列表#EXT-X-MEDIA-SEQUENCE切换。如果解析时只处理了第一个播放列表合并出来的文件就只有第一段。解决的思路解析m3u8时循环处理所有的#EXT-X-PLAYLIST或者跳转链接把所有分段的分片都汇总到同一个列表里再统一合并。这个逻辑我现在的脚本已经有处理但处理得还不算完美遇到特殊的跳转结构可能还是会有遗漏。如果合并后的时长和原视频差得比较多优先检查索引文件里是否有多个播放列表。6. 让工具更好用的扩展思路脚本目前是命令行版本技术门槛还是有点高。我最近在考虑做一个简单的图形界面用Python的tkinter库做个文件选择框和进度条这样不熟悉命令行的朋友也能轻松使用。另外还有一个扩展方向是批量处理。现在脚本一次只能处理一条视频如果缓存了整部剧集就得手动运行多次。加一个批量模式自动扫描所有缓存目录并逐一合并输出省去重复操作的麻烦。还有一个思路是把这个脚本的核心合并逻辑封装成库提供函数接口方便其他开发者集成到自己工具里。目前代码里已经有比较清晰的模块划分但还没有做成标准包的形式。后续整理一下依赖和文档可以直接发布到pip源。从技术角度看这个工具的核心逻辑并不复杂但解决的实际问题却给很多人省了不少事。这也是我喜欢做这类工具的原因用一点技术手段消灭那些日常生活中烦人的小事。代码已经放在我的代码仓库里了需要的朋友可以直接下载使用有问题也欢迎在评论区交流。