Python os.system() 和 subprocess 怎么选?运行系统命令哪个更好用?

引言

Python 提供了多种从代码中运行系统命令的方法。无论您需要列出文件、调用外部工具,还是自动化 shell 工作流,标准库都包含了处理所有这些场景的模块。两种主要方法是较旧的 os.system() 函数和现代的 subprocess 模块,它提供了 subprocess.run()subprocess.call()subprocess.Popen() 以及相关工具。

本教程通过实际示例逐步介绍每种方法,解释何时使用每种方法,并涵盖重要主题,如输出捕获、错误处理、返回码,以及与 shell=True 相关联的安全风险。到最后,您将知道哪种方法适合您的用例,以及如何从 Python 安全地运行外部命令。

关键要点

  • subprocess.run() 是 Python 3.5 及更高版本中执行系统命令的推荐方式,取代了 os.system()subprocess.call()
  • 除非您特别需要 shell 功能(如管道或通配符),否则避免使用 shell=True。传递参数列表而不使用 shell 可以防止命令注入漏洞。
  • 使用 capture_output=Truesubprocess.run() 一起捕获 stdoutstderr 作为字符串或字节。这让您完全控制命令输出。
  • 设置 check=True 以在命令返回非零退出码时自动引发 CalledProcessError,使错误处理变得简单。
  • 对于高级场景,如长时间运行的进程或实时输出流,使用 subprocess.Popen 配合 .communicate().poll().wait()

什么是 Python 系统命令?

Python 系统命令是从 Python 代码中调用操作系统执行外部程序或 shell 指令的任何调用。这包括运行如 lsgitcurl 或机器上安装的任何其他命令行工具。

Python 通过几个内置函数和模块支持此功能:

  • os.system() 将命令字符串传递给系统 shell。这是最高效的方法,但无法访问命令的输出。
  • subprocess.call() 运行命令并返回其退出码。它是 subprocess 模块的一部分,被认为是比 os.system() 更高级的选择。
  • subprocess.run() 是现代的推荐函数(Python 3.5 引入)。它返回一个 CompletedProcess 对象,包含返回码、捕获的输出和错误流。
  • subprocess.Popen() 提供了最多的控制。它允许您实时与运行中的进程交互,按行流式传输输出,并直接管理 stdin/stdout/stderr 管道。

本教程的其余部分将详细介绍这些方法,并提供可工作的代码示例。

使用os.system()运行 shell 命令

os.system() 函数通过系统 shell 执行命令字符串并返回命令的退出状态。它是从 Python 运行外部命令的最基本方法。

以下是一个简单的示例,用于检查安装的 Python 版本:

复制代码
import os

cmd = "python3 --version"

returned_value = os.system(cmd)
print("returned value:", returned_value)

输出:

复制代码
Python 3.14.3
returned value: 0

返回值为 0 表示命令成功运行。任何非零值表示错误。

使用 os.system() 时需要注意几点:

  • 无输出捕获。 命令的输出直接输出到控制台。您无法将其存储到变量中。
  • 有限的错误处理。 您只能获取退出码。无法单独捕获 stderr
  • Shell 执行。 命令字符串直接传递给 shell(Unix 上为 /bin/sh,Windows 上为 cmd.exe),如果命令的任何部分来自用户输入,这会引入潜在的安全风险。

Python os 模块文档本身推荐使用 subprocess 模块代替。os.system() 函数保留用于向后兼容,但 subprocess.run() 是新代码的更好选择。

使用subprocess.call()执行命令

subprocess.call() 函数是 subprocess 模块的一部分,其工作方式类似于 os.system()。它运行一个命令并返回其退出码。不同之处在于它提供了更多对命令调用方式的控制。

复制代码
import subprocess

cmd = "python3 --version"

returned_value = subprocess.call(cmd, shell=True)
print("returned value:", returned_value)

输出:

复制代码
Python 3.14.3
returned value: 0

你也可以将命令作为参数列表传递,这样完全避免使用 shell:

复制代码
import subprocess

returned_value = subprocess.call(["python3", "--version"])
print("returned value:", returned_value)

输出:

复制代码
Python 3.14.3
returned value: 0

传递列表更安全,因为它绕过了 shell 解释。命令及其参数直接发送给操作系统,而无需任何 shell 解析,从而消除了命令注入的风险。

虽然 subprocess.call() 改进了 os.system(),但它仍然无法捕获输出。要实现这一点,你需要使用 subprocess.run()subprocess.check_output()

使用subprocess.run():推荐的现代方法

subprocess.run() 在 Python 3.5 中引入,是运行外部命令的推荐方式。它返回一个 CompletedProcess 对象,其中包含返回码、捕获的标准输出和捕获的标准错误。

基本用法

复制代码
import subprocess

result = subprocess.run(["echo", "Hello from subprocess"], capture_output=True, text=True)
print("stdout:", result.stdout.strip())
print("returncode:", result.returncode)

输出:

复制代码
stdout: Hello from subprocess
returncode: 0

capture_output=True 参数告诉 Python 收集 stdoutstderr。设置 text=True 会将输出作为字符串返回,而不是原始字节。

使用 check=True 自动抛出错误

当你传递 check=True 时,如果命令以非零返回码退出,Python 会抛出 subprocess.CalledProcessError

复制代码
import subprocess

try:
    result = subprocess.run(
        ["ls", "/nonexistent_path"],
        capture_output=True,
        text=True,
        check=True
    )
except subprocess.CalledProcessError as e:
    print(f"Command failed with return code: {e.returncode}")
    print(f"stderr: {e.stderr.strip()}")

输出:

复制代码
Command failed with return code: 1
stderr: ls: /nonexistent_path: No such file or directory

这种模式比每次命令后手动检查返回码要简洁得多。

设置 timeout

你可以使用 timeout 参数(单位为秒)为命令设置最大执行时间:

复制代码
import subprocess

try:
    result = subprocess.run(["sleep", "10"], capture_output=True, text=True, timeout=2)
except subprocess.TimeoutExpired as e:
    print(f"Command timed out after {e.timeout} seconds")

输出:

复制代码
Command timed out after 2 seconds

超时机制在调用可能挂起的外部服务或命令时非常有用。

理解shell=True及其安全风险

当你将 shell=True 传递给任何 subprocess 函数时,命令将通过系统 shell 执行(在 Unix 上为 /bin/sh,在 Windows 上为 cmd.exe)。这允许你使用 shell 特性,如管道、通配符和环境变量展开:

复制代码
import subprocess

result = subprocess.run("echo $HOME", shell=True, capture_output=True, text=True)
print(result.stdout.strip())

这会打印你的主目录,因为 shell 解释了 $HOME

shell=True 的安全问题

shell=True 与不受信任的输入一起使用会产生命令注入漏洞。请考虑这种危险模式:

复制代码
import subprocess

user_input = "hello; rm -rf /tmp/testdir"
subprocess.run(f"echo {user_input}", shell=True)

用户输入中的分号会导致 shell 执行第二个意外命令。这是在接受用户提供数据的系统中一个众所周知的攻击向量。

shell=True 的安全替代方案

最安全的方法是将命令作为参数列表传递,而不使用 shell=True

复制代码
import subprocess

user_input = "hello; rm -rf /tmp/testdir"
result = subprocess.run(["echo", user_input], capture_output=True, text=True)
print(result.stdout.strip())

输出:

复制代码
hello; rm -rf /tmp/testdir

在这里,整个字符串被视为 echo 的单个参数。shell 永远不会解释分号。

如果你需要安全地将命令字符串拆分为参数列表,请使用 shlex.split()

复制代码
import shlex

cmd = 'grep -r "search term" /var/log'
args = shlex.split(cmd)
print(args)

输出:

复制代码
['grep', '-r', 'search term', '/var/log']

shlex.split() 函数正确处理引号字符串和转义字符,生成一个可以直接传递给 subprocess.run() 的列表。

何时可以使用 shell=True 只有在明确需要 shell 特性(如管道或通配符展开)且完全控制命令字符串而没有用户输入时才使用它。

使用 subprocess.Popen 进行高级进程控制

subprocess.Popen 让你对子进程进行直接控制。与 subprocess.run() 不同,后者等待命令完成后再返回,Popen 会启动进程并让你在它运行时与之交互。

使用 communicate() 的基本 Popen 用法

.communicate() 方法向进程发送输入并等待其完成,返回一个 (stdout, stderr) 元组:

复制代码
import subprocess

process = subprocess.Popen(
    ["echo", "hello from Popen"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)
stdout, stderr = process.communicate()
print("stdout:", stdout.strip())
print("returncode:", process.returncode)

输出:

复制代码
stdout: hello from Popen
returncode: 0

轮询长时间运行的 process

使用 .poll() 检查进程是否仍在运行,而不会阻塞:

复制代码
import subprocess
import time

process = subprocess.Popen(["sleep", "3"])

while process.poll() is None:
    print("Process is still running...")
    time.sleep(1)

print(f"Process finished with return code: {process.returncode}")

输出:

复制代码
Process is still running...
Process is still running...
Process is still running...
Process finished with return code: 0

进程间管道

Popen 让你可以将命令串联起来,复制 shell 管道:

复制代码
import subprocess

p1 = subprocess.Popen(["echo", "apple\nbanana\ncherry"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["grep", "banana"], stdin=p1.stdout, stdout=subprocess.PIPE, text=True)
p1.stdout.close()

output, _ = p2.communicate()
print("Filtered output:", output.strip())

输出:

复制代码
Filtered output: banana

这种方法比使用 shell=True 和管道字符更安全,因为每个命令都在自己的进程中运行,不经过 shell 解释。

Popenrun() 的使用时机

大多数情况下,使用 subprocess.run() 运行命令并等待结果。当你需要以下功能时,选择 Popen

  • 实时流式传输进程的输出
  • 交互式向运行中的进程发送输入
  • 并行运行多个进程
  • 在进程间构建管道

重定向stdoutstderrstdin

subprocess 模块通过 stdoutstderrstdin 参数提供对标准流的具体控制。

分别捕获 output

复制代码
import subprocess

result = subprocess.run(
    ["ls", "/tmp", "/nonexistent"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)
print("stdout:", result.stdout[:80])
print("stderr:", result.stderr.strip())

这会将标准输出和标准错误分别捕获到不同的变量中,让你可以独立处理成功输出和错误消息。

output 重定向到文件

复制代码
import subprocess

with open("output.txt", "w") as f:
    subprocess.run(["echo", "writing to file"], stdout=f)

命令输出会直接写入 output.txt,而不是控制台。

合并 stdoutstderr

要将标准错误合并到标准输出中,使用 stderr=subprocess.STDOUT

复制代码
import subprocess

result = subprocess.run(
    ["ls", "/tmp", "/nonexistent"],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True
)
print("combined output:", result.stdout)

当你希望获得命令的所有输出的单一流时,这很有用。

向命令发送 input

使用 input 参数向命令的标准输入传递数据:

复制代码
import subprocess

result = subprocess.run(
    ["grep", "world"],
    input="hello\nworld\nfoo\n",
    capture_output=True,
    text=True
)
print("matched:", result.stdout.strip())

输出:

复制代码
matched: world

有关使用 os 模块进行更复杂输入处理的说明,请参阅 Python os 模块教程。

处理return codes和错误

每个外部命令都会返回一个退出码。退出码 0 表示成功;任何非零值都表示错误。Python 提供了多种方式来检查和响应这些码。

手动检查 return code

复制代码
import subprocess

result = subprocess.run(["ls", "/nonexistent"], capture_output=True, text=True)

if result.returncode != 0:
    print(f"命令执行失败(退出码 {result.returncode})")
    print(f"错误: {result.stderr.strip()}")
else:
    print("成功:", result.stdout)

使用 check=True 自动抛出错误

如前所述,check=True 会在非零退出码时抛出 subprocess.CalledProcessError。以下是一个包含适当异常处理的完整示例:

复制代码
import subprocess

try:
    result = subprocess.run(
        ["python3", "-c", "import sys; sys.exit(42)"],
        capture_output=True,
        text=True,
        check=True
    )
except subprocess.CalledProcessError as e:
    print(f"退出码: {e.returncode}")
    print(f"命令: {e.cmd}")
except FileNotFoundError:
    print("系统中未找到该命令")
except subprocess.TimeoutExpired:
    print("命令执行时间过长")

输出:

复制代码
Return code: 42
Command: ['python3', '-c', 'import sys; sys.exit(42)']

常见的 exception 类型及其修复方法

异常 发生时机 修复方法
subprocess.CalledProcessError 命令返回非零退出码(使用 check=True 通过 e.stderr/e.output 检查命令输出。仔细检查命令参数、文件路径和权限。修复通常涉及修正命令或输入数据。
FileNotFoundError 系统中不存在该可执行文件 确保命令或可执行文件已安装并在系统的 PATH 中可用。使用 which <command> 或按需安装缺失的二进制文件。验证程序名称拼写。
subprocess.TimeoutExpired 命令超过指定的 timeout 如果命令经常需要更长时间运行,则增加 timeout 值,或优化命令以更快完成。可选地,在预期会超时的情况下处理清理/重试逻辑。
PermissionError 可执行文件存在但由于权限不足无法运行 使用 ls -l(Unix 系统)或文件属性(Windows)检查文件权限。使用 chmod +x <file> 使文件可执行,或在必要时以提升权限运行脚本(例如,使用 sudo,或在 Windows 上以管理员身份运行)。

预见并处理这些异常有助于使你的自动化脚本更加健壮,特别是当脚本可能在不同的环境中运行或遇到缺失依赖时。有关有效的 Python 错误处理,参见 Python 错误处理教程。

对比表格:os.systemsubprocess方法

特性 os.system() subprocess.call() subprocess.run() subprocess.Popen()
捕获 stdout 是 (capture_output=True) 是 (stdout=PIPE)
捕获 stderr 是 (capture_output=True) 是 (stderr=PIPE)
访问退出码 是 (返回值) 是 (返回值) 是 (result.returncode) 是 (process.returncode)
错误时抛出异常 是 (check=True) 否 (手动检查)
支持超时 是 (timeout 参数) 是 (timeout 参数) 手动 (.wait(timeout))
发送输入 (stdin) 是 (input 参数) 是 (stdin=PIPE)
实时流式处理
需要 shell 是 (总是) 可选 可选 可选
推荐用于新代码 高级场景

对于大多数任务,subprocess.run() 提供了简单性和控制性的最佳平衡。只有在需要与运行中的进程进行实时交互时,才使用 Popen

常见问题解答

1. Python 中 os.system() 和 subprocess.run() 有什么区别?

os.system() 将命令字符串传递给系统 shell,仅返回退出码。它无法捕获命令的输出或错误消息。subprocess.run()(Python 3.5 引入)返回一个 CompletedProcess 对象,其中包含返回码、stdout 和 stderr。它还支持超时、通过 check=True 自动抛出错误,并且当传递参数列表时可以完全绕过 shell。Python 官方文档推荐使用 subprocess.run() 替代 os.system()

2. Python 3 中 os.system() 是否已被弃用?

没有,os.system() 并未正式弃用。它在 Python 3.14 中仍然有效,并返回命令的退出状态。然而,Python 官方文档指出 subprocess 模块提供了"更强大的生成新进程的功能",并推荐使用 subprocess.run() 替代。

3. 在 subprocess 中何时应该使用 shell=True?

仅当需要 shell 特定功能(如管道 |、通配符 * 或环境变量展开 $HOME)并且完全控制命令字符串时,才使用 shell=True。如果命令的任何部分来自用户输入,绝不要使用 shell=True,因为这会造成命令注入漏洞。更安全的做法是传递参数列表(例如 subprocess.run(["ls", "-la"])),完全绕过 shell。

4. 如何在 Python 中捕获 shell 命令的输出?

使用 subprocess.run() 并设置 capture_output=Truetext=True

复制代码
import subprocess

result = subprocess.run(["date"], capture_output=True, text=True)
print(result.stdout.strip())

这会将命令的标准输出作为字符串存储在 result.stdout 中。如果不使用 text=True,输出将作为 bytes 对象返回。你也可以使用较旧的 subprocess.check_output() 函数,它直接返回输出,但在退出码非零时会抛出异常。

5. subprocess 中非零返回码意味着什么?

非零返回码表示命令未成功完成。具体值取决于命令。例如,grep 在未找到匹配项时返回 1ls 在严重错误(如无效选项)时返回 2。调用 subprocess.run() 后,可以通过 result.returncode 检查返回码。如果传递 check=True,当返回码非零时 Python 会自动抛出 subprocess.CalledProcessError,从而简化错误处理。

结论

Python 的 subprocess 模块让你完全掌控运行系统命令,从简单的单行命令到具有实时输出流式的复杂管道。对于新项目,默认使用 subprocess.run()。它在一个函数调用中处理输出捕获、错误检查和超时。将 subprocess.Popen() 保留用于需要与运行中进程交互的情况,并在新代码中避免使用 os.system(),因为它缺少输出捕获和适当的错误处理。

在使用外部命令时,始终优先将参数作为列表传递,而不是使用 shell=True,以防止命令注入。将 check=True 与适当的异常处理结合使用,可以编写出可预测失败并提供清晰错误消息的脚本。

对于在自己的 Python 脚本中处理命令行参数,或调试复杂程序,社区还有更多基于这些概念的教程。

后续步骤

如果你正在构建运行远程服务器上 shell 命令的 Python 自动化脚本,Droplets 提供了一种快速启动 Linux 环境用于测试和生产的方法。你可以部署一个预装 Python 的 Droplet,并使用本教程中的技术来自动化服务器管理任务。

相关推荐
星空椰21 小时前
JavaScript 基础进阶:分支、循环与数组实战总结
开发语言·javascript·ecmascript
yong999021 小时前
IHAOAVOA:天鹰优化算法与非洲秃鹫优化算法的混合算法(Matlab实现)
开发语言·算法·matlab
努力学习_小白21 小时前
ResNet-50——pytorch版
人工智能·pytorch·python
t***5441 天前
有哪些常见的架构设计模式在现代C++中应用
开发语言·c++
战族狼魂1 天前
基于LibreOffice +python 实现一个小型销售管理系统的数据库原型教学实验
数据库·python
m0_640309301 天前
PHP函数怎样适配高可靠性存储硬件_PHP在ZFS RAIDZ环境配置【技巧】
jvm·数据库·python
Once_day1 天前
网络以太网之(3)LLDP协议
网络·以太网·lldp
2402_854808371 天前
Django REST Framework 中实现用户资料更新的完整实践指南
jvm·数据库·python
m0_748839491 天前
golang如何理解weak pointer弱引用_golang weak pointer弱引用总结
jvm·数据库·python
m0_738120721 天前
渗透测试基础ctfshow——Web应用安全与防护(五)
前端·网络·数据库·windows·python·sql·安全