Python 中 os.system 和 subprocess.run 的选型

FreeGuideOnline 最新 2026-07-04

python import os exit_code = os.system("ls -l") print(exit_code) # 0 表示成功,非零表示失败


**主要特点:**

- 参数是一个完整的命令字符串(包括参数),由系统的默认 shell 解析(如 Linux 上的 `/bin/sh -c` 或 Windows 上的 `cmd.exe /c`)。
- 返回值为整数:命令的退出码,通常 0 表示成功。
- 执行期间,Python 脚本会 **阻塞** 直到命令完成。
- 命令的输出直接打印到终端,无法在 Python 代码中捕获或存储。
- 没有任何安全防护,容易受到 **shell 注入** 攻击(当命令中包含用户输入时)。

**简单示例:**

```python
import os
os.system("mkdir new_folder")
os.system("echo Hello > file.txt")

局限性一览:

  • 无法获取命令的标准输出(stdout)或标准错误(stderr),只能等待它们打印到控制台。
  • 不提供超时控制,无法优雅地终止卡住的进程。
  • 无法传递输入流(stdin)到子进程。
  • 命令是在 shell 中执行的,这引入额外的开销和安全风险。

subprocess.run 现代标准

从 Python 3.5 起,subprocess.run 成为 推荐的主函数 用于运行外部命令。它提供了远比 os.system 强大的功能,例如捕获输出、传递输入、设置超时、检查返回码以及控制 shell 是否介入。

import subprocess
result = subprocess.run(["ls", "-l"])
print(result.returncode)   # 退出码

基础用法与捕获输出

默认情况下,subprocess.run 不捕获输出,标准输出和标准错误会直接流到父进程的终端。要捕获它们,需要指定 capture_output=True(或分别设置 stdout=subprocess.PIPE, stderr=subprocess.PIPE)。

result = subprocess.run(["echo", "Hello World"], capture_output=True, text=True)
print(result.stdout)   # "Hello World\n"
print(result.stderr)   # 如果有错误的话
  • text=True:让输出以字符串形式返回(默认为字节流)。
  • check=True:如果命令返回非零退出码,则自动抛出 CalledProcessError,可以让你及时处理失败。

避免 Shell 注入

当使用 命令列表形式(第一个参数是列表)时,subprocess.run 会直接调用底层系统调用,不经过 shell 解析。这意味着参数中的特殊字符(如空格、分号)会被安全地当作字面参数传递,不会被 shell 解释,从而杜绝注入风险。

# 安全:shell=False(默认)
subprocess.run(["ls", "-l", "; rm -rf /"])   # 只是列出名为 "; rm -rf /" 的文件,不会执行删除

如果你确实需要 shell 的功能(例如管道、通配符),可以设置 shell=True,但 必须确保命令字符串是受信任的,否则极易引入命令注入漏洞。

# 有风险,仅当命令不包含任何外部输入时使用
subprocess.run("ls -l | grep txt", shell=True)

超时与输入

  • 超时控制:通过 timeout 参数,命令运行超时将自动终止并抛出 TimeoutExpired 异常。
import subprocess
try:
    subprocess.run(["sleep", "10"], timeout=5)
except subprocess.TimeoutExpired:
    print("命令执行超时!")
  • 传递输入:可以用 input 参数向子进程发送字符串或字节(需要配合 textencoding)。
result = subprocess.run(["python3", "-c", "print(input())"], 
                        input="来自Python的问候", text=True, capture_output=True)
print(result.stdout)  # 来自Python的问候

如何选型:一张表说清楚

特性 os.system subprocess.run
是否推荐使用 老旧,不推荐 ✅ 官方推荐
输出捕获 ❌ 无法捕获 stdout/stderr ✅ 通过 capture_output 轻松捕获
安全性(防注入) ❌ 总是通过 shell 执行,不安全 ✅ 默认不经过 shell,安全(列表形式)
超时控制 ❌ 不支持 timeout 参数
传递输入 ❌ 不支持 input 参数
获取退出码 返回值即为退出码 result.returncode
处理错误 手动检查 check=True 自动抛出异常
执行灵活性 仅支持字符串命令 列表形式(不调 shell)或字符串形式
跨平台行为 依赖系统 shell,行为有差异 行为统一,可指定 shell=True/False

典型场景下的选择建议

场景 1:简单的、不需要捕获输出的命令,且完全信任命令来源

保守建议:仍然用 subprocess.run,它更明确,且不会养成坏习惯。但如果只是写一次性使用的 5 行小脚本,os.system 也可接受。

# 可以,但不推荐
os.system("mkdir temp_folder")

# 推荐
subprocess.run(["mkdir", "temp_folder"])

场景 2:需要收集命令输出或检查错误

唯一答案:subprocess.run

result = subprocess.run(["find", "/usr", "-name", "python3"], capture_output=True, text=True)
if result.returncode == 0:
    locations = result.stdout.splitlines()

场景 3:命令中包含用户输入或外部数据

必须用 subprocess.run 并避免 shell=True

user_filename = input("请输入文件名:")
# 危险:os.system(f"cat {user_filename}")  如果输入 "a.txt; rm -rf /" 将灾难
# 安全:
subprocess.run(["cat", user_filename])

场景 4:使用管道、重定向等 shell 特性

一定要用 shell 时,优先考虑用多个 subprocess.PIPE 连接多个子进程来模拟管道,而不是直接 shell=True。如果必须使用 shell=True,确保命令字符串是硬编码的,并且不包含任何用户输入。

# 更好的方式:纯 Python 连接两个子进程
ps = subprocess.Popen(["ls", "-l"], stdout=subprocess.PIPE)
grep = subprocess.Popen(["grep", "txt"], stdin=ps.stdout, stdout=subprocess.PIPE)
ps.stdout.close()
output = grep.communicate()[0]

迁移示例:从 os.system 到 subprocess.run

假设原来的脚本:

import os
os.system("gcc -o hello hello.c")
os.system("./hello > output.txt")

重写后:

import subprocess, shlex

# 编译
result = subprocess.run(["gcc", "-o", "hello", "hello.c"])
if result.returncode != 0:
    print("编译失败")
    exit(result.returncode)

# 运行并重定向输出到文件
with open("output.txt", "w") as f:
    subprocess.run(["./hello"], stdout=f)