1. 媒体批处理的特点
一句话总结: 媒体处理是「CPU 密集 + 长耗时 + 单文件可独立重试」的典型场景,这三个特征决定了脚本必须并行、必须能续跑。
一张图片缩放几百毫秒,一段视频转码可能几十分钟。这意味着脚本的瓶颈永远是 CPU 而不是磁盘 IO,因此并发度可以贴着核数走;同时因为任务粒度高、相互独立,任何失败都可以只重跑单个文件,不需要从头来。
# 串行处理 100 张图片
time for f in *.jpg; do convert "$f" -resize 800x "${f%.jpg}_small.jpg"; done
# 并行处理:立刻快好几倍
time ls *.jpg | xargs -P "$(nproc)" -I {} sh -c \
'convert "$1" -resize 800x "${1%.jpg}_small.jpg"' _ {}
1.1 幂等与断点续跑
一句话总结: 「输出存在且比输入新」就跳过,是最简单可靠的续跑判据。
# 幂等处理:输出比输入新则跳过
process_if_needed() {
local src="$1" dst="$2"
if [[ -f "$dst" && "$dst" -nt "$src" ]]; then
return 0 # 已处理且更新,跳过
fi
convert "$src" -resize 800x "$dst"
}
1.2 依赖探测
一句话总结: 入口处检查 magick/convert 与 ffmpeg/ffprobe 是否存在,缺依赖立即失败而不是跑到一半崩。
need() {
command -v "$1" >/dev/null 2>&1 || { echo "缺少依赖: $1" >&2; exit 1; }
}
need ffmpeg
need ffprobe
command -v magick >/dev/null 2>&1 && IM=magick || IM=convert
2. ImageMagick 批量处理
一句话总结: ImageMagick 的
mogrify是「原地批量」、convert是「一对一」,选错会覆盖原图。
2.1 尺寸与格式转换
一句话总结: 缩放用
-resize WxH保持比例,>与^修饰符控制「只缩小」与「铺满裁切」。
# 只缩小不放大(大图变小,小图不动)
mogrify -resize '1600x1600>' *.jpg
# 铺满裁切:先按短边缩放再裁中心
convert in.jpg -resize '800x800^' -gravity center -extent 800x800 out.jpg
# 转 WebP,质量 82
convert in.jpg -quality 82 out.webp
# 批量转换格式(mogrify 原地改名)
mogrify -format webp -quality 82 *.png
2.2 水印与合成
一句话总结:
-gravity定位置、-geometry定偏移、-dissolve定透明度,三件套搞定水印。
# 右下角半透明水印
convert in.jpg watermark.png \
-gravity southeast -geometry +20+20 -compose dissolve -define compose:args=40 \
-composite out.jpg
# 批量加水印
for f in *.jpg; do
convert "$f" watermark.png -gravity southeast -geometry +20+20 \
-compose dissolve -define compose:args=40 -composite "wm_$f"
done
2.3 联系表与缩略图
一句话总结:
montage把多图拼成一张联系表,适合快速人工审片。
# 生成 4x4 缩略图联系表
montage *.jpg -thumbnail 200x200 -tile 4x4 -geometry +4+4 contact.jpg
# 用占位色填充缺失的格子
montage *.jpg -thumbnail 200x200 -tile 4x4 -background '#eeeeee' contact.jpg
2.4 资源限制
一句话总结: ImageMagick 默认会吃掉大量内存与磁盘,批量处理前必须设上限,否则整机被拖垮。
# 限制内存、磁盘与线程
export MAGICK_MEMORY_LIMIT=512MiB
export MAGICK_MAP_LIMIT=1GiB
export MAGICK_DISK_LIMIT=2GiB
export MAGICK_THREAD_LIMIT=2 # 每进程 2 线程,配合外层并发
mogrify -resize '1200x1200>' *.jpg
3. ffmpeg 批量转码
一句话总结: ffmpeg 参数顺序决定一切——输入在前、输出在后,
-c copy能复制流就不重编码。
# 无重编码封装转换(秒级完成)
ffmpeg -i in.mkv -c copy out.mp4
# H.264 转码,CRF 23 是画质与体积的平衡点
ffmpeg -i in.mov -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k out.mp4
# 提取音频
ffmpeg -i in.mp4 -vn -c:a libmp3lame -q:a 2 out.mp3
3.1 常用批量模式
一句话总结: 批量转码的核心是「遍历 + 输出命名 + 并发」,与图片处理套路完全一致。
# 批量转 720p
find . -name '*.mov' -print0 | xargs -0 -P 2 -I {} sh -c '
ffmpeg -nostdin -y -i "$1" -vf scale=-2:720 -c:v libx264 -crf 23 \
-c:a aac -b:a 128k "${1%.mov}_720p.mp4" </dev/null
' _ {}
# 批量生成缩略图(第 1 秒的帧)
find . -name '*.mp4' -print0 | xargs -0 -P "$(nproc)" -I {} sh -c '
ffmpeg -nostdin -y -ss 1 -i "$1" -frames:v 1 "${1%.mp4}.jpg" </dev/null
' _ {}
3.2 关键参数解析
一句话总结:
-nostdin防 ffmpeg 抢 stdin、-y覆盖输出、-vf scale=-2:H保证宽高为偶数。
# -nostdin:并发时防止多个 ffmpeg 争抢终端输入
# -y:已存在输出直接覆盖,配合幂等判断使用
# scale=-2:720:高度 720,宽度按比例取整到偶数(H.264 要求偶数)
ffmpeg -nostdin -y -i in.mp4 -vf scale=-2:720 -c:v libx264 out.mp4
3.3 硬件加速
一句话总结: 有 GPU 时用
h264_nvenc或h264_videotoolbox,速度可提升数倍,代价是画质略降。
# NVIDIA NVENC
ffmpeg -hwaccel cuda -i in.mp4 -c:v h264_nvenc -preset p4 -cq 23 out.mp4
# macOS VideoToolbox
ffmpeg -i in.mp4 -c:v h264_videotoolbox -b:v 4M out.mp4
# 探测可用编码器
ffmpeg -hide_banner -encoders | grep -E 'nvenc|videotoolbox|qsv'
4. 并行与断点续跑
一句话总结: 并发度按核数与内存算,任务清单落地成文件,失败项单独记录后重跑。
4.1 并发度计算
一句话总结: 视频转码建议并发 = 核数的一半(每任务本身多线程),图片处理可以等于核数。
# 视频:每任务自带多线程,并发数取核数一半
jobs=$(( $(nproc) / 2 ))
[[ $jobs -lt 1 ]] && jobs=1
# 图片:单线程任务,可跑满核数
img_jobs=$(nproc)
4.2 失败清单与重跑
一句话总结: 每个任务把失败写到独立文件,汇总后只重跑失败项,比全量重来快得多。
# 每个任务独立记录状态
run_task() {
local src="$1" dst="$2"
if ffmpeg -nostdin -y -loglevel error -i "$src" -c:v libx264 "$dst" </dev/null; then
printf '%s\n' "$src" >> done.txt
else
printf '%s\n' "$src" >> failed.txt
fi
}
export -f run_task
# 重跑:只处理失败清单
[[ -f failed.txt ]] && xargs -a failed.txt -P "$jobs" -I {} ./retry.sh {}
4.3 断点续跑脚本骨架
一句话总结: 「已完成清单」是续跑的唯一真相来源,启动时先加载它再过滤任务列表。
#!/usr/bin/env bash
set -euo pipefail
DONE=done.txt; touch "$DONE"
# 生成待处理清单:排除已完成
pending() {
comm -23 <(find . -name '*.mov' -printf '%p\n' | sort) <(sort -u "$DONE")
}
pending | xargs -P "$jobs" -I {} ./process-one.sh {}
5. 元数据与 EXIF
一句话总结: 元数据既是资产也是隐私风险,发布前要剥离 GPS 与设备信息,归档时则要保留时间与方向。
# 查看 EXIF(exiftool 比 identify -verbose 友好得多)
exiftool -s -DateTimeOriginal -GPSLatitude -GPSLongitude -Model photo.jpg
# 剥离所有元数据
exiftool -all= -overwrite_original photo.jpg
# 只保留时间,剥离 GPS 与设备
exiftool -GPS:all= -Model= -Software= -overwrite_original photo.jpg
5.1 批量读取与导出
一句话总结: exiftool 支持
-csv与-json输出,批量审计元数据时直接生成可分析的表。
# 导出所有图片的关键元数据为 CSV
exiftool -csv -r -DateTimeOriginal -Model -ImageSize -GPSPosition . > meta.csv
# 按拍摄时间重命名(需先安装 Time::Piece)
exiftool '-FileName<DateTimeOriginal' -d '%Y%m%d_%H%M%S%%-c.%%e' *.jpg
5.2 时间戳一致性
一句话总结: 文件系统时间与 EXIF 时间经常不一致,归档前统一用 EXIF 时间校正 mtime。
# 用 EXIF 时间设置文件 mtime
exiftool -overwrite_original -P '-FileModifyDate<DateTimeOriginal' *.jpg
# 校验:比较两者差异
exiftool -T -DateTimeOriginal -FileModifyDate *.jpg | awk '$1 != $2'
6. 校验与质量检查
一句话总结: 转码后必须校验产物完整性,否则「成功」的输出可能是 0 字节或截断文件。
# 校验图片可解码
identify -regard-warnings in.jpg >/dev/null && echo "图片有效"
# 校验视频完整性
ffprobe -v error -show_entries format=duration -of default=nw=1 in.mp4
# 检查 0 字节与损坏文件
find . -name '*.jpg' -size 0 -print
find . -name '*.jpg' -print0 | xargs -0 -P 4 -I {} sh -c \
'identify -regard-warnings "$1" >/dev/null 2>&1 || echo "损坏: $1"' _ {}
6.1 尺寸与体积对比
一句话总结: 转码前后的体积比是发现「参数写错导致输出暴涨」的最快指标。
# 统计压缩比
total_in=0; total_out=0
while IFS= read -r f; do
out="${f%.jpg}.webp"
[[ -f "$out" ]] || continue
total_in=$(( total_in + $(stat -c%s "$f") ))
total_out=$(( total_out + $(stat -c%s "$out") ))
done < <(find . -name '*.jpg')
echo "原始 $(( total_in / 1024 ))KB -> 输出 $(( total_out / 1024 ))KB"
6.2 视觉回归抽样
一句话总结: 自动对比原图与处理后图的感知哈希,差异过大的样本人工复核。
# 计算感知哈希(需 ImageMagick 支持 phash)
phash_of() { convert "$1" -resize 32x32 -colorspace gray -format '%#' info:; }
for f in *.jpg; do
out="${f%.jpg}_small.jpg"; [[ -f "$out" ]] || continue
d=$(compare -metric PHASH "$f" "$out" null: 2>&1 || true)
awk -v d="$d" -v f="$f" 'BEGIN { if (d+0 > 15) print "差异大: " f, d }'
done
7. 实战:可续跑的媒体处理流水线
一句话总结: 把探测、处理、校验、重试串成一条流水线,用状态文件保证任何中断都能接着跑。
#!/usr/bin/env bash
set -euo pipefail
SRC_DIR="${1:?用法: media-pipeline.sh <源目录>}"
OUT_DIR="${OUT_DIR:-./out}"
STATE_DIR=".media-state"
JOBS="${JOBS:-$(( $(nproc) / 2 ))}"
[[ $JOBS -lt 1 ]] && JOBS=1
mkdir -p "$OUT_DIR" "$STATE_DIR"
DONE="$STATE_DIR/done.list"; FAILED="$STATE_DIR/failed.list"
touch "$DONE"; : > "$FAILED"
process_one() {
local src="$1" dst
grep -qxF "$src" "$DONE" 2>/dev/null && return 0
dst="$OUT_DIR/${src#"$SRC_DIR"/}"; dst="${dst%.*}.mp4"
if ! ffmpeg -nostdin -y -loglevel error -i "$src" \
-vf scale=-2:720 -c:v libx264 -crf 23 -c:a aac -b:a 128k \
"$dst" </dev/null; then
printf '%s\n' "$src" >> "$FAILED"; return 1
fi
if ffprobe -v error -show_entries format=duration -of default=nw=1 "$dst" \
| grep -q '^duration='; then
printf '%s\n' "$src" >> "$DONE"
else
printf '%s\n' "$src" >> "$FAILED"
fi
}
export -f process_one
export SRC_DIR OUT_DIR DONE FAILED
find "$SRC_DIR" -type f \( -name '*.mov' -o -name '*.mkv' -o -name '*.avi' \) -print0 \
| xargs -0 -P "$JOBS" -I {} bash -c 'process_one "$@"' _ {}
echo "完成: $(wc -l < "$DONE") 失败: $(wc -l < "$FAILED")"
[[ -s "$FAILED" ]] && { echo "失败清单:"; cat "$FAILED"; exit 1; }
7.1 失败重跑
一句话总结: 流水线跑完只需针对失败清单重跑,成功后从失败清单移除。
# 重跑失败项
if [[ -s "$FAILED" ]]; then
cp "$FAILED" "$FAILED.bak"
: > "$FAILED"
xargs -a "$FAILED.bak" -P "$JOBS" -I {} bash -c 'process_one "$@"' _ {}
fi
7.2 资源与优雅退出
一句话总结: 加
trap清理临时文件,加nice/ionice让流水线不抢占生产服务资源。
# 优雅退出与资源让步
cleanup() { rm -f "$OUT_DIR"/*.tmp.mp4 2>/dev/null || true; }
trap cleanup EXIT INT TERM
# 低优先级运行
nice -n 19 ionice -c3 bash media-pipeline.sh /data/videos
8. 总结
| 环节 | 要点 |
|---|---|
| 场景特征 | CPU 密集、长耗时、任务独立,天然适合并行 |
| 幂等 | 输出比输入新则跳过,是最可靠的续跑判据 |
| ImageMagick | mogrify 原地批量、convert 一对一,务必设资源上限 |
| ffmpeg | 参数顺序敏感,-nostdin -y 是并发必备 |
| 并发 | 视频取核数一半、图片取核数,按内存再收紧 |
| 状态 | done/failed 清单落地,失败项单独重跑 |
| 元数据 | 发布剥离 GPS 与设备,归档保留时间并校正 mtime |
| 校验 | ffprobe/identify 验证产物,0 字节与截断都要拦 |
媒体批处理与文本批处理最大的不同在于成本:一次错误的视频转码可能烧掉几小时 CPU,所以「先校验产物、再记录状态」必须写进流程。把幂等判断、失败清单、资源上限三件事做扎实,流水线就能在无人值守的夜里稳定跑完。下一站我们离开本机,去看云上资源的批量管理。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。