Windows 上 subprocess 传中文参数实测:参数没坏,坏的是输出解码
起因
有个跨平台的工具要在 Windows 上调外部命令,参数里带中文。同事说「Windows 下中文参数会乱码,得写个转义函数」,我先测了再说。
结论有点反直觉:参数本身一点没乱。 乱的是子进程输出那一环。
参数这一侧是干净的
我写了个小程序打印收到的 sys.argv,用列表形式传中文、带空格和空串:
subprocess.run([sys.executable, script, '中文参数', 'a b', ''], capture_output=True, text=True)
子进程那边收到的是:
['中文参数', 'a b', '']
三个都对。中文没变成乱码,空格没被拆开,空字符串也没被吞掉。
原因是 Windows 上 Python 走的是 Unicode API(CreateProcessW),参数全程以 UTF-16 传递,不经过编码转换。这一段不存在「GBK 和 UTF-8 打架」的问题。
真正会出事的是 shell=True
shell=True 时,命令字符串被交给 cmd.exe 解析,这一步会经过系统代码页。
实测几种:
echo hello && echo world加shell=True→ 输出两行,&&被 shell 解释了echo a && echo b加shell=False→ 输出a && echo b,整个字符串被当成一个字面参数传给echo
第二个结果容易看漏。shell=False 时 && 不是「不生效」,而是变成了普通字符。指望用列表形式以 && 串联命令,它会安静地不干活。
更危险的是 shell=True 加上用户可控的输入:
subprocess.run(f'type {user_input}', shell=True)
user_input 里塞个 & del /f /q D:\some\dir 就直接执行了。列表形式没这个问题,因为参数不会被 shell 解析。
所以顺序是:能用列表形式就用列表形式,必须用 shell 特性(管道、通配符、重定向)时才开 shell=True,而且命令字符串里不能拼接外部输入。
输出侧才是编码的战场
参数传递没问题,但子进程输出的编码由子进程自己决定。
我用 cmd.exe 跑了个 copy 命令,它输出的中文提示是 GBK(cp936)。抓到的原始字节是:
b'\xd2\xd1\xb8\xb4\xd6\xc6 1 \xb8\xf6\xce\xc4\xbc\xfe\xa1\xa3\r\n'
按 GBK 解是「已复制 1 个文件。」,按 UTF-8 解直接抛 UnicodeDecodeError。
真正坑人的是它不抛给你
capture_output=True 配 text=True 时,读取子进程输出用的是内部线程,解码异常就发生在那个线程里。实测现象是:run() 正常返回,returncode 是 0,result.stdout 是 None,异常只打到 stderr 上。
后面那行 result.stdout.strip() 报的是:
AttributeError: 'NoneType' object has no attribute 'strip'
报错离病因(编码不对)隔了两层。排查时先看 result.stdout is None。想看到替换字符得加 errors='replace',但那时数据已损坏,只是把崩溃换成静默的错。
默认用哪个编码,取决于解释器版本
text=True 不写 encoding 时,解码用的是解释器的默认编码,而这个默认值在 Python 3.15 变了。
PEP 686 让 UTF-8 模式成为默认,3.15 起不用设环境变量就生效。此时默认编码是 UTF-8,text=True 不指定编码去读 GBK 输出,就是上面那个 None。
两种模式都跑过:
- UTF-8 模式开(3.15 起的默认):
stdout是None - UTF-8 模式关(
PYTHONUTF8=0,3.14 及更早的常态):同样一行代码,stdout是'已复制 1 个文件。'
同一份代码,升级 Python 就炸了。换机器或升版本就吐 None,根因在这。
修法
显式指定 encoding:
subprocess.run(['cmd', '/c', 'copy', src, dst],
capture_output=True, text=True, encoding='gbk')
子进程输出是 UTF-8(自带的 Python 程序,或设了 chcp 65001)就用 encoding='utf-8'。
更省事的思路:别去解码子进程的输出。让子进程把结果写成文件,Python 直接读,编码自己定死。命令行参数传路径,结果用文件回传,就没有编码协商环节了。
顺便一个 Windows 特有的细节
Python 的 subprocess 在 Windows 上默认会弹黑框。控制台程序在 GUI 里跑起来,黑框一闪而过,像闪退。
解决是用 creationflags:
flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0)
subprocess.run(['cmd', '/c', 'echo ok'], creationflags=flags, capture_output=True)
我测了 CREATE_NO_WINDOW 的值是 134217728,加上后命令正常执行且不弹窗。
这个常量是 Windows 专有的,用 getattr 加默认值才能同时跑在 Linux 上。
一句话
参数传递在 Windows 上是安全的,Unicode API 全程 UTF-16。有风险的是 shell=True(命令注入 + && 语义变化)和输出解码(子进程自定代码页)。前者用列表形式,后者显式指定 encoding。
- 点赞
- 收藏
- 关注作者
评论(0)