资讯动态

Python批量下载M3U8视频:并发控制、FFmpeg合并与断点续传实战

发布时间:2026/10/20 2:41:45 来源:尧图企业网站定制
简介M3U8批量视频流下载工具面向需要离线保存网页视频的普通用户与运维人员解决m3u8流媒体地址解析困难、逐个下载效率低的问题。压缩包共13个文件约23.6MB以exe可执行程序、json配置与日志文件为主另含dat、conf、session等辅助文件其中主程序负责解析播放地址并获取真实下载链接ffmpeg与aria2c等组件承担转码与多线程下载任务日志与配置文件则便于查看运行状态和调整参数。目前已有3481人学习下载。使用时可批量抓取m3u8视频流下载完成后直接转码为通用格式方便在电脑本地播放配套的缓存设置、会话记录与请求日志也为排查下载失败、断点续传等问题提供了参考适合想快速搭建批量下载流程的读者直接取用。1. 批量下载 M3U8从手动点播到脚本流水线的效率跃迁如果你手头攒了几十个甚至上百个 M3U8 视频链接还在浏览器里一个个点开、等它加载、再右键另存那这套流程基本等于用勺子挖隧道。M3U8 本质是一个 UTF-8 编码的播放列表文件里面记录了一串 TS 分片地址播放器按顺序拉取分片再拼接成完整视频。批量下载的核心思路就是解析播放列表、并发拉取分片、按序合并、最后转封装成 MP4。这套资源包提供的正是这条流水线的完整实现适合需要归档课程录像、整理直播回放、批量备份素材的从业者。它不依赖浏览器插件也不挑操作系统只要能跑 Python 和 FFmpeg 就能落地。下面我从实际拆包的角度把选型、参数、并发控制和踩坑点逐一讲透。2. 拆解 M3U8 下载链路从播放列表到 MP4 的四个阶段2.1 播放列表的两种形态与解析入口M3U8 文件分两种一种是主播放列表master playlist里面用#EXT-X-STREAM-INF标记不同码率的子播放列表地址另一种是媒体播放列表media playlist直接列出#EXTINF和对应的 TS 分片路径。批量下载时第一步永远是判断拿到的是哪种。如果链接返回的内容里出现#EXT-X-STREAM-INF说明需要先选一路码率再进入子列表解析。常见做法是用requests拉取文本按行扫描。下面这段代码展示了如何区分并提取真正的分片地址import requests from urllib.parse import urljoin def parse_m3u8(url, session): 解析 M3U8返回分片地址列表和是否需要二次解析 resp session.get(url, timeout15) resp.raise_for_status() lines resp.text.splitlines() # 判断是否为主播放列表 if any(line.startswith(#EXT-X-STREAM-INF) for line in lines): # 取第一个码率的子列表地址也可按带宽筛选 for i, line in enumerate(lines): if line.startswith(#EXT-X-STREAM-INF): sub_url lines[i 1].strip() return parse_m3u8(urljoin(url, sub_url), session) # 媒体播放列表提取非注释行作为分片地址 segments [] for line in lines: line line.strip() if line and not line.startswith(#): segments.append(urljoin(url, line)) return segments逻辑说明urljoin负责把相对路径补全成绝对地址因为很多 M3U8 里的分片写的是相对路径。参数上timeout15是防止某个链接卡死整个队列如果目标站点响应慢可以调到 30。注意主列表里可能有多个#EXT-X-STREAM-INF我一般按带宽从高到低排取第一个能成功拉取的避免选到 4K 但分片拉不动的尴尬。2.2 分片下载的并发模型与限速策略拿到分片列表后最忌讳的就是无脑开几百个线程。TS 分片通常几百 KB 到几 MB服务器对单 IP 的并发连接数有隐性限制开太多反而触发 403 或连接重置。我一般用concurrent.futures.ThreadPoolExecutor并发数控制在 8 到 16 之间具体看目标站点的响应头里有没有Retry-After或限速迹象。import os from concurrent.futures import ThreadPoolExecutor, as_completed def download_segment(args): 下载单个分片并写入临时文件 idx, seg_url, session, tmp_dir args resp session.get(seg_url, timeout20) resp.raise_for_status() path os.path.join(tmp_dir, f{idx:05d}.ts) with open(path, wb) as f: f.write(resp.content) return idx, len(resp.content) def batch_download(segments, session, tmp_dir, workers12): 并发下载所有分片返回按序号排序的文件列表 os.makedirs(tmp_dir, exist_okTrue) tasks [(i, url, session, tmp_dir) for i, url in enumerate(segments)] results [] with ThreadPoolExecutor(max_workersworkers) as pool: futures {pool.submit(download_segment, t): t[0] for t in tasks} for fut in as_completed(futures): idx, size fut.result() results.append((idx, size)) results.sort(keylambda x: x[0]) return [os.path.join(tmp_dir, f{i:05d}.ts) for i, _ in results]参数说明workers12是经验值对大多数 CDN 友好如果遇到 429 状态码降到 4 并加time.sleep(0.5)。idx:05d保证文件名按字典序排列就是播放顺序后面合并时不用再排序。每个分片单独写文件而不是拼接到内存是为了断点续传——某个分片失败只需重下那一个不用从头来。2.3 合并与转封装为什么不能直接 cat很多人以为把所有 TS 文件cat到一起改个后缀就是 MP4结果播放器要么花屏要么音画不同步。TS 是流式容器每个分片有独立的时间戳和 PCR直接拼接会导致时间戳跳变。正确做法是用 FFmpeg 的concat协议让它重新计算时间轴并转封装。# 先生成 concat 列表文件 for f in $(ls tmp_dir/*.ts | sort); do echo file $PWD/$f concat_list.txt done # 用 FFmpeg 合并并转封装为 MP4 ffmpeg -f concat -safe 0 -i concat_list.txt -c copy -bsf:a aac_adtstoasc output.mp4逻辑说明-c copy表示不重新编码只做容器转换速度极快-bsf:a aac_adtstoasc是处理 AAC 音频从 ADTS 头转到 ASC 头的必要步骤不加这行在某些播放器上会没声音。-safe 0允许列表里出现绝对路径。如果源分片是 H.265 编码-c copy依然适用但目标 MP4 的兼容性要看播放器支持。2.4 批量任务的队列管理与断点续传单个视频跑通后批量处理的关键在于任务队列。我一般用一个 JSON 文件记录每个 URL 的状态pending、downloading、done、failed。每次启动先读状态跳过done的重试failed的。临时目录按 URL 的哈希命名避免不同视频的分片混在一起。import json, hashlib def task_key(url): return hashlib.md5(url.encode()).hexdigest()[:12] def load_tasks(state_file): if os.path.exists(state_file): with open(state_file) as f: return json.load(f) return {} def save_tasks(state, state_file): with open(state_file, w) as f: json.dump(state, f, indent2)这样即使脚本中途被 CtrlC下次运行也能接着下。临时目录不要手动删等确认 MP4 能正常播放后再清理否则一旦合并失败就得全部重来。3. 避坑与排查批量下载中最容易翻车的五个点3.1 现象分片下载到一半大量 403原因目标站点对单个 IP 的并发连接数做了限制或者分片地址带了时效性 token过期后拒绝访问。解决把并发数降到 4 以下并在每次请求前检查 token 是否过期如果 M3U8 里的分片地址带?tokenxxx需要重新拉取播放列表刷新地址。3.2 现象合并后的 MP4 时长不对只有第一个分片原因concat 列表文件里的路径用了相对路径而 FFmpeg 的工作目录不对。解决列表里统一写绝对路径或者cd到临时目录再执行。另外检查concat_list.txt里是否有空行或中文路径未转义。3.3 现象视频能播但音画不同步越到后面越明显原因TS 分片的时间戳不连续直接-c copy会保留原始 PTS导致累积偏移。解决在 FFmpeg 命令里加-fflags genpts重新生成时间戳或者对每个分片先做一次-c copy转封装再合并。如果源本身是 VFR可变帧率建议重新编码音频。3.4 现象某些分片返回 200 但内容是 HTML原因CDN 在分片失效时返回一个错误页面而不是 404脚本按二进制写入后合并出来的视频中间夹了一段乱码。解决下载后检查文件头TS 分片应以0x47开头如果不是就标记为失败并重试。可以在download_segment里加两行判断。3.5 现象批量跑了几十个后磁盘满了原因临时 TS 文件没有及时清理每个视频的分片加起来可能比最终 MP4 还大。解决合并成功后立即删除该任务的临时目录并在脚本里加一个磁盘剩余空间检查低于 5GB 就暂停队列。4. 进阶技巧用 FFmpeg 直接拉流与并发数动态调整如果不想自己管理分片文件FFmpeg 本身就能直接读 M3U8 并输出 MP4一条命令搞定ffmpeg -headers Referer: https://example.com/ -i https://example.com/playlist.m3u8 -c copy -bsf:a aac_adtstoasc output.mp4这条命令的好处是省去了手动解析和合并FFmpeg 内部会处理分片顺序和时间戳。但它的缺点是并发控制不灵活遇到慢速分片会卡住整个流程而且断点续传能力弱。我一般用脚本方案处理大批量任务用 FFmpeg 直拉作为快速验证单个链接是否可下的手段。另一个实战技巧是动态调整并发数。启动时用 8 个线程每完成 50 个分片统计一次平均耗时和失败率如果失败率低于 1% 且平均耗时稳定就把并发提到 16如果出现 403 或超时立刻降到 4 并维持。这个逻辑用一个简单的滑动窗口就能实现不需要复杂的自适应算法。def adjust_workers(current, fail_rate, avg_time): 根据失败率和平均耗时动态调整并发数 if fail_rate 0.05: return max(2, current // 2) if fail_rate 0.01 and avg_time 1.5: return min(32, current 4) return current参数上fail_rate统计最近 100 个分片的失败比例avg_time是单个分片的平均下载秒数。这个策略在多个不同 CDN 的源上试过基本能稳住吞吐量又不触发封禁。最后说一个我自己的习惯每次批量任务启动前先拿列表里的第一个 URL 跑一遍完整流程——解析、下载前 10 个分片、合并、用ffprobe检查时长和编码。确认没问题再放开全量队列。这个动作花不到两分钟但能避免跑了几小时才发现某个参数不对的翻车。从那以后我每次批量下载都强制走一遍单链接验证希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑