引言
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=True与subprocess.run()一起捕获stdout和stderr作为字符串或字节。这让您完全控制命令输出。 - 设置
check=True以在命令返回非零退出码时自动引发CalledProcessError,使错误处理变得简单。 - 对于高级场景,如长时间运行的进程或实时输出流,使用
subprocess.Popen配合.communicate()、.poll()或.wait()。
什么是 Python 系统命令?
Python 系统命令是从 Python 代码中调用操作系统执行外部程序或 shell 指令的任何调用。这包括运行如 ls、git、curl 或机器上安装的任何其他命令行工具。
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 收集 stdout 和 stderr。设置 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 解释。
Popen 与 run() 的使用时机
大多数情况下,使用 subprocess.run() 运行命令并等待结果。当你需要以下功能时,选择 Popen:
- 实时流式传输进程的输出
- 交互式向运行中的进程发送输入
- 并行运行多个进程
- 在进程间构建管道
重定向stdout、stderr和stdin
subprocess 模块通过 stdout、stderr 和 stdin 参数提供对标准流的具体控制。
分别捕获 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,而不是控制台。
合并 stdout 和 stderr
要将标准错误合并到标准输出中,使用 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.system与subprocess方法
| 特性 | 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=True 和 text=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 在未找到匹配项时返回 1,ls 在严重错误(如无效选项)时返回 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,并使用本教程中的技术来自动化服务器管理任务。