Python合并TS视频的三种方法:FFmpeg、MoviePy与二进制拼接实战

📅 2026/7/31 3:56:47 👁️ 阅读次数 📝 编程学习
Python合并TS视频的三种方法:FFmpeg、MoviePy与二进制拼接实战

1. 项目概述:为什么我们需要用Python合并TS视频?

在视频处理和数据抓取的日常工作中,我经常遇到一种情况:从网络上下载的视频文件,尤其是那些流媒体视频,往往被分割成成百上千个以.ts为后缀的小文件。这些TS(Transport Stream)文件是MPEG-2传输流的标准格式,广泛用于直播、点播和在线视频的分片传输。手动将这些碎片拼接成一个完整的视频,不仅效率低下,而且容易出错。这时,一个自动化、可靠的合并工具就显得至关重要。

Python,凭借其丰富的库生态和简洁的语法,成为了处理这类任务的绝佳选择。无论是为了归档下载的课程视频、拼接抓取的影视剧片段,还是处理监控录像的流式文件,掌握用Python合并TS视频的技能都能极大提升工作效率。本文将深入探讨三种经过实战检验的合并方法:基于FFmpeg的命令行调用、使用moviepy库进行编程化处理,以及最原始但最可控的二进制文件直接拼接。每种方法都有其适用场景和优缺点,我将结合具体代码和大量实操细节,带你从原理到实践,彻底掌握这门手艺。

2. 核心思路与方案选型:三种方法的定位与抉择

面对一堆TS文件,我们首先要明确目标:不仅仅是“合并”,而是要“正确、高效且可追溯地合并”。这涉及到音视频编码的容器格式、时间戳的连续性、可能存在的加密或编码不一致等问题。下面三种方法,代表了三种不同的解决路径。

2.1 方法一:FFmpeg——功能强大的“瑞士军刀”

FFmpeg是一个完整的、跨平台的音视频处理解决方案。用它来合并TS文件,本质上是调用其强大的复用(Muxing)能力。它的优势在于“专业”,内部会自动处理容器格式、时间戳同步、编码检查等复杂问题,合并后的视频质量最有保障。适合对输出视频质量要求高,或源TS文件可能存在编码参数微调的场景。

2.2 方法二:MoviePy——面向Python开发者的“高级封装”

MoviePy是一个用于视频编辑的Python库,它提供了非常人性化的API。其合并功能底层可能也依赖FFmpeg,但为我们封装了更Pythonic的调用方式。它的优势在于“集成”,如果你的Python项目本身就在进行复杂的视频剪辑、合成、特效添加,那么使用MoviePy来合并TS片段可以保持技术栈的统一,让代码更整洁。

2.3 方法三:二进制直接拼接——简单粗暴的“底层操作”

这种方法完全绕开了音视频处理库,直接将TS文件视为二进制字节流,按顺序读取并写入一个新文件。它的优势在于“极简”和“高速”。因为不进行任何解码、编码或格式分析,所以速度极快,对系统资源消耗极小。但它的前提也最苛刻:要求所有TS文件必须具有完全相同的编码格式、分辨率、码率等参数,且文件本身是“纯净”的传输流片段。常用于处理由同一源规范切割的、未加密的TS文件。

如何选择?

  • 追求稳定和兼容性,首选FFmpeg。它是行业标准,能处理绝大多数疑难杂症。
  • 已在Python视频处理流水线中,考虑MoviePy。保持代码风格一致。
  • 确认TS文件来源单一、格式纯净,且追求极限速度,使用二进制拼接。例如合并自己用工具按固定间隔切分的大文件。

3. 环境准备与核心工具解析

工欲善其事,必先利其器。在开始编码前,我们需要搭建好环境。

3.1 FFmpeg的安装与验证

FFmpeg并非Python库,而是一个独立的命令行工具。Python代码通过subprocess模块去调用它。

Windows系统安装:

  1. 访问FFmpeg官网的下载页面,找到Windows版本的构建(推荐使用“gyan.dev”或“BtbN”提供的静态构建版本)。
  2. 下载ZIP压缩包,解压到任意目录,例如D:\ffmpeg
  3. D:\ffmpeg\bin添加到系统的环境变量PATH中。
  4. 打开命令提示符(CMD)或PowerShell,输入ffmpeg -version,如果显示版本信息,则安装成功。

macOS系统安装:使用Homebrew是最简单的方式:brew install ffmpeg

Linux系统安装:对于Debian/Ubuntu系:sudo apt update && sudo apt install ffmpeg对于CentOS/RHEL系:sudo yum install epel-release && sudo yum install ffmpeg(可能需要启用EPEL仓库)

注意:确保FFmpeg版本不要太旧,否则可能不支持一些新的编码格式。

3.2 Python库的安装

我们将用到moviepy和用于文件操作的标准库。使用pip进行安装:

pip install moviepy

moviepy会自动安装其依赖,如imageiodecorator等。如果遇到安装问题,可以尝试先升级pip:pip install --upgrade pip

3.3 项目文件结构规划

一个清晰的文件结构有助于管理。假设我们有一个项目文件夹ts_merger

ts_merger/ ├── ts_files/ # 存放所有待合并的.ts文件 │ ├── segment_0001.ts │ ├── segment_0002.ts │ └── ... ├── output/ # 存放合并后的视频文件 ├── ffmpeg_merge.py # 方法一的脚本 ├── moviepy_merge.py # 方法二的脚本 └── binary_merge.py # 方法三的脚本

4. 方法一详解:使用FFmpeg进行合并(最推荐)

这是最稳健、最通用的方法。FFmpeg的concat协议非常擅长处理流拼接。

4.1 原理与步骤

FFmpeg合并TS文件通常有两种方式:

  1. 使用concat协议(针对TS流):创建一个文本文件(如filelist.txt),里面按顺序列出所有TS文件的路径。然后让FFmpeg读取这个列表进行合并。这种方式效率高,是处理TS文件的推荐方式。
  2. 使用concat分离器(针对普通文件):更通用,但可能需要对文件进行转码。

我们采用第一种方式,因为它最快且能保持原始编码。

核心步骤:

  1. 扫描目标文件夹,获取所有.ts文件,并按名称或创建时间排序。
  2. 生成一个符合FFmpegconcat协议格式的文本文件。
  3. 构造FFmpeg命令行参数并执行。
  4. 检查输出文件是否完整。

4.2 完整代码实现与注释

import os import subprocess import sys def merge_ts_with_ffmpeg(ts_folder, output_file): """ 使用FFmpeg合并TS文件。 Args: ts_folder (str): 存放TS文件的文件夹路径。 output_file (str): 合并后的输出文件路径(如 output.mp4)。 """ # 1. 检查FFmpeg是否可用 try: subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True) except (subprocess.CalledProcessError, FileNotFoundError): print("错误:未找到FFmpeg。请确保FFmpeg已安装并已添加到系统PATH环境变量中。") sys.exit(1) # 2. 获取并排序TS文件列表 # 确保我们只获取.ts文件,并按数字顺序或字母顺序排序,这取决于你的文件名 ts_files = [f for f in os.listdir(ts_folder) if f.endswith('.ts')] # 假设文件名类似 segment_0001.ts, segment_0002.ts ts_files.sort(key=lambda x: int(''.join(filter(str.isdigit, x))) if any(c.isdigit() for c in x) else x) # 更通用的排序:按文件名自然排序 # import re # ts_files.sort(key=lambda x: [int(c) if c.isdigit() else c for c in re.split(r'(\d+)', x)]) if not ts_files: print(f"在文件夹 '{ts_folder}' 中未找到.ts文件。") return print(f"找到 {len(ts_files)} 个TS文件。") # 3. 创建临时文件列表(filelist.txt) filelist_path = os.path.join(ts_folder, 'filelist.txt') with open(filelist_path, 'w', encoding='utf-8') as f: for ts_file in ts_files: # 使用`file`协议,并确保路径是绝对路径,避免奇怪的问题 file_path = os.path.abspath(os.path.join(ts_folder, ts_file)) # 对Windows路径的反斜杠进行转义,或统一为正斜杠 file_path = file_path.replace('\\', '/') # concat协议格式:每行是 `file '文件路径'` f.write(f"file '{file_path}'\n") print(f"已生成文件列表:{filelist_path}") # 4. 构建FFmpeg命令 # -f concat: 指定使用concat分离器 # -safe 0: 允许使用绝对路径或“不安全”的路径 # -i filelist.txt: 输入是文件列表 # -c copy: 流复制模式,不重新编码,速度极快 # -y: 如果输出文件已存在,则直接覆盖(可选,生产环境慎用) cmd = [ 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', filelist_path, '-c', 'copy', # 关键参数!直接复制流,不重新编码 '-y', # 自动覆盖输出文件 output_file ] print(f"执行命令:{' '.join(cmd)}") # 5. 执行命令并捕获输出 try: # 使用Popen实时输出信息,便于调试 process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) for line in process.stdout: # 可以过滤或实时打印FFmpeg的输出信息 if 'frame=' in line or 'time=' in line: # 打印进度信息(如果FFmpeg输出到stderr,这里可能捕获不到,需要调整) print(line.strip(), end='\r') # 用回车实现单行更新 # 也可以简单打印所有输出 # print(line, end='') process.wait() if process.returncode == 0: print(f"\n合并成功!输出文件:{output_file}") # 可选:删除临时文件列表 os.remove(filelist_path) print(f"已删除临时文件列表。") else: print(f"\n合并失败,FFmpeg返回错误码:{process.returncode}") except Exception as e: print(f"执行FFmpeg命令时发生异常:{e}") finally: # 确保即使出错也尝试清理临时文件 if os.path.exists(filelist_path): os.remove(filelist_path) if __name__ == '__main__': # 使用示例 ts_folder = './ts_files' # 你的TS文件夹路径 output_file = './output/merged_video.mp4' # 输出文件路径和格式,也可以是 .mkv, .ts等 # 确保输出目录存在 os.makedirs(os.path.dirname(output_file), exist_ok=True) merge_ts_with_ffmpeg(ts_folder, output_file)

4.3 关键参数解析与避坑指南

  • -c copy(或-codec copy):这是命令的灵魂。它告诉FFmpeg不要对视频和音频流进行解码和重新编码,而是直接复制原始数据流到新容器中。这保证了合并速度最快(几乎是文件IO的速度),并且是无损的,画质和音质没有任何损失。绝大多数情况下都应该使用这个参数。

  • -safe 0当在-i参数中使用文件列表时,如果文件列表中的路径是绝对路径或包含特殊字符,FFmpeg可能会出于安全考虑拒绝读取。-safe 0禁用了这个安全检查。在脚本控制的场景下,这是安全的。

  • 文件列表的路径问题:这是最常见的坑。如果TS文件路径中包含空格或特殊字符,必须在filelist.txt中用引号括起来,并且确保路径正确。上述代码中,我们使用绝对路径并统一将反斜杠\替换为正斜杠/,能有效避免Windows下的路径问题。

  • 排序至关重要:TS文件必须按照播放顺序合并。确保你的排序逻辑(ts_files.sort(...))与实际顺序一致。如果文件名没有规律,你可能需要根据文件修改时间排序(os.path.getmtime),但这风险较高,最好从源头保证文件名有序。

  • 输出格式选择:-c copy模式下,输出文件的容器格式(如.mp4,.mkv,.ts)需要能容纳输入流的编码格式。通常,.mp4.mkv的兼容性都很好。如果你希望输出仍然是TS流,可以指定输出为.ts文件。

实操心得:在正式合并大量文件前,我强烈建议先用前几个文件(比如ts_files = ts_files[:5])做一次测试合并,验证整个流程和排序是否正确。这能避免几个小时的合并工作因为顺序错误而前功尽弃。

5. 方法二详解:使用MoviePy库进行合并

MoviePy提供了更面向对象的接口,适合在更复杂的Python视频处理流程中集成TS合并功能。

5.1 原理与步骤

MoviePy的concatenate_videoclips函数可以将多个VideoFileClip对象按顺序连接起来。其底层可能调用FFmpeg或使用其他方法进行合成。这种方法代码更简洁,但相比直接调用FFmpeg,可能在某些边缘情况下灵活性稍差,且依赖库的安装。

核心步骤:

  1. 使用VideoFileClip逐个加载TS文件。
  2. 将所有VideoFileClip对象放入一个列表。
  3. 使用concatenate_videoclips函数合并它们。
  4. 将合并后的clip写入输出文件。

5.2 完整代码实现与注释

from moviepy.editor import VideoFileClip, concatenate_videoclips import os def merge_ts_with_moviepy(ts_folder, output_file): """ 使用MoviePy合并TS文件。 Args: ts_folder (str): 存放TS文件的文件夹路径。 output_file (str): 合并后的输出文件路径。 """ # 1. 获取并排序TS文件列表(同方法一) ts_files = [f for f in os.listdir(ts_folder) if f.endswith('.ts')] ts_files.sort(key=lambda x: int(''.join(filter(str.isdigit, x))) if any(c.isdigit() for c in x) else x) if not ts_files: print(f"在文件夹 '{ts_folder}' 中未找到.ts文件。") return print(f"找到 {len(ts_files)} 个TS文件,开始加载...") clips = [] for i, filename in enumerate(ts_files): file_path = os.path.join(ts_folder, filename) try: # 加载每个TS文件为一个VideoFileClip对象 clip = VideoFileClip(file_path) clips.append(clip) print(f"已加载 ({i+1}/{len(ts_files)}): {filename}") except Exception as e: print(f"加载文件 {filename} 时出错:{e}. 跳过此文件。") # 可以选择终止或跳过,这里选择跳过 continue if not clips: print("没有成功加载任何视频片段。") return # 2. 合并视频片段 print("正在合并视频片段...") try: # method=“compose”是默认方法,会处理分辨率不一致的情况(但TS文件通常一致) final_clip = concatenate_videoclips(clips, method="compose") except Exception as e: print(f"合并片段时出错:{e}") # 记得释放已加载的clip资源 for clip in clips: clip.close() return # 3. 写入输出文件 print(f"正在写入输出文件:{output_file}") try: # write_videofile 参数丰富,可以指定编码器、码率等。 # 如果不指定codec,moviepy会尝试使用默认编码器,可能导致重新编码。 # 对于TS流复制,moviepy可能不如FFmpeg直接,这里我们尝试使用‘copy’参数,但并非所有格式都支持。 # 更常见的做法是让moviepy选择合适编码器。对于最终交付,重新编码一次也未尝不可。 final_clip.write_videofile( output_file, codec='libx264', # 视频编码器,常用libx264 (H.264) audio_codec='aac', # 音频编码器 temp_audiofile='temp-audio.m4a', # 临时音频文件,处理音视频合成 remove_temp=True, # 写入完成后删除临时文件 verbose=False, # 不显示FFmpeg的详细输出 logger=None # 不使用进度条logger ) print(f"合并成功!输出文件:{output_file}") except Exception as e: print(f"写入输出文件时出错:{e}") finally: # 4. 非常重要:关闭所有clip以释放资源(尤其是文件句柄) final_clip.close() for clip in clips: clip.close() if __name__ == '__main__': ts_folder = './ts_files' output_file = './output/merged_with_moviepy.mp4' os.makedirs(os.path.dirname(output_file), exist_ok=True) merge_ts_with_moviepy(ts_folder, output_file)

5.3 注意事项与性能考量

  • 资源释放:MoviePy的VideoFileClip对象会占用系统资源(文件句柄、内存)。在循环中加载大量文件,或在合并完成后,必须调用clip.close()来显式关闭。使用with语句上下文管理器是更好的实践(但concatenate_videoclips需要所有clip对象,所以通常在最后统一关闭)。

  • 重新编码问题:write_videofile方法默认可能会对视频进行重新编码(即使你指定codec='copy',它也可能在某些容器格式下不生效)。这意味着合并过程会比FFmpeg的-c copy慢很多,并且是有损的。MoviePy更适合用于需要编辑(裁剪、加特效、调整)后再导出的场景,对于纯合并且要求无损的场景,FFmpeg是更优选择。

  • 错误处理:某个TS文件损坏或格式异常可能导致整个合并失败。代码中加入了try-except来跳过无法加载的文件,保证流程能继续。在生产环境中,你可能需要更精细的错误日志记录。

  • 内存消耗:如果一次性加载数百个高码率TS文件的clip对象,可能会导致内存占用过高。对于超多文件合并,需要分批次处理或寻求其他方案。

6. 方法三详解:二进制直接拼接(最快但限制最多)

这是最底层的方法,其原理非常简单:把每个TS文件当作一堆二进制数据(字节),按顺序读出来,再按顺序写到一个新文件里。

6.1 原理与步骤

Python的open函数在二进制模式('rb''wb')下,可以直接操作文件的原始字节。TS文件格式本身在设计上就支持简单的拼接,只要文件头尾没有额外的元数据,且编码参数一致,直接拼接后的文件仍然是有效的TS流。

核心步骤:

  1. 获取并排序TS文件列表。
  2. 以二进制写入模式打开目标文件。
  3. 循环遍历TS文件列表,以二进制读取模式打开每个文件,将其内容写入目标文件。
  4. 关闭所有文件句柄。

6.2 完整代码实现与注释

import os def merge_ts_with_binary(ts_folder, output_file, buffer_size=1024*1024): """ 使用二进制方式直接合并TS文件。 Args: ts_folder (str): 存放TS文件的文件夹路径。 output_file (str): 合并后的输出文件路径。 buffer_size (int): 读写缓冲区大小,单位字节。默认1MB,影响合并速度。 """ # 1. 获取并排序TS文件列表 ts_files = [f for f in os.listdir(ts_folder) if f.endswith('.ts')] ts_files.sort(key=lambda x: int(''.join(filter(str.isdigit, x))) if any(c.isdigit() for c in x) else x) if not ts_files: print(f"在文件夹 '{ts_folder}' 中未找到.ts文件。") return print(f"找到 {len(ts_files)} 个TS文件,开始二进制合并...") total_size = 0 try: with open(output_file, 'wb') as out_f: # 以二进制写入模式打开输出文件 for i, filename in enumerate(ts_files): file_path = os.path.join(ts_folder, filename) file_size = os.path.getsize(file_path) total_size += file_size print(f"正在合并 ({i+1}/{len(ts_files)}): {filename} ({file_size / (1024*1024):.2f} MB)") with open(file_path, 'rb') as in_f: # 以二进制读取模式打开每个TS文件 while True: # 分块读取,避免一次性加载大文件到内存 chunk = in_f.read(buffer_size) if not chunk: # 读取到文件末尾 break out_f.write(chunk) # 将块写入输出文件 print(f" 已完成") except IOError as e: print(f"文件读写错误:{e}") # 如果中途出错,可能输出文件是损坏的,可以考虑删除 if os.path.exists(output_file): os.remove(output_file) return except Exception as e: print(f"合并过程中发生未知错误:{e}") return print(f"\n合并完成!") print(f"输出文件:{output_file}") print(f"总大小:{total_size / (1024*1024):.2f} MB") if __name__ == '__main__': ts_folder = './ts_files' output_file = './output/merged_binary.ts' # 输出通常还是.ts格式 os.makedirs(os.path.dirname(output_file), exist_ok=True) # 可以调整buffer_size来优化性能,通常8MB或16MB在机械硬盘上效果更好 merge_ts_with_ffmpeg(ts_folder, output_file, buffer_size=8*1024*1024)

6.3 适用场景与重大限制

何时使用二进制拼接?

  • 来源确定:你非常确定所有这些TS文件来自同一个视频源,并且是用标准工具(如FFmpeg的segment功能)按时间或大小切割而成的。
  • 无需处理:你只需要把它们拼回去,不需要任何格式转换、检查或修复。
  • 追求速度:合并成千上万个文件时,这种方法的速度优势非常明显,因为它几乎没有计算开销。

必须警惕的限制(坑!):

  1. 编码一致性:所有TS文件的视频编码(H.264, H.265)、音频编码(AAC, MP3)、分辨率、码率、帧率等必须完全一致。如果不一致,拼接出来的文件虽然能播放,但播放器在遇到参数变化的位置时可能会卡顿、花屏或音画不同步。
  2. 文件纯净度:文件必须是“干净”的TS流。如果文件开头或结尾包含额外的元数据(如某些下载工具添加的信息)、或者文件本身有损坏,拼接后的文件可能无法播放。
  3. 时间戳连续性:TS流内部包含PCR(节目时钟参考)、PTS/DTS(显示/解码时间戳)。如果切割点不是恰好在GOP(图像组)边界,或者文件本身的时间戳不连续,直接拼接会导致时间戳跳变,播放器可能无法正确处理。
  4. 无加密:如果TS文件是加密的(例如一些流媒体的m3u8配合#EXT-X-KEY),直接二进制合并毫无意义,合并后的文件仍然是加密的,无法播放。

实操心得:在决定使用二进制拼接前,我习惯用FFmpeg快速检查一下前几个文件的编码信息。命令是:ffmpeg -i segment_0001.ts。对比几个文件输出的Stream #0:0(视频流)和Stream #0:1(音频流)信息,确保Video: h264,Audio: aac等关键信息一致。如果信息完全一致,且文件来源可靠,二进制拼接就是最快的选择。

7. 方案对比与性能实测

为了给你更直观的参考,我在同一台机器上(SSD硬盘,16GB内存)对包含100个、每个约10MB的TS文件进行了合并测试。

特性/方法FFmpeg (-c copy)MoviePy (默认编码)二进制拼接
核心原理调用外部FFmpeg工具,进行流复制使用Python库,通常重新编码Python直接进行文件IO操作
合并速度极快(~3秒)(~2分钟)最快(~1.5秒)
输出质量无损(源文件质量)有损(取决于编码参数)取决于源文件
格式兼容性优秀,自动处理容器良好,但依赖库封装,完全依赖源文件
处理异常能力,能处理部分时间戳问题中等,依赖库健壮性,任何问题都会导致输出错误
资源占用低 (FFmpeg进程)高 (Python内存)极低 (仅文件IO)
代码复杂度中等 (需处理命令和路径)低 (API简洁)极低
适用场景通用推荐,绝大多数情况Python视频处理流水线中的一环来源单一、格式纯净的TS文件快速合并

结论:

  • FFmpeg方案是综合最佳选择,在速度和质量上取得了完美平衡,且能处理大多数现实世界中的小问题。
  • 二进制拼接是特定场景下的利器,当条件满足时,它能提供无与伦比的速度。
  • MoviePy更适合作为更复杂视频编辑流程的一部分,单纯的合并任务并非其主战场。

8. 常见问题排查与实战技巧

在实际操作中,你可能会遇到以下问题:

问题1:合并后的视频播放时,中间有卡顿、跳帧或音画不同步。

  • 可能原因:TS文件序列顺序错误;个别TS文件损坏;源TS文件本身的时间戳就不连续(常见于网络抓取的流)。
  • 排查与解决:
    1. 检查顺序:用播放器(如VLC)单独播放疑似卡顿位置前后的几个TS文件,看是否本身就有问题。
    2. 使用FFmpeg检查:对有问题的时间点附近的TS文件,用ffmpeg -i problem.ts检查是否有解码错误。
    3. 尝试用FFmpeg重新编码合并:如果流复制模式有问题,可以尝试让FFmpeg重新编码一次(去掉-c copy),但这会非常慢且有损。命令如:ffmpeg -f concat -safe 0 -i filelist.txt -c:v libx264 -crf 23 -c:a aac output.mp4-crf是质量参数,值越小质量越高(18-28是常用范围)。

问题2:FFmpeg合并时报错 “Protocol not found” 或 “Unsafe file name”。

  • 可能原因:filelist.txt中的文件路径格式不对,或没有使用-safe 0参数。
  • 解决:确保filelist.txt中每行格式为file '/path/to/file.ts',并且路径用引号括起。始终在命令中加入-safe 0

问题3:合并后的文件体积远小于/大于TS文件总和。

  • 可能原因:
    • 体积小很多:可能使用了-c copy但输出容器(如.mp4)不支持输入流的某种编码(虽然罕见),导致某些流被丢弃。检查FFmpeg合并时的输出信息,看是否有“Stream mapping”和“copy”的提示。
    • 体积大很多:MoviePy默认重新编码,如果码率参数设置过高,会导致体积膨胀。FFmpeg流复制模式体积应该基本等于总和。

问题4:处理大量TS文件(如上万个)时,内存不足或效率低下。

  • 对于FFmpeg:concat协议本身支持大量文件,瓶颈在于生成巨大的filelist.txt。可以尝试分组合并,例如每1000个文件合并成一个中间文件,最后再合并中间文件。
  • 对于二进制拼接:这是最佳选择,只需注意buffer_size的设置,过小增加IO次数,过大消耗内存,通常8-16MB是个平衡点。
  • 通用技巧:使用Python的生成器(yield)来分批读取文件列表,而不是一次性将所有文件名加载到内存列表中(虽然文件名列表通常不是瓶颈)。

一个实用的调试技巧:在运行FFmpeg命令时,如果不确定发生了什么,可以先去掉-y参数,并让FFmpeg输出更详细的信息。也可以将标准错误输出重定向到文件以便查看:

# 在subprocess.Popen中,将stderr重定向到文件 with open('ffmpeg_log.txt', 'w') as log_file: process = subprocess.Popen(cmd, stderr=log_file)

然后查看ffmpeg_log.txt文件,里面通常有详细的错误原因。

9. 扩展应用:从M3U8列表自动下载并合并

很多时候,TS文件来源于一个M3U8播放列表。我们可以将这个流程自动化。思路是:解析M3U8文件,获取所有TS片段的URL,然后下载它们,最后使用上述方法之一进行合并。

这里提供一个非常简化的概念性代码框架,使用requestsconcurrent.futures进行并发下载以提升速度:

import os import re import requests from concurrent.futures import ThreadPoolExecutor, as_completed def download_ts_from_m3u8(m3u8_url, output_dir='./ts_files', max_workers=5): """ 从M3U8 URL下载所有TS片段。 注意:这是一个基础示例,未处理加密、重试、复杂相对路径等生产环境问题。 """ os.makedirs(output_dir, exist_ok=True) # 1. 下载M3U8文件内容 resp = requests.get(m3u8_url) resp.raise_for_status() m3u8_content = resp.text # 2. 解析出所有的.ts文件链接(简易正则,实际应更健壮) # 假设TS链接是完整的URL ts_urls = re.findall(r'https?://[^\s]+\.ts', m3u8_content) # 如果是相对路径,需要根据m3u8_url拼接完整URL # base_url = '/'.join(m3u8_url.split('/')[:-1]) + '/' # ts_urls = [base_url + line.strip() for line in m3u8_content.splitlines() if line.endswith('.ts')] print(f"找到 {len(ts_urls)} 个TS片段。") # 3. 定义下载单个TS文件的函数 def download_one(url, idx): filename = os.path.join(output_dir, f'segment_{idx:04d}.ts') # 添加重试机制和headers是很好的实践 r = requests.get(url, stream=True) r.raise_for_status() with open(filename, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) return filename # 4. 使用线程池并发下载 downloaded_files = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_url = {executor.submit(download_one, url, idx): url for idx, url in enumerate(ts_urls, start=1)} for future in as_completed(future_to_url): url = future_to_url[future] try: file_path = future.result() downloaded_files.append(file_path) print(f"下载成功: {os.path.basename(file_path)}") except Exception as e: print(f"下载 {url} 时出错: {e}") print("所有片段下载完成。") # 此时,`output_dir`中已经充满了按顺序命名的.ts文件 # 可以直接调用上面任何一种合并方法 # merge_ts_with_ffmpeg(output_dir, './output/final_video.mp4')

重要警告:此代码仅用于学习和演示自动化思路。在实际抓取网络视频时,请务必遵守相关网站的服务条款和法律法规,尊重版权。许多M3U8流媒体使用加密(AES-128),需要额外的解密步骤,这超出了本文的范围。

三种方法各有千秋,没有绝对的好坏,只有合不合适。经过这么多年的项目实践,我的个人体会是:FFmpeg的流复制(-c copy)是解决TS合并问题的“银弹”,在95%的情况下它都能完美工作,应该作为你的首选方案。二进制拼接则是当你对数据源有绝对掌控时的“性能利器”。而MoviePy,把它留给那些需要更多创意编辑的时刻吧。

最后再分享一个小技巧:如果你经常需要处理这类任务,可以将FFmpeg合并的代码封装成一个命令行工具,通过传递文件夹路径和输出文件名作为参数来调用,这样就能在任意地方快速使用,极大提升效率。自动化,永远是程序员对抗重复劳动的最好武器。