编程学习网 > 编程语言 > Python > 这五个 python 视频处理代码,一键复制就能用!
2026
10-12

这五个 python 视频处理代码,一键复制就能用!


先说一个可能有点反常识的结论:用 python 处理视频,真正干活的那个程序并不是 python。

是 ffmpeg。

python 在这件事里干的活特别轻,把参数拼成一条命令丢给 ffmpeg,跑完再把文件收回来。

有小伙伴可能会问,那 moviepy 呢。

moviepy 的官方文档第一段就写着,它读写视频依赖 ffmpeg 这个软件,不用自己装,ImageIO 会在你第一次用的时候自动下载。文档里也标着,2.0 是一次破坏性变更,升级要改写法。

也就是说,那些包装库最后也是把活儿转给 ffmpeg,中间多了一层翻译。卡卡这次干脆把这层翻译去掉,直接用 python 调 ffmpeg。

好处很直接,不用 pip install 任何东西,也就少了一处会随版本变动的地方。

代价是本机得有一个 ffmpeg。命令行敲一句 ffmpeg -version,有版本号出来就行。没有的话去官网下个解压包,把 bin 目录加进环境变量。

下面这五段,卡卡拿三段素材从头到尾跑过。一段 1920 乘 1080 的十秒录屏、一段 1280 乘 720 的八秒 mov、一段 1080 乘 1920 的六秒竖屏,加起来 13 MB 出头。文章里的体积和时长都是这三段真跑出来的,不是估的。

补一句口径:卡卡用的 ffmpeg 是 N-116875-g3d0d0f68d5(2024 年 9 月 5 日构建),系统是 Windows,python 3.13。每段的耗时都是三段素材合起来一次跑完的时间,不是单段。

每段开头那几行是一样的骨架,找到 ffmpeg、指定待处理的文件夹、指定输出文件夹。卡卡故意不把它抽出来单放,这样你复制哪一段,哪一段都能自己跑起来。

一、批量把视频转成 GIF,颜色脏不脏就看有没有跑两遍

聊天窗口里发一段视频,对方得点开才看得到;发一张 GIF,它自己就在对话框里动起来了。所以卡卡经常要把录屏掐一小段转成 GIF。

最省事的写法是一条命令直接转,但出来的 GIF 颜色发脏,渐变的地方一圈一圈的色块。原因是一次编码只能用一张固定调色板,画面颜色一多就顾不过来。

ffmpeg 里专门有两个滤镜干这件事:palettegen 负责生成调色板,paletteuse 负责拿它上色。所以要跑两遍,第一遍先扫这四秒的画面,生成一张只属于这段视频的调色板,第二遍再逐帧上色。

# 片段一:批量把视频转成 GIF
import shutil
import subprocess
from pathlib import Path
FFMPEG = shutil.which("ffmpeg") or r"D:\ffmpeg\bin\ffmpeg.exe"
SRC = Path(r"D:\videos")
OUT = Path(r"D:\videos\out")
OUT.mkdir(parents=True, exist_ok=True)

def video_to_gif(src, dst, start="00:00:01", seconds=4, fps=12, width=480):
    """取视频中间 4 秒,转成宽 480 的 GIF"""
    palette = dst.with_name(dst.stem + "_palette.png")
    # 第一遍:先扫一遍画面,生成这张图专用的调色板
    subprocess.run(
        [FFMPEG, "-y", "-ss", start, "-t", str(seconds), "-i", str(src),
         "-vf", f"fps={fps},scale={width}:-1:flags=lanczos,palettegen",
         str(palette)],
        check=True, capture_output=True)
    # 第二遍:按这张调色板逐帧上色,出来的颜色才不脏
    subprocess.run(
        [FFMPEG, "-y", "-ss", start, "-t", str(seconds), "-i", str(src),
         "-i", str(palette),
         "-lavfi", f"fps={fps},scale={width}:-1:flags=lanczos[x];[x][1:v]paletteuse",
         str(dst)],
        check=True, capture_output=True)
    palette.unlink()
    return dst

if __name__ == "__main__":
    for f in sorted(SRC.iterdir()):
        if f.suffix.lower() in {".mp4", ".mov", ".mkv", ".avi"}:
            out = video_to_gif(f, OUT / (f.stem + ".gif"))
            print(f"{f.name} -> {out.name}  {out.stat().st_size / 1024:.0f} KB")

改动就两处,SRC 和 OUT 换成你自己的文件夹。第一行后面那一截是卡卡本机 ffmpeg 的位置,你的不一样就改掉,或者干脆把 ffmpeg 加进环境变量。

卡卡跑出来的是 480 宽、48 帧,三段体积分别是 685 KB、775 KB、1993 KB。48 帧这个数对得上,四秒乘 12 帧每秒。

注意竖屏那段:它也是 480 宽,但高度按比例到了 853,体积差不多是横屏那段的 3 倍。想再小一点,把 width 从 480 往下调,或者把 seconds 缩短,体积是按像素总数走的,宽和高一起降才降得快。

不适用:GIF 装不下声音,要带声音的场合就得用视频。另外 1920 宽的录屏缩到 480 只剩四分之一,密密麻麻的代码演示会看不清,看界面操作还凑合。

二、批量截取视频画面当封面

发视频之前基本都要配一张封面图。手动拖进度条截图,既慢又截不准。

# 片段二:批量截取视频画面当封面
import shutil
import subprocess
from pathlib import Path
FFMPEG = shutil.which("ffmpeg") or r"D:\ffmpeg\bin\ffmpeg.exe"
SRC = Path(r"D:\videos")
OUT = Path(r"D:\videos\out")
OUT.mkdir(parents=True, exist_ok=True)

def grab_cover(src, dst, at="00:00:02", max_width=1280, quality=2):
    """在 at 这个时间点截一帧,宽超过 max_width 就等比缩小"""
    scale = f"scale='min({max_width},iw)':-2"
    subprocess.run(
        [FFMPEG, "-y", "-ss", at, "-i", str(src),
         "-frames:v", "1", "-vf", scale, "-q:v", str(quality),
         str(dst)],
        check=True, capture_output=True)
    return dst

if __name__ == "__main__":
    for f in sorted(SRC.iterdir()):
        if f.suffix.lower() in {".mp4", ".mov", ".mkv", ".avi"}:
            out = grab_cover(f, OUT / (f.stem + "_封面.jpg"))
            print(f"{f.name} -> {out.name}  {out.stat().st_size / 1024:.0f} KB")

-ss 放在 -i 前面,ffmpeg 会直接跳到那个时间点再去读文件;-frames:v 1 是只要一帧。

-vf 那一行是个护栏,宽度超过 1280 才等比缩到 1280,没超过就不动。

三段素材跑出来是 1280 乘 720、1280 乘 720、1080 乘 1920,体积 61 KB、63 KB、119 KB。竖屏那段宽度本来就只有 1080,没碰到 1280 这条线,所以它是原尺寸出来的。

不适用:如果第 2 秒正好是黑场或者转场,截出来就是一张黑图。多换几个时间点,总有一张能用。这条路子是按时间找画面,要精确到某一帧,得先知道那一帧落在第几秒。

三、把视频压到指定体积以内

微信发视频有体积上限,一段十分钟的录屏轻轻松松就超了。用剪辑软件导一遍也行,就是参数得自己一遍遍试。

这段的思路反过来,先定「我想让它多大」,再从时长倒推码率。

target_mb * 8192 / 秒数 算出来的是总码率,单位是千比特每秒;减掉音频占的 96 kbps,剩下的全给画面。

为什么要跑两遍:第一遍是试算,不写文件,输出扔到空设备,音频也用 -an 关掉省时间;第二遍才真正写出来。这样压出来的体积才靠得住,不用拍脑袋定一个画质参数。

# 片段三:把视频压到指定体积以内(微信发得出去)
import os
import shutil
import subprocess
from pathlib import Path
FFMPEG = shutil.which("ffmpeg") or r"D:\ffmpeg\bin\ffmpeg.exe"
FFPROBE = shutil.which("ffprobe") or r"D:\ffmpeg\bin\ffprobe.exe"
SRC = Path(r"D:\videos")
OUT = Path(r"D:\videos\out")
OUT.mkdir(parents=True, exist_ok=True)
NULL_OUT = "NUL" if os.name == "nt" else "/dev/null"

defduration_of(src):
    """读视频总时长,单位秒"""
    r = subprocess.run(
        [FFPROBE, "-v", "error", "-show_entries", "format=duration",
         "-of", "csv=p=0", str(src)],
        check=True, capture_output=True, text=True)
    returnfloat(r.stdout.strip())

def compress_to_size(src, dst, target_mb=20, audio_kbps=96):
    """把体积压到 target_mb 兆以内"""
    if src.stat().st_size <= target_mb * 1048576:
        print(f"{src.name} 本来就不到 {target_mb} MB,不用压")
        return src
    secs = duration_of(src)
    video_kbps = max(int(target_mb * 8192 / secs) - audio_kbps, 200)
    log = str(dst.with_suffix(""))
    for p in ("1", "2"):
        cmd = [FFMPEG, "-y", "-i", str(src),
               "-c:v", "libx264", "-preset", "medium",
               "-b:v", f"{video_kbps}k", "-pass", p, "-passlogfile", log]
        if p == "1":
            cmd += ["-an", "-f", "mp4", NULL_OUT]
        else:
            cmd += ["-c:a", "aac", "-b:a", f"{audio_kbps}k",
                    "-movflags", "+faststart", str(dst)]
        subprocess.run(cmd, check=True, capture_output=True)
    return dst

if __name__ == "__main__":
    for f insorted(SRC.iterdir()):
        if f.suffix.lower() in {".mp4", ".mov", ".mkv", ".avi"}:
            before = f.stat().st_size
            out = compress_to_size(f, OUT / (f.stem + "_压缩.mp4"), target_mb=2)
            after = out.stat().st_size
            print(f"{f.name}  {before/1048576:.2f} MB -> {after/1048576:.2f} MB")

卡卡三段素材按 2 MB 压下来,结果是 1.97 MB、1.97 MB、2.01 MB,时长一秒没少,音轨也都还在。

划重点一:第一遍那个 -an 千万别带进第二遍。卡卡第一版就是两遍都挂着 -an,跑完体积特别漂亮,一查流才发现音轨没了,只剩画面。用 ffprobe 看一眼就现原形。

划重点二:这个函数只管「不超过」。目标设得比源文件大的时候,它会老老实实把源文件撑大。卡卡第一次拿 8 MB 当目标,一段 2.61 MB 的录屏被压成了 7.69 MB。所以代码里留了一行护栏,源文件本来就小就直接跳过。

不适用:2 MB 的十秒 1080p,画面码率只剩大约 1542 kbps,细节会糊。发群、发文件够用,想留档就别压这么狠。

四、批量给视频打上文字水印

素材被人搬走以后,水印是唯一还留在画面上的东西。

卡住卡卡的地方不在命令,在字体路径。Windows 的字体路径带一个盘符冒号,而 ffmpeg 的滤镜参数也是用冒号分隔的,两个冒号撞在一起,它就解析不动了。

卡卡把冒号原样写进去试了一次,报错是这两句:

[AVFilterGraph @ ...] No option name near '/Windows/Fonts/msyh.ttc:text=test:x=10:y=10:...'
Error opening output files: Invalid argument

解法就是把那个冒号转义一下,写成 C\:/Windows/Fonts/msyh.ttc,多加一根反斜杠的事。

# 片段四:批量给视频打上文字水印
import shutil
import subprocess
from pathlib import Path
FFMPEG = shutil.which("ffmpeg") or r"D:\ffmpeg\bin\ffmpeg.exe"
SRC = Path(r"D:\videos")
OUT = Path(r"D:\videos\out")
OUT.mkdir(parents=True, exist_ok=True)
FONT = r"C\:/Windows/Fonts/msyh.ttc"   # 微软雅黑,冒号要转义

def add_watermark(src, dst, text="卡皮巴拉"):
    style = (
        f"drawtext=fontfile='{FONT}':text='{text}'"
        f":x=w-tw-24:y=h-th-24:fontsize=40:fontcolor=white@0.9"
        f":box=1:boxcolor=black@0.35:boxborderw=12"
    )
    subprocess.run(
        [FFMPEG, "-y", "-i", str(src), "-vf", style,
         "-c:v", "libx264", "-preset", "veryfast", "-crf", "22",
         "-c:a", "copy", str(dst)],
        check=True, capture_output=True)
    return dst

if __name__ == "__main__":
    for f in sorted(SRC.iterdir()):
        if f.suffix.lower() in {".mp4", ".mov", ".mkv", ".avi"}:
            out = add_watermark(f, OUT / (f.stem + "_水印.mp4"))
            print(f"{f.name} -> {out.name}  {out.stat().st_size / 1048576:.2f} MB")

三段加完水印是 2.46 MB、6.65 MB、3.42 MB。音轨那一行写的是 -c:a copy,意思是音频不重编直接搬过来,所以快,三段一共 6 秒。

水印落没落上去,卡卡是抽了一帧出来看的,字在右下角,没糊。

不适用:这段只能打纯文字。要贴一张图片当水印,得把 drawtext 换成 overlay,还得自己算图片的坐标和透明度。

五、批量变速,音调还得是对的

教程录屏经常要快进。直接把帧率改掉,声音会跟着变尖,听着像磁带快放。

# 片段五:批量给视频变速(音调不变)
import shutil
import subprocess
from pathlib import Path
FFMPEG = shutil.which("ffmpeg") or r"D:\ffmpeg\bin\ffmpeg.exe"
SRC = Path(r"D:\videos")
OUT = Path(r"D:\videos\out")
OUT.mkdir(parents=True, exist_ok=True)

def change_speed(src, dst, speed=2.0):
    """speed=2.0 是两倍速,0.5 是慢放一半"""
    subprocess.run(
        [FFMPEG, "-y", "-i", str(src),
         "-filter_complex",
         f"[0:v]setpts={1 / speed:.4f}*PTS[v];[0:a]atempo={speed}[a]",
         "-map", "[v]", "-map", "[a]",
         "-c:v", "libx264", "-preset", "veryfast", "-crf", "20",
         "-c:a", "aac", str(dst)],
        check=True, capture_output=True)
    return dst

if __name__ == "__main__":
    for f in sorted(SRC.iterdir()):
        if f.suffix.lower() in {".mp4", ".mov", ".mkv", ".avi"}:
            out = change_speed(f, OUT / (f.stem + "_两倍速.mp4"))
            print(f"{f.name} -> {out.name}  {out.stat().st_size / 1048576:.2f} MB")

setpts=0.5*PTS 是把画面的时间轴压到一半,atempo=2 是让音频同步跟上。画面和声音分开处理,音调才不会跟着变。

「音调不变」这句话卡卡专门量了一下。素材里的声音是纯正弦音,卡卡把原始文件和两倍速文件各取一秒音频,采样率降到 8000,做 4000 点的频率扫描,在 200 到 1200 Hz 之间找能量最高的那个频率。

原始文件主频 440 Hz。两倍速文件主频还是 440 Hz,而时长从 10 秒变成了 5.02 秒。

补一句口径:这个扫描是卡卡自己写的,不是拿耳朵听的。8 kHz 采样、4000 点,频率分辨力是 2 Hz 一档,卡卡按 1 Hz 的步长扫过去,440 落在档上,对得齐。

划重点:atempo 的取值有范围,ffmpeg 自带的帮助里写着 from 0.5 to 100。要更快的倍率,就把两个 atempo 串起来接;慢放低于 0.5 倍同理。

总结

五段到这里就完了。它们其实是同一个骨架,拿 python 拼一条 ffmpeg 命令,用 subprocess 跑出去,完事数一下文件大小。

卡卡自己的使用顺序是:先转 GIF 看节奏对不对,定了以后压体积发出去,要留档的补一层水印,教程录屏再丢进变速那一段。

卡卡给自己定的规矩是:只要本机有 ffmpeg,这些活儿就不用再装任何 python 库。少一层包装,就少一处会随版本变动的地方。

如果你手里也压着一堆等着转 GIF、等着压体积的视频,可以先拿走第一段试试,反正不用装东西。

以上就是“这五个 python 视频处理代码,一键复制就能用!”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。 

扫码二维码 获取免费视频学习资料

Python编程学习

查 看2022高级编程视频教程免费获取