Python视频压缩实战:从码率计算到自动化批量处理
前言
作为开发者,日常少不了和视频文件打交道——录了个 Bug 复现视频 200MB,想发给同事结果企业微信限制 50MB;游戏录屏 1GB,传 GitHub Release 要等到地老天荒。手动打开 FFmpeg 敲命令能解决,但每次都算码率、调 crf、试参数实在太低效了。最近我封装了一套 Python 脚本,把视频压缩流程自动化了,同时也发现了一些实用的在线工具。这篇文章把经验分享出来。
基础概念:视频文件大小到底由什么决定
在动手写代码之前,先搞清楚几个核心概念。
一个视频文件的大小主要由三个参数决定:
文件大小 ≈ 码率(bitrate) × 时长 + 音频大小其中视频码率是压缩时最需要关注的变量,它又受以下因素影响:
| 参数 | 作用 | 典型值 |
|---|---|---|
| 分辨率 | 画面像素尺寸 | 1080p (1920×1080)、720p (1280×720) |
| 帧率 | 每秒画面数量 | 30fps、60fps |
| 编码格式 | 压缩算法 | H.264 (兼容性最好)、H.265 (压缩率更高)、AV1 (最新) |
| CRF | 恒定质量因子,越低画质越好 | 18(接近无损)、23(默认)、28(可接受) |
码率反推公式
当我们需要「压缩到指定大小」时,核心是把目标文件大小反推为目标码率:
python
# 目标码率计算(kbps) # 目标大小单位 MB,时长单位秒 target_bitrate = (target_size_mb * 8192) / duration_seconds - audio_bitrate举个例子:一段 5 分钟的视频要压缩到 8MB(Discord 限制),音频码率取 128kbps:
python
target_size_mb = 8 duration_seconds = 5 * 60 # 300秒 audio_bitrate = 128 target_bitrate = (8 * 8192) / 300 - 128 # 结果 ≈ 90 kbps print(f"目标视频码率: {target_bitrate:.0f} kbps")这就是每次手动敲 FFmpeg 命令时心里要做的那道数学题。接下来把它自动化。
环境准备
- Python 3.8+
- 系统需安装 FFmpeg 并在 PATH 中(https://ffmpeg.org/download.html)
- 依赖库:Python 自带 subprocess,无需额外安装
验证安装:
bash
ffmpeg -version # 应输出版本信息 python -c "import subprocess; print('OK')"实现步骤
Step 1: 用 ffprobe 提取视频信息
压缩的第一步是获取视频的元数据——分辨率、码率、时长、编码格式。可以用 FFmpeg 自带的 ffprobe 提取:
python
import subprocess import json from pathlib import Path def get_video_info(video_path: str) -> dict: """提取视频元数据 Args: video_path: 视频文件路径 Returns: 包含分辨率、码率、时长等信息的字典 """ cmd = [ "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", str(video_path), ] result = subprocess.run(cmd, capture_output=True, text=True, check=True) data = json.loads(result.stdout) # 从 streams 中找视频流 video_stream = None audio_stream = None for stream in data.get("streams", []): if stream["codec_type"] == "video": video_stream = stream elif stream["codec_type"] == "audio": audio_stream = stream if not video_stream: raise ValueError("未找到视频流") info = { "path": str(video_path), "duration": float(data["format"]["duration"]), "file_size_mb": Path(video_path).stat().st_size / (1024 * 1024), "resolution": f"{video_stream['width']}x{video_stream['height']}", "fps": eval(video_stream.get("r_frame_rate", "0/1")), "video_codec": video_stream.get("codec_name", "unknown"), "video_bitrate_kbps": int(video_stream.get("bit_rate", 0)) // 1000 if video_stream.get("bit_rate") else 0, } if audio_stream: info["audio_codec"] = audio_stream.get("codec_name", "unknown") info["audio_bitrate_kbps"] = ( int(audio_stream.get("bit_rate", 0)) // 1000 if audio_stream.get("bit_rate") else 0 ) return infoStep 2: 根据目标大小计算压缩参数
有了视频信息后,下一步是根据目标文件大小,自动计算 FFmpeg 所需的压缩参数:
python
def calculate_compression_params( video_info: dict, target_size_mb: float, audio_bitrate_kbps: int = 64, codec: str = "hevc", ) -> dict: """根据目标文件大小计算 FFmpeg 压缩参数 Args: video_info: get_video_info() 返回的视频信息 target_size_mb: 目标文件大小(MB) audio_bitrate_kbps: 目标音频码率(kbps),默认 64 codec: 视频编码器,'hevc'=H.265, 'h264'=H.264 Returns: 包含码率、编码器等参数的字典 """ # 将目标大小转为 kbits target_size_kbits = target_size_mb * 8192 # 1 MB = 8192 kbits # 分配:音频占固定部分,剩余给视频 audio_kbits = audio_bitrate_kbps * video_info["duration"] video_kbits = target_size_kbits - audio_kbits # 反推视频码率 target_video_bitrate = video_kbits / video_info["duration"] # H.265 比 H.264 约节省 30%-50% 码率 codec_map = { "hevc": { "encoder": "libx265", "crf": 28, # H.265 默认 CRF "efficiency": 1.0, # 基准 }, "h264": { "encoder": "libx264", "crf": 23, # H.264 默认 CRF "efficiency": 0.6, # 同样画质需更多码率 }, } config = codec_map.get(codec, codec_map["hevc"]) # 根据压缩比决定是否降低分辨率 original_bitrate = video_info.get("video_bitrate_kbps", 0) or ( video_info["file_size_mb"] * 8192 / video_info["duration"] ) compression_ratio = original_bitrate / max(target_video_bitrate, 1) # 如果压缩比 > 5,建议降低分辨率 new_resolution = video_info["resolution"] if compression_ratio > 5 and "x" in new_resolution: w, h = map(int, new_resolution.split("x")) if h >= 1080: new_resolution = "1280x720" elif h >= 720: new_resolution = "960x540" return { "target_video_bitrate_kbps": int(target_video_bitrate), "target_audio_bitrate_kbps": audio_bitrate_kbps, "encoder": config["encoder"], "crf": config["crf"], "resolution": new_resolution, "compression_ratio": round(compression_ratio, 1), }Step 3: 执行压缩
最后一步:拼装 FFmpeg 命令并执行:
python
def compress_video( input_path: str, output_path: str, params: dict, overwrite: bool = True, ) -> bool: """执行视频压缩 Args: input_path: 输入视频路径 output_path: 输出路径 params: calculate_compression_params() 返回的参数字典 overwrite: 是否覆盖已有输出文件 Returns: 压缩是否成功 """ cmd = [ "ffmpeg", "-i", str(input_path), "-c:v", params["encoder"], "-b:v", f"{params['target_video_bitrate_kbps']}k", "-maxrate", f"{params['target_video_bitrate_kbps']}k", "-bufsize", f"{params['target_video_bitrate_kbps'] * 2}k", "-c:a", "aac", "-b:a", f"{params['target_audio_bitrate_kbps']}k", "-preset", "medium", ] # 如果需要降分辨率 if params["resolution"] != get_video_info(input_path)["resolution"]: cmd.extend(["-vf", f"scale={params['resolution']}"]) if overwrite: cmd.append("-y") cmd.append(str(output_path)) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(f"压缩失败: {result.stderr[-200:]}") return False output_size = Path(output_path).stat().st_size / (1024 * 1024) print(f"压缩完成: {output_size:.1f}MB") return True def batch_compress(input_dir: str, output_dir: str, target_size_mb: float = 20): """批量压缩目录下的所有视频""" input_path = Path(input_dir) output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) video_extensions = {".mp4", ".mov", ".avi", ".mkv", ".webm"} videos = [f for f in input_path.iterdir() if f.suffix.lower() in video_extensions] print(f"找到 {len(videos)} 个视频文件,目标大小: {target_size_mb}MB\n") success = 0 for i, video in enumerate(videos, 1): print(f"[{i}/{len(videos)}] 处理: {video.name}") try: info = get_video_info(str(video)) params = calculate_compression_params(info, target_size_mb) out_file = output_path / f"{video.stem}_compressed.mp4" if compress_video(str(video), str(out_file), params): success += 1 except Exception as e: print(f" 错误: {e}") print(f"\n完成: {success}/{len(videos)} 个文件压缩成功")完整代码
把以上三步整合到一个脚本中,保存为video_compressor.py:
python
#!/usr/bin/env python3 """ 视频批量压缩脚本 用法: python video_compressor.py input.mp4 8 # 压缩单个视频到 8MB python video_compressor.py ./videos/ ./output/ 20 # 批量压缩目录到 20MB """ import sys import subprocess import json from pathlib import Path def get_video_info(video_path: str) -> dict: cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", str(video_path)] result = subprocess.run(cmd, capture_output=True, text=True, check=True) data = json.loads(result.stdout) video_stream = next((s for s in data.get("streams", []) if s["codec_type"] == "video"), None) audio_stream = next((s for s in data.get("streams", []) if s["codec_type"] == "audio"), None) if not video_stream: raise ValueError("未找到视频流") info = { "duration": float(data["format"]["duration"]), "file_size_mb": Path(video_path).stat().st_size / 1048576, "resolution": f"{video_stream['width']}x{video_stream['height']}", "video_bitrate_kbps": int(video_stream.get("bit_rate", 0)) // 1000 if video_stream.get("bit_rate") else 0, } return info def calculate_params(info: dict, target_mb: float, codec: str = "hevc") -> dict: target_kbits = target_mb * 8192 audio_kbps = 64 target_video_bitrate = (target_kbits - audio_kbps * info["duration"] ) / info["duration"] codec_config = { "hevc": {"encoder": "libx265", "crf": 28}, "h264": {"encoder": "libx264", "crf": 23}, } config = codec_config.get(codec, codec_config["hevc"]) return { "encoder": config["encoder"], "bitrate": int(target_video_bitrate), "audio_bitrate": audio_kbps, "resolution": info["resolution"], } def compress(input_path: str, output_path: str, params: dict) -> bool: cmd = ["ffmpeg", "-y", "-i", str(input_path), "-c:v", params["encoder"], "-b:v", f"{params['bitrate']}k", "-c:a", "aac", "-b:a", f"{params['audio_bitrate']}k", str(output_path)] result = subprocess.run(cmd, capture_output=True, text=True) return result.returncode == 0 def main(): if len(sys.argv) < 3: print("用法: python video_compressor.py <输入> <目标MB>") print(" python video_compressor.py video.mp4 8") print(" python video_compressor.py ./input/ ./output/ 20") sys.exit(1) target_mb = float(sys.argv[-1]) source = Path(sys.argv[-3] if len(sys.argv) == 4 else sys.argv[-2]) if source.is_file(): info = get_video_info(str(source)) params = calculate_params(info, target_mb) out = source.parent / f"{source.stem}_compressed.mp4" if compress(str(source), str(out), params): output_size = out.stat().st_size / 1048576 print(f"完成: {source.name} → {output_size:.1f}MB (目标 {target_mb}MB)") elif source.is_dir(): output_dir = Path(sys.argv[-2]) if len(sys.argv) == 4 else Path("compressed") output_dir.mkdir(exist_ok=True) exts = {".mp4", ".mov", ".avi", ".mkv", ".webm"} videos = [f for f in source.iterdir() if f.suffix.lower() in exts] for v in videos: info = get_video_info(str(v)) params = calculate_params(info, target_mb) out = output_dir / f"{v.stem}_compressed.mp4" if compress(str(v), str(out), params): print(f"完成: {v.name}") if __name__ == "__main__": main()运行效果
单文件压缩
bash
python video_compressor.py Screen_Recording_2024.mp4 8 # 输出: # 完成: Screen_Recording_2024.mp4 → 7.8MB (目标 8MB)原始视频 327MB(1080p 30fps H.264),压缩到 7.8MB,画质虽然有轻微下降,但用于发给同事看 Bug 复现步骤完全够用。
批量压缩
bash
python video_compressor.py ./raw_videos/ ./compressed/ 20 # 输出: # 完成: Gameplay_Footage.mp4 # 完成: Meeting_Recording.mov # 完成: Demo_Walkthrough.avi一键处理整个目录,每个文件都按目标大小自动计算最优码率。
什么时候用脚本,什么时候用在线工具
上面的自动化方案适合批量处理和需要精确控制编码参数的场景。但还有一种常见情况——偶尔处理一两个视频,懒得打开终端敲命令。这种时候用浏览器端工具更省事。
VideoCompress 就是这类工具的代表:打开网页 → 拖入视频 → 输入目标大小(比如 8MB)→ 下载。不需要算码率、不需要装 FFmpeg。它本质上是把「码率反推 + 编码压缩」这个流程完全黑盒化了。
几个关键参数(实测所得):
| 维度 | 数据 |
|---|---|
| 输入格式 | 40+ 种(MP4 / MOV / AVI / MKV / WebM 等) |
| 单文件限制 | 最大 1GB |
| 免费额度 | 每月 30 积分 |
| 水印 | 无 |
| 注册要求 | 无需强制注册 |
| 隐私 | 上传加密,24 小时自动删除 |
| 画质 | 有损压缩,目标「肉眼无差别」(visually lossless) |
一句话总结选型逻辑:
- 批量处理、精确控制参数 → Python 脚本 + FFmpeg
- 偶尔快速压缩一两个视频、不想折腾 → VideoCompress 直接浏览器搞定
- 专业影视级编码、逐帧调色 → HandBrake / FFmpeg 命令行
