subprocess.run() 是最简安全的选择,支持 capture_output、text、timeout 参数自动处理输出、编码和超时,异常由 CalledProcessError 统一抛出;实时流式处理需用 Popen.communicate();encoding 应显式设为 'utf-8' 避免乱码;禁用 shell=True 防注入与编码问题。

subprocess.run() 是最简安全的选择
直接用 subprocess.run() 捕获输出,避免手动管理管道和进程生命周期。它默认阻塞、自动回收资源,且能统一处理 stdout/stderr。
常见错误是先用 subprocess.Popen() 再调用 .communicate(),但忘了加 timeout 或没处理编码异常。而 run() 天然支持这些:
- 加
capture_output=True自动捕获 stdout 和 stderr(等价于stdout=subprocess.PIPE, stderr=subprocess.PIPE) - 加
text=True直接返回字符串,不用手动.decode() - 加
timeout=5防止子进程卡死 - 异常由
subprocess.CalledProcessError统一抛出,不需手动检查returncode
try:
result = subprocess.run(['ls', '-l'], capture_output=True, text=True, timeout=3)
print(result.stdout)
except subprocess.TimeoutExpired as e:
print("命令超时")
except subprocess.CalledProcessError as e:
print(f"非零退出码: {e.returncode}, stderr: {e.stderr}")
需要实时读取输出时必须用 Popen + communicate()
subprocess.run() 是全量等待,不适合日志流、进度条或超长运行命令的边执行边处理场景。
此时必须用 Popen,但关键点不是“怎么开进程”,而是“怎么安全读取”:
- 不要用
.stdout.readline()循环——容易因缓冲或换行缺失卡住 - 不要在子进程未结束时反复调用
.stdout.read(1)——性能差且易阻塞 - 正确做法:调用
.communicate(timeout=...),它内部做了非阻塞轮询和缓冲管理 - 若真要逐行处理,得设
bufsize=1且子进程自身用sys.stdout.flush()或stdbuf -oL
encoding 错误和 locale 问题比想象中常见
text=True 不等于“自动适配环境编码”。Python 默认用 locale.getpreferredencoding() 解码,但在 Docker 容器、CI 环境或 Windows 子系统里常是 ANSI_X3.4-1968(即 ASCII),导致中文乱码或 UnicodeDecodeError。
解决方式很直接:
- 显式指定
encoding='utf-8'(推荐,尤其跨平台时) - 或设
errors='replace'避免崩溃,但会丢失字符 - 避免依赖
sys.getdefaultencoding(),它和子进程实际编码无关
result = subprocess.run(['echo', '你好'], capture_output=True, text=True, encoding='utf-8')
shell=True 带来的隐性风险不能忽略
很多示例直接写 shell=True 来支持管道、重定向,但它会启动 /bin/sh(Linux/macOS)或 cmd.exe(Windows),带来三类问题:
- 参数注入漏洞:变量拼接进字符串时,如
f"grep {user_input}",可能执行任意命令
- 信号传递异常:Ctrl+C 可能只终止 shell,不终止真正子进程
- 编码更难控制:shell 层可能二次转码,
encoding 参数有时失效
f"grep {user_input}",可能执行任意命令encoding 参数有时失效除非明确需要 shell 功能(比如 |、&&、文件通配),否则一律传命令列表,禁用 shell=True。需要用管道就分步调用,或改用 shlex.split() + 显式 shell 调用(并严格校验输入)。
真实项目里,多数所谓“必须用 shell”的需求,其实只是没意识到 subprocess 本身就能组合多个命令。










