Python 只有一个正确答案:subprocess.run(列表, capture_output=True)。 但它有四个默认值需要你显式改掉,否则迟早出事。
这一章最后一节,也是最实用的一节。 前面讲的「传参、等、收输出」在 Python 里都有现成答案, 但默认值不是为「调 ffmpeg」这个场景设计的。
一、三种写法,能力一层比一层强
① os.system(命令):只返回一个状态码。
输出直接打到屏幕,你拿不到。
它连「成功」都要你自己移位判断(Linux 上)。
只适合「跑一下,成不成无所谓」。
② subprocess.run(字符串, shell=True):能拿输出与返回码。
但要多经一层 shell,参数又会被解析一遍。
除非你要用管道、通配符,否则没理由用它。
③ subprocess.run(列表, capture_output=True):参数数组直传。
没有 shell 这一层,引号问题不存在。这是唯一推荐的写法。
三者的差别不在「能不能跑」,在「旁边那两件事处理得干不干净」。 第三种把输出、返回码、退出码全给你,而且不引入额外解析。
二、四个默认值要显式改掉
subprocess.run 的默认值是为「跑个短命令」设计的,调 ffmpeg 得改四处:
参数 默认值 该改成
shell False 保持 False,传列表 stdin 继承父进程 DEVNULL timeout 不设 设一个上限 encoding 依 locale "utf-8" + errors
stdin 这一项最容易被忽略。
不指定它,子进程就继承你的标准输入;
ffmpeg 默认会去读它,于是「后台跑时被挂起」或者「抢走父进程的输入」都可能发生。
stdin=subprocess.DEVNULL 是比 -nostdin 更彻底的一层保险。
encoding 这一项在中文 Windows 上一定撞。
默认按 locale 走,而中文 Windows 的 locale 是 cp936(GBK);
ffmpeg 输出的是 UTF-8,解码那一刻就抛 UnicodeDecodeError。
最迷惑的地方是:命令明明跑成功了,程序却在「读输出」这一步崩掉。
三、check=True 与 capture_output=True
capture_output=True 相当于同时设 stdout=PIPE 与 stderr=PIPE,
于是你拿到一个带 stdout/stderr 属性的结果对象。
check=True 的意思是「返回码非 0 就抛异常」。
抛的是 CalledProcessError,里面带着 returncode、stdout、stderr。
好处是「忘了判返回码」这种 bug 不可能发生;
代价是你得写 try,而且要做到「异常里也能拿到 stderr」。
本机实测:子进程 exit 3,异常里 returncode 是 3,stderr 是 'boom'。
这条 stderr 就是排查的唯一线索——所以捕获异常时一定要把它写进日志。
四、超时与 kill
timeout=秒数 到点会抛 TimeoutExpired,但【子进程不会自动死】。
异常对象里带着 cmd,你要自己 kill()。
subprocess.run 在超时的时候会先 kill 再 wait,
所以用它一般不会漏掉孤儿进程;
但如果你用的是 Popen 手工管理,就必须自己收尾。
转码这种长任务,超时值按「最长可能的耗时 × 2」设, 别设成「平均耗时」,否则大文件会被误杀。
五、一份可以直接抄的模板
把前面所有要点收进一个函数:
import subprocess
def run_ffmpeg(args, timeout=3600):
cmd = ["ffmpeg", "-hide_banner", "-nostdin"] + args
p = subprocess.run(cmd,
stdin=subprocess.DEVNULL,
capture_output=True,
timeout=timeout,
encoding="utf-8", errors="replace")
if p.returncode != 0:
raise RuntimeError("ffmpeg 失败,返回码 "
+ str(p.returncode) + ":" + p.stderr[-800:])
return p.stdout
这十行里有五处是前面几节攒下来的:-nostdin、-hide_banner、
stdin=DEVNULL、timeout、encoding + errors。
第六处是习惯:报错只留 stderr 的最后 800 个字符。
ffmpeg 的 stderr 能有几千行,全塞进异常里,日志就没法看了。
这就是这一章的落点:调外部程序不难,难在把「默认值」一个个改对。 C++ 要自己拼引号、Java 要自己读流、Python 要把四个默认值改掉—— 三种语言,同一份清单。
六、动手 Demo:三种写法与四个坑
下面把「三种写法能拿到什么」和「编码坑」一次测完。 第三段是本机的特殊情况:这台机器被设成了 UTF-8,所以默认能通过; 而中文 Windows 的默认代码页是 GBK,同样的代码在那里会崩。
import locale, os, subprocess, sys
PY = sys.executable
print("os.system ->", os.system(PY + " -c \"print(1)\""))
r = subprocess.run([PY, "-c", "print('hi')"],
capture_output=True, text=True)
print("run 列表 ->", r.returncode, repr(r.stdout.strip()))
try:
subprocess.run([PY, "-c", "import sys; sys.exit(3)"], check=True)
except subprocess.CalledProcessError as e:
print("check=True ->", type(e).__name__, e.returncode)
print("locale ->", locale.getpreferredencoding(False))
try:
"正在转码".encode("utf-8").decode("gbk")
except UnicodeDecodeError as e:
print("gbk 解 utf-8 -> UnicodeDecodeError @", e.start)
实跑输出:
--------------------------------------------------------------------
12.3 Python 调用 FFmpeg 命令行
--------------------------------------------------------------------
**① 三种写法,能拿到的东西一层比一层多**
写法 拿到什么
--------------------------------------------------------------------
os.system(命令) 只有返回值 0,输出直接打屏
run(字符串, shell=True) 返回值 0 加输出 'hi'
run(列表) 返回值 0 加输出 'hi'
**第一种只适合「跑一下、成不成无所谓」;第二种能拿输出,但要多经一层 shell;**
**第三种是唯一推荐的:参数用列表,shell 这一层根本不存在。**
**而三种写法【共同】的坑是 stdin——不显式指定,子进程就继承你的。**
**② `check=True` 把「非 0」变成异常,省掉手写 if**
写法 子进程 exit 3 时发生什么
------------------------------------------------------------------------
run(...) 默认 返回对象,returncode = 3,自己判
run(..., check=True) 抛出 CalledProcessError,rc=3,stderr='boom'
**`check=True` 的好处是「忘了判返回码」这种 bug 再也不会发生——**
**异常里连 stderr 都给你带上了,直接就能往日志里写。**
**③ 编码:这一条在中文 Windows 上一定会撞**
这一项 本机的值
--------------------------------------------------------------
locale 的首选编码 utf-8
拿 gbk 解 utf-8 字节 UnicodeDecodeError,位置 8
**本机这一套被设成了 UTF-8,所以默认解码能通过;**
**而中文 Windows 的默认代码页是 `cp936`(GBK),**
**ffmpeg 输出的 UTF-8 中文在那里就会撞上 `UnicodeDecodeError`。**
**这种报错最迷惑人的地方是:命令明明跑成功了,程序却在「读输出」这一步崩了。**
**所以 `text=True` 一定要配对写 `encoding="utf-8"`,再加 `errors="replace"` 兜底。**
**④ 一份可以直接抄的模板**
import subprocess
def run_ffmpeg(args, timeout=3600):
cmd = ["ffmpeg", "-hide_banner", "-nostdin"] + args
p = subprocess.run(cmd, stdin=subprocess.DEVNULL,
capture_output=True, timeout=timeout,
encoding="utf-8", errors="replace")
if p.returncode != 0:
raise RuntimeError("ffmpeg 失败(%d): %s"
% (p.returncode, p.stderr[-800:]))
return p.stdout
**这十行里有五处是前面几节攒下来的:`-nostdin`(不吃父进程输入)、**
**`-hide_banner`(别把版本横幅混进日志)、`stdin=DEVNULL`(双保险)、**
**`timeout`(不然就可能永久卡住)、`encoding + errors`(不然中文就崩)。**
**还有一处是习惯:报错只留 stderr 的最后 800 个字符。**
**ffmpeg 的 stderr 能有几千行,全塞进异常里,日志就没法看了。**
os.system 只给 0,输出直接打到终端;run 列表能给返回码和输出。
check=True 把非 0 变成了异常,连 returncode 都带着。
最后两行:本机 locale 是 utf-8,但显式用 gbk 解就会在位置 8 崩。
30 秒实验
- 把
capture_output=True换成stdout=subprocess.PIPE:看 stderr 去哪儿了。 - 把
errors="replace"删掉再看一次 gbk 解码:看异常长什么样。 - 给
run加timeout=0.01:看TimeoutExpired里带了什么。
常见误区
- 用
os.system又想要输出。它只给状态码。 text=True不配encoding。中文 Windows 上按 GBK 解 UTF-8,必崩。- 不给
stdin。ffmpeg 会去读标准输入,后台跑时被挂起。
自测三问(点开对答案)
1. 为什么推荐传列表而不是字符串? —— 列表参数直传,不经过 shell,带空格的参数不会被切开。 2. `check=True` 的异常里有什么? —— `returncode`、`stdout`、`stderr`,够直接写日志。 3. `text=True` 在中文 Windows 上为什么容易崩? —— 默认按 locale(cp936)解码,而 ffmpeg 输出 UTF-8。- ☐ 12.3 完——全书十二章 208 篇完结