批量处理脚本:Windows批处理/Bash脚本/Python脚本调用FFmpeg批量处理

说实话,做音视频处理这行,最怕的就是重复劳动。你想想看,一个项目下来,几百个视频文件要转码、要提取音频、要截图……一个个手动敲FFmpeg命令?那不得累死。我早期刚入行的时候,就干过这种傻事——对着几十个文件挨个敲命令,敲到第三个就开始怀疑人生了。

后来我学乖了。批量处理,说白了就是让脚本替我们干活。今天我就把三种最常用的脚本方案——Windows批处理、Bash脚本、Python脚本——都给你捋一遍。每种方案都有它的适用场景,选对了能省一半时间。

为什么需要批量处理?

我遇到过不少新手,觉得写脚本比手动操作还麻烦。嗯,这个想法我理解。但你想过没有:

  • 手动处理100个文件,每个文件耗时30秒,总共50分钟
  • 写个脚本花10分钟,然后让脚本自动跑完,你喝杯咖啡回来就搞定了

哪个划算?而且脚本还能复用,下次遇到类似任务,改几个参数就能继续用。这笔账,你算算就明白了。

核心思路:批量处理 = 循环遍历文件 + 调用FFmpeg命令 + 输出结果管理

方案一:Windows批处理脚本(.bat)

Windows环境下,批处理是最直接的方式。我个人习惯在Windows开发机上用这个,因为不需要装任何额外环境。

批量转码MP4到H.265

@echo off
setlocal enabledelayedexpansion

set INPUT_DIR=.\input
set OUTPUT_DIR=.\output
set CRF=23

if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"

for %%f in ("%INPUT_DIR%\*.mp4") do (
    set "filename=%%~nf"
    echo 正在处理: !filename!
    ffmpeg -i "%%f" -c:v libx265 -crf %CRF% -c:a copy "%OUTPUT_DIR%\!filename!_h265.mp4"
    echo 完成: !filename!
)

echo 全部处理完毕!
pause

这段脚本的逻辑很简单:遍历input文件夹下所有.mp4文件,用FFmpeg转成H.265编码,输出到output文件夹。注意setlocal enabledelayedexpansion这行,它允许我们在循环里使用!变量名!这种延迟扩展语法——这个坑我踩过,不加的话变量值会不对。

小技巧:批处理里用%%~nf获取文件名(不含扩展名),%%~xf获取扩展名。这些修饰符很实用,建议记住。

批量提取音频流

@echo off
setlocal enabledelayedexpansion

for %%f in (*.mp4 *.mov *.avi) do (
    ffmpeg -i "%%f" -vn -acodec copy "%%~nf_audio.%%~xf"
    echo 已提取: %%f → %%~nf_audio.%%~xf
)

echo 音频提取完成!
pause

这个脚本更简洁,直接在当前目录下处理所有视频文件,提取音频流。注意-vn参数表示不要视频,-acodec copy表示音频直接复制不重新编码——这样速度快,质量无损。

注意:批处理对文件名中的空格和特殊字符很敏感。如果文件名里有空格,一定要用双引号把路径包起来,就像上面代码里那样。我曾经因为一个文件名里多了个空格,脚本跑了一半报错,排查了半天才发现。

方案二:Bash脚本(.sh)

到了Linux或macOS环境,Bash脚本就是标配了。我在服务器上做批量处理时,几乎都用Bash。它的字符串处理和管道操作比批处理灵活得多。

批量截取视频缩略图

#!/bin/bash

INPUT_DIR="./videos"
OUTPUT_DIR="./thumbnails"
TIME="00:00:05"

mkdir -p "$OUTPUT_DIR"

for video in "$INPUT_DIR"/*.mp4; do
    filename=$(basename "$video" .mp4)
    echo "正在生成缩略图: $filename"
    ffmpeg -i "$video" -ss "$TIME" -vframes 1 "$OUTPUT_DIR/${filename}_thumb.jpg"
done

echo "全部缩略图生成完毕!"

这个脚本从每个视频的第5秒截取一帧作为缩略图。basename命令用来提取文件名,-vframes 1表示只输出一帧。速度很快,几百个视频几分钟就能跑完。

批量合并音频和视频

#!/bin/bash

for video in *.mp4; do
    base="${video%.mp4}"
    audio="${base}.aac"
    
    if [ -f "$audio" ]; then
        echo "合并: $video + $audio"
        ffmpeg -i "$video" -i "$audio" -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 "${base}_merged.mp4"
    else
        echo "跳过 $video:未找到对应的音频文件 $audio"
    fi
done

这段脚本会检查每个视频文件是否有同名的.aac音频文件,有的话就合并,没有就跳过。我在做多语言配音项目时经常用这个——视频是同一份,音频有多个语言版本,批量合并效率极高。

Bash小技巧:${variable%pattern}可以去掉变量末尾匹配的部分,比如${filename%.mp4}就去掉.mp4后缀。这个语法比用basename更灵活,建议掌握。

方案三:Python脚本

说实话,如果任务比较复杂,我首选Python。批处理和Bash脚本适合简单的循环调用,但一旦涉及条件判断、异常处理、并行执行,Python的优势就出来了。

批量处理框架(带日志和错误处理)

import subprocess
import os
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('batch_process.log'),
        logging.StreamHandler()
    ]
)

INPUT_DIR = "./input"
OUTPUT_DIR = "./output"
MAX_WORKERS = 4  # 并行任务数

def process_video(filename):
    """处理单个视频文件"""
    input_path = os.path.join(INPUT_DIR, filename)
    output_path = os.path.join(OUTPUT_DIR, f"processed_{filename}")
    
    # 构建FFmpeg命令
    cmd = [
        'ffmpeg',
        '-i', input_path,
        '-c:v', 'libx264',
        '-preset', 'medium',
        '-crf', '23',
        '-c:a', 'aac',
        '-b:a', '128k',
        '-y',  # 覆盖输出文件
        output_path
    ]
    
    try:
        # 执行命令
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=300  # 5分钟超时
        )
        
        if result.returncode == 0:
            logging.info(f"成功: {filename}")
            return True
        else:
            logging.error(f"失败: {filename}\n{result.stderr}")
            return False
            
    except subprocess.TimeoutExpired:
        logging.error(f"超时: {filename}")
        return False
    except Exception as e:
        logging.error(f"异常: {filename} - {str(e)}")
        return False

def main():
    # 创建输出目录
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    
    # 获取所有视频文件
    videos = [f for f in os.listdir(INPUT_DIR) 
              if f.lower().endswith(('.mp4', '.mov', '.avi'))]
    
    logging.info(f"找到 {len(videos)} 个视频文件")
    
    # 使用线程池并行处理
    success_count = 0
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        futures = {executor.submit(process_video, v): v for v in videos}
        
        for future in as_completed(futures):
            if future.result():
                success_count += 1
    
    logging.info(f"处理完成: 成功 {success_count}/{len(videos)}")

if __name__ == "__main__":
    main()

这个脚本我用了很久,基本框架一直没变。几个关键点:

  • 日志系统:同时输出到控制台和文件,方便事后排查问题
  • 异常处理:每个文件独立处理,一个失败不影响其他文件
  • 超时控制:防止某个文件卡死导致整个任务挂起
  • 并行处理:用ThreadPoolExecutor同时处理多个文件,充分利用CPU

注意:并行数不是越多越好。FFmpeg本身就会用多核,如果你同时跑太多进程,反而会因为CPU争抢导致性能下降。我一般建议并行数不超过CPU物理核心数的一半。比如8核CPU,设4个并行任务比较合适。

三种方案对比

特性 Windows批处理 Bash脚本 Python脚本
学习成本 中高
跨平台 仅Windows Linux/macOS 全平台
错误处理
并行处理 困难 较困难 简单
适用场景 简单批量、Windows环境 服务器、Linux环境 复杂逻辑、生产环境

实战建议

我个人经验是:

  • 临时任务:用批处理或Bash,写起来快,用完就删
  • 定期任务:用Python,加上日志和错误通知,出了问题能及时发现
  • 生产环境:Python + 任务队列(比如Celery),支持断点续传和任务调度

嗯,说到断点续传,我踩过一个坑。有一次处理2000多个视频,跑到第1800个的时候服务器重启了……没有断点续传,只能从头再来。后来我学乖了,每次处理前先记录已完成的文件列表,脚本启动时先检查哪些已经处理过了,跳过它们。

核心原则:批量处理脚本要具备「幂等性」——同一个脚本跑多次,结果应该是一样的。这样即使中途失败,重新跑一次也不会出问题。

知识体系图

FFmpeg批量处理脚本方案 Windows批处理 Bash脚本 Python脚本 特点 • 无需安装环境 • 适合Windows平台 • 简单循环处理 特点 • 管道操作强大 • 服务器首选 • 字符串处理灵活 特点 • 完整异常处理 • 支持并行处理 • 跨平台通用 核心逻辑:遍历文件 → 调用FFmpeg → 输出结果 最佳实践:幂等性设计 + 日志记录 + 异常处理

这张图把三种方案的核心逻辑和特点都串起来了。你看,不管用哪种脚本,底层逻辑都是一样的——遍历文件、调用FFmpeg、输出结果。区别在于每种方案在错误处理、并行能力、跨平台支持上的强弱。

最后说一句:别纠结于「哪种脚本最好」。没有最好的,只有最适合当前场景的。Windows桌面环境用批处理,Linux服务器用Bash,复杂生产环境用Python——各司其职,这才是工程思维。

公众号:蓝海资料掘金营,微信deep3321