ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+Charles抓包实战:小程序短剧m3u8下载与合并

Python+Charles抓包实战:小程序短剧m3u8下载与合并 小程序里的付费短剧本质上是把视频流切片后通过接口按集下发地址客户端拿到地址再去拉流播放。这个链路里最薄弱的环节不是加密算法而是地址下发这一步——只要能在请求响应里看到真实的媒体地址后面的下载就是个体力活。我用Python配合Charles把这套流程跑通了好几次中间踩的坑比想象中多尤其是证书和模拟器这两块网上大部分教程都语焉不详。下面把完整思路、代码和避坑点摊开讲适合有一定Python基础、想搞清楚抓包与媒体下载链路的同学参考。1. 先想清楚为什么是Charles加Python这套组合1.1 抓包工具那么多为什么偏偏选Charles抓包工具市面上常见的有Charles、Fiddler、Wireshark、tcpdump这几类。Wireshark和tcpdump偏底层抓的是网卡层面的原始流量面对HTTPS只能看到加密后的字节流除非你导入会话密钥否则解不出明文。Fiddler功能接近Charles但它在处理移动端HTTPS解密时的证书信任链路在部分安卓版本上比Charles更容易出问题而且界面和会话过滤的顺手程度我个人更偏向Charles。Charles的核心优势在于它是一个中间人代理MITM。它在你和目标服务器之间架一个代理客户端以为自己在跟服务器说话实际上是在跟Charles说话Charles再转发给服务器。因为Charles持有自己签发的根证书只要客户端信任了这个根证书Charles就能解密HTTPS明文。这就是为什么证书安装是整个流程里绕不开的一步。选Charles还有一个现实原因它对移动端的支持成熟能直接抓安卓模拟器或真机的流量会话列表里按域名过滤、按接口路径筛选都很方便导出会话也简单。对于找到短剧视频地址这个目标来说效率最高。1.2 Python在这套流程里扮演什么角色很多人以为抓包工具能直接下载视频其实Charles只负责看不负责拿。你从Charles里看到的是一个m3u8播放列表或者一个mp4直链真正把这些分片下载、合并、转码成完整文件需要自己写脚本。Python在这里的价值就是自动化和批量化自动请求接口拿到每一集的媒体地址省去手动一集集点自动下载m3u8里的所有ts分片按顺序合并处理请求头、Referer、User-Agent这些容易被忽略但缺了就403的字段批量重命名、归档方便后续管理。如果只有几集手动下载也行。但短剧动辄几十上百集手动操作不现实脚本化是必然选择。1.3 这套方案的适用边界需要说清楚的是这套方法适用于你自己有权访问、有权保存的内容。抓包和下载技术本身是中性的但用在什么地方决定了它的性质。本文讨论的是技术链路本身——如何理解一个App的媒体下发机制、如何用工具观察和分析网络请求、如何用脚本处理媒体分片。请把它用在合规的场景里比如分析自己开发的小程序、研究媒体分发架构、或者处理你有合法访问权限的内容。2. 环境搭建Charles与模拟器的正确打开方式2.1 Charles的安装与基础配置Charles官网下载安装包Windows和macOS都有对应版本。安装完成后第一次启动会要求授予网络权限这个必须允许否则抓不到任何流量。启动后进入Proxy菜单勾选Windows ProxymacOS上是macOS Proxy这样系统层面的流量才会走Charles。接下来是关键的SSL代理设置。进入Proxy - SSL Proxying Settings勾选Enable SSL Proxying然后在Include列表里添加你要抓的域名。这里有个新手常犯的错误直接填*:*想抓所有域名。理论上可行但会导致大量无关流量涌入会话列表瞬间被刷屏找目标接口非常痛苦。正确做法是先大致确定目标小程序用到的域名只添加这几个。怎么确定域名先不配SSL代理让Charles以明文方式跑一遍观察哪些域名在请求。小程序通常会有几个固定域名一个业务接口域名、一个媒体CDN域名、可能还有统计和日志域名。找到业务接口域名后再把它加进SSL代理列表。2.2 证书安装这一步90%的人会卡住Charles要解密HTTPS必须让客户端信任Charles的根证书。电脑端的证书安装相对简单Help - SSL Proxying - Install Charles Root Certificate然后按系统提示导入到受信任的根证书颁发机构。Windows上要注意导入时一定要手动选择存储位置为受信任的根证书颁发机构如果只是双击安装到个人存储区浏览器和系统仍然不信任抓包会显示unknown。移动端才是真正的坑区。安卓7.0以后系统不再信任用户安装的证书用于应用流量只信任系统级证书。这意味着你把Charles证书装到用户证书里浏览器能抓但小程序、App的流量依然抓不到Charles里显示一片unknown或者直接连接失败。解决办法有两个方向方向一使用安卓7.0以下的模拟器或设备。这是最省事的办法。雷电模拟器有多个安卓版本可选选安卓5.1或7.0以下的镜像用户证书就能生效。这也是为什么热词里charles雷电模拟器出现频率很高——大家都在用这个组合。方向二把证书装到系统证书目录。需要root权限把证书文件转成特定格式后推到/system/etc/security/cacerts/目录下。这个过程涉及格式转换和权限修改稍有不慎会搞坏系统。如果你只是想快速跑通流程方向一更实际。证书装好后在模拟器的WiFi设置里配置代理主机名填电脑的局域网IP端口填Charles的默认端口8888。注意不能用127.0.0.1因为模拟器里的localhost指向模拟器自己不是你的电脑。电脑IP用ipconfigWindows或ifconfigmacOS查。2.3 验证抓包是否成功配置完成后在模拟器里打开浏览器访问一个HTTPS网站比如随便一个新闻站。如果Charles里能看到明文的请求和响应说明证书和代理都配对了。如果看到的是unknown或者连接被拒绝回到上一步检查证书存储位置和代理设置。这一步验证非常重要不要跳过。很多人直接打开小程序就开始抓结果抓不到又回头排查浪费大量时间。先用浏览器验证链路通畅再进小程序。3. 定位短剧媒体地址从接口到m3u8的完整链路3.1 小程序请求的特征与过滤技巧小程序运行在微信或宿主App的容器里它的网络请求和普通网页请求有几个区别。第一User-Agent里通常带有MicroMessenger字样方便过滤。第二请求往往带有一堆自定义header比如token、设备标识、版本号。第三接口路径通常有规律比如/api/drama/、/v1/video/这类。在Charles里用Filter框输入关键词过滤。先试m3u8因为短剧几乎都用HLS协议媒体地址大概率是m3u8。如果没结果试video、play、episode这些词。找到可疑接口后右键选择Focus把它单独拎出来看。3.2 识别m3u8与mp4两种媒体形态短剧的媒体下发一般有两种形态形态一直接返回mp4地址。接口响应里直接给一个完整的mp4链接下载下来就是完整视频。这种最简单但较少见因为不利于防盗链和分片加密。形态二返回m3u8播放列表。这是主流。m3u8是一个文本文件里面列出了所有ts分片的地址。客户端拿到m3u8后按顺序请求每个ts分片边下边播。你要做的是把m3u8和所有ts都下载下来再合并成一个完整视频。m3u8的内容长这样#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.000000, segment_000.ts #EXTINF:10.000000, segment_001.ts ... #EXT-X-ENDLIST#EXTINF后面是分片时长下一行是分片文件名。如果文件名是相对路径需要和m3u8的URL拼接成完整地址。3.3 接口里的加密参数怎么处理有些小程序的接口会带签名参数比如sign、timestamp、nonce。这些参数通常由前端JS计算生成直接复制URL过一会儿就失效。遇到这种情况有两个思路思路一在Charles里直接复制完整的请求。右键接口 - Copy cURL Request把整个请求包括所有header和参数复制出来在Python里用requests库原样发送。这样签名参数是现成的只要不过期就能用。缺点是签名有时效过期后要重新复制。思路二分析签名算法。如果接口调用频繁手动复制不现实就需要逆向小程序的JS代码找到签名生成逻辑用Python复现。这涉及反编译小程序包、定位加密函数工作量较大。对于一次性下载任务思路一足够。我个人的经验是先用思路一快速跑通确认整个链路可行再考虑要不要做思路二的自动化。很多情况下短剧的媒体地址一旦拿到有效期比较长不需要频繁重新签名。4. Python下载脚本从m3u8到完整视频4.1 下载m3u8并解析分片列表先写一个函数负责下载m3u8文件并解析出所有ts分片的完整URL。import requests import os from urllib.parse import urljoin def fetch_m3u8(m3u8_url, headers, save_dir): resp requests.get(m3u8_url, headersheaders, timeout15) resp.raise_for_status() content resp.text # 保存原始m3u8方便排查 os.makedirs(save_dir, exist_okTrue) with open(os.path.join(save_dir, index.m3u8), w, encodingutf-8) as f: f.write(content) segments [] for line in content.splitlines(): line line.strip() if not line or line.startswith(#): continue # 相对路径转绝对路径 full_url urljoin(m3u8_url, line) segments.append(full_url) return segments这里用urljoin处理相对路径是关键。很多m3u8里的分片是相对路径直接拼字符串容易出错urljoin会自动处理各种边界情况。4.2 带重试的分片下载分片下载最容易遇到的问题是网络抖动导致某个分片失败。如果不做重试整个下载就断了。下面这个函数带重试机制import time def download_segment(url, headers, save_path, retries3): for attempt in range(retries): try: resp requests.get(url, headersheaders, timeout20, streamTrue) resp.raise_for_status() with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) return True except Exception as e: print(f分片下载失败 {url} 第{attempt1}次: {e}) time.sleep(1.5) return FalsestreamTrue配合iter_content是处理大文件的标准做法避免一次性把整个分片读进内存。分片通常几百KB到几MB虽然不大但养成流式下载的习惯没坏处。4.3 并发下载与顺序保证短剧一集可能有几十上百个分片串行下载太慢。用线程池并发下载能大幅提速但要注意分片的顺序不能乱合并时必须按m3u8里的顺序来。from concurrent.futures import ThreadPoolExecutor, as_completed def download_all_segments(segments, headers, save_dir, max_workers8): os.makedirs(save_dir, exist_okTrue) results {} def task(idx_url): idx, url idx_url save_path os.path.join(save_dir, f{idx:05d}.ts) ok download_segment(url, headers, save_path) return idx, save_path, ok with ThreadPoolExecutor(max_workersmax_workers) as executor: futures [executor.submit(task, (i, u)) for i, u in enumerate(segments)] for future in as_completed(futures): idx, path, ok future.result() results[idx] (path, ok) # 按索引排序返回有序列表 ordered [results[i] for i in range(len(segments))] return ordered用索引命名分片文件00000.ts、00001.ts是保证顺序的简单办法。合并时按文件名排序即可不依赖下载完成的顺序。并发数不要开太大。我试过开32个线程结果被CDN限流大量分片返回403。8到16个是比较稳妥的范围具体看目标服务器的容忍度。4.4 合并分片与格式转换所有ts分片下载完成后直接二进制拼接就能得到可播放的视频文件。ts格式本身支持拼接这是HLS协议的设计特性。def merge_segments(ordered_segments, output_path): with open(output_path, wb) as out: for path, ok in ordered_segments: if not ok: print(f警告分片缺失 {path}) continue with open(path, rb) as f: out.write(f.read()) print(f合并完成: {output_path})拼接出来的文件是ts容器格式大部分播放器能直接播。如果要用剪辑软件处理建议用ffmpeg转成mp4ffmpeg -i merged.ts -c copy -bsf:a aac_adtstoasc output.mp4-c copy表示不重新编码只做容器转换速度极快。-bsf:a aac_adtstoasc是处理音频流的比特流过滤器ts转mp4时经常需要不加可能导致音频异常。5. 那些教程不会告诉你的坑5.1 证书装了还是unknown的三种原因这是最高频的问题。证书明明装了Charles里还是unknown。按可能性排序原因一证书装到了用户存储区但目标App只信任系统证书。前面说过安卓7.0以上这是默认行为。换低版本模拟器或root后装系统证书。原因二代理配置的IP或端口不对。模拟器里配的代理必须是电脑的局域网IP不是127.0.0.1。端口要和Charles设置里的一致默认8888但如果你改过就要对应改。原因三目标App使用了证书固定SSL Pinning。有些App会在代码里硬编码只信任特定证书不信任系统证书链。这种情况下Charles的证书再正确也没用。应对方法涉及hook框架复杂度较高不在本文讨论范围。遇到这种建议换目标或放弃。排查顺序先用浏览器验证证书是否生效如果浏览器能抓而App不能基本就是原因一或原因三。5.2 分片下载403的Referer与User-Agent问题媒体CDN通常有防盗链机制检查请求头里的Referer和User-Agent。如果你下载分片时只带了URL没带这两个头服务器直接返回403。解决办法是从Charles里把请求头完整复制出来在Python里原样带上headers { User-Agent: Mozilla/5.0 (Linux; Android 7.1.2) ..., Referer: https://servicewechat.com/..., Accept: */*, Accept-Encoding: identity, }Accept-Encoding设成identity很重要。如果设成gziprequests会自动解压但ts是二进制流不应该被当作文本压缩处理。设成identity让服务器返回原始字节。5.3 m3u8里的加密分片怎么识别有些m3u8里会出现#EXT-X-KEY标签#EXT-X-KEY:METHODAES-128,URIkey.key,IV0x...这表示分片用AES-128加密了需要先下载key文件再用key解密每个分片。解密逻辑from Crypto.Cipher import AES def decrypt_segment(encrypted_data, key, iv): cipher AES.new(key, AES.MODE_CBC, iv) return cipher.decrypt(encrypted_data)key文件就是16字节的原始密钥IV在m3u8里以十六进制给出。如果m3u8里没有#EXT-X-KEY说明没加密直接下载即可。遇到加密的先确认自己有权处理再动手。5.4 模拟器网络模式导致的抓包失败雷电模拟器有桥接和NAT两种网络模式。NAT模式下模拟器通过电脑上网代理配置正确就能抓。桥接模式下模拟器相当于独立设备代理配置方式不同。如果按教程配了代理还是抓不到检查一下模拟器的网络模式。另外有些模拟器自带网络加速或代理功能会和Charles冲突。在模拟器设置里把这些功能关掉。6. 把流程串起来一个可复用的下载脚本骨架把前面的函数组装成一个完整脚本输入m3u8地址和请求头输出合并后的视频文件。def download_drama(m3u8_url, headers, output_dir, output_nameoutput.ts): os.makedirs(output_dir, exist_okTrue) print(步骤1: 下载并解析m3u8) segments fetch_m3u8(m3u8_url, headers, output_dir) print(f共 {len(segments)} 个分片) print(步骤2: 并发下载分片) ordered download_all_segments(segments, headers, output_dir) failed [p for p, ok in ordered if not ok] if failed: print(f有 {len(failed)} 个分片下载失败建议重跑) print(步骤3: 合并分片) output_path os.path.join(output_dir, output_name) merge_segments(ordered, output_path) return output_path调用时把从Charles复制的请求头填进去headers { User-Agent: 你的UA, Referer: 你的Referer, } download_drama( m3u8_url从Charles里复制的m3u8地址, headersheaders, output_dir./drama_ep01 )这个骨架可以根据需要扩展批量处理多集、自动读取接口返回的m3u8列表、下载完成后自动调ffmpeg转mp4等。7. 关于效率与稳定性的几点实操心得并发数不是越大越好。我最初图快开了32线程结果CDN直接限流一半分片403。后来降到8线程稳定跑完。如果目标服务器响应快可以试12到16但要做好失败重试。分片下载失败不要慌脚本里已经带了重试。如果重试后还有失败把失败的分片单独拎出来重新下载不用整个重跑。可以在脚本里加个逻辑只下载本地不存在的分片已下载的跳过。这样重跑成本很低。m3u8地址有时效性。有些接口返回的m3u8链接带token几小时后就失效。所以拿到地址后尽快下载不要拖。如果确实需要长期保存下载完的ts分片和合并后的视频才是可靠的原始链接不可靠。最后Charles的会话可以导出为.chls文件方便以后回看。如果某个接口的参数逻辑没搞明白导出会话慢慢分析比对着屏幕干看强。这套流程我跑过好几次从最初的证书卡壳到后来的批量下载中间踩的坑基本都在这了。核心就三件事证书配对、请求头带全、分片顺序别乱。把这三件事做好剩下的就是写脚本的体力活。
返回列表