Python 中 os.system 和 subprocess.run 的选型
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参数向子进程发送字符串或字节(需要配合text或encoding)。
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)