subprocess 模块用于生成子进程、连接其输入/输出/错误管道并获取其返回码的强大工具,旨在替代一些旧的模块(如 os.systemos.spawn*)。


1. 核心概念:为什么要用 subprocess?

在 Python 中,有时你需要与系统外部的命令或程序进行交互,例如调用 lsgrep, 甚至另一个 Python 脚本或二进制可执行文件。subprocess 模块提供了一种一致且安全的方式来创建和管理这些子进程。

核心目标:启动一个新的进程(子进程),并与它进行通信(发送输入、获取输出和错误信息),并等待它完成。


2. 推荐使用的核心函数:run()

Python 3.5 引入了 subprocess.run() 函数,这是目前推荐使用的、最高阶的 API,它能够处理大多数常见用例。

基本语法

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, 
               capture_output=False, shell=False, cwd=None, timeout=None, 
               check=False, encoding=None, errors=None, text=None, env=None)

简单示例:运行一个命令

import subprocess

# 运行 'ls -l' 命令
result = subprocess.run(['ls', '-l'])

# 默认情况下,输出会直接打印到终端。
# result 是一个 CompletedProcess 对象
print(result.returncode) # 查看命令的退出状态码,0 通常表示成功

常用参数详解

  • args最重要参数。指定要执行的命令。
  • 强烈建议传递一个列表,例如 ['ls', '-l']。这是最安全的方式,可以避免 shell 注入攻击。
  • 也可以传递一个字符串,但必须同时设置 shell=True,例如 run('ls -l', shell=True)除非有明确理由,否则应避免使用 shell=True,因为它有安全风险。
  • stdoutstderr: 控制子进程的输出和错误流。
  • subprocess.PIPE: 捕获输出,以便在 Python 代码中访问。
  • subprocess.DEVNULL: 将输出重定向到特殊文件 /dev/null(即丢弃输出)。
  • None (默认): 输出不重定向,直接打印到终端。
  • 一个已经打开的文件对象:将输出写入该文件。
  • capture_output: 如果设为 True,相当于同时设置 stdout=subprocess.PIPE 和 stderr=subprocess.PIPE。这是 Python 3.7 的新特性,非常方便。
  • check: 如果设为 True,并且子进程以非零状态码退出,将抛出一个 CalledProcessError 异常。这对于确保命令成功执行非常有用。
  • text (或 encodingerrors): 如果设为 True,输入/输出流将以文本字符串(而不是字节序列 bytes)的形式处理。encoding 和 errors 参数用于指定编解码器和错误处理方案(类似于 open() 函数)。
  • cwd: 在运行子进程之前,将工作目录切换到 cwd 所指定的路径。
  • timeout: 设置命令超时时间(秒)。如果进程在超时后仍未结束,将抛出 TimeoutExpired 异常。
  • input: 传递给子进程的输入数据。如果 text=True,则应为字符串;否则应为字节序列。通常与 stdin=subprocess.PIPE 一起使用。
  • env: 一个字典,用于为子进程定义环境变量。例如 env={'MY_VAR': 'value'}。默认情况下,子进程会继承当前进程的环境变量。

3. 实战示例

示例 1:捕获输出

import subprocess

# 捕获标准输出
result = subprocess.run(['echo', 'Hello World!'], 
                        capture_output=True, 
                        text=True)

print(f"Output: {result.stdout}")      # 输出: Hello World!
print(f"Return code: {result.returncode}") # 输出: 0

示例 2:处理错误(check=True)

import subprocess

try:
    # 运行一个肯定会失败的命令
    result = subprocess.run(['ls', '/nonexistent/directory'], 
                           capture_output=True, 
                           text=True, 
                           check=True) # 如果失败会抛出异常
except subprocess.CalledProcessError as e:
    print(f"Command failed with return code {e.returncode}")
    print(f"Error message: {e.stderr}") # ls: cannot access '/nonexistent/directory': No such file or directory

示例 3:传递输入给子进程(与进程交互)

import subprocess

# 与 'grep' 命令交互,向其传递输入
input_text = """apple
banana
grape
orange
"""

# 让 grep 从标准输入读取,并搜索 ‘ap’
result = subprocess.run(['grep', 'ap'], 
                        input=input_text, 
                        capture_output=True, 
                        text=True)

print(result.stdout) # 输出: apple\ngrape\n

示例 4:超时控制

import subprocess

try:
    # 运行一个会休眠 10 秒的命令,但我们只等 2 秒
    result = subprocess.run(['sleep', '10'], 
                           timeout=2, 
                           capture_output=True)
except subprocess.TimeoutExpired:
    print("Command timed out! It was killed.")

示例 5:修改工作目录和环境变量

import subprocess

# 在指定目录下运行命令
result1 = subprocess.run(['pwd'], cwd='/tmp')

# 使用自定义环境变量运行命令
custom_env = {'MY_MESSAGE': 'Hello from subprocess'}
result2 = subprocess.run(['env'], env=custom_env, capture_output=True, text=True)
print(result2.stdout) # 会看到 MY_MESSAGE=Hello from subprocess 以及其他环境变量

4. 底层组件:Popen 类

subprocess.run() 实际上是在 Popen 类的基础上构建的一个便利函数。对于更高级的用例(例如需要实时处理输出流,或者进行复杂的管道连接),你需要直接使用 Popen 类。

Popen 基本用法

import subprocess

# 启动进程
process = subprocess.Popen(['python3', '-c', 'import time; time.sleep(5); print("Done")'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE,
                           text=True)

# 等待进程结束,并获取输出和错误
stdout, stderr = process.communicate()

print(f"STDOUT: {stdout}")
print(f"STDERR: {stderr}")
print(f"Return code: {process.returncode}")

Popen 的强大之处在于 communicate() 方法,它处理了所有管道交互,并等待进程结束。你也可以使用 process.poll() 来非阻塞地检查进程是否已结束,或者逐行读取输出(for line in process.stdout:)。


5. 安全警告:关于 shell=True

尽量避免使用 shell=True

  • 安全风险:如果你使用 shell=True 并且命令字符串来自用户输入,攻击者可能会执行任意 shell 命令(这被称为 shell 注入攻击)。
  • 危险示例subprocess.run(f"echo {user_input}", shell=True)
    如果 user_input 是 "; rm -rf / #",后果不堪设想。
  • 性能开销:启动一个 shell(如 /bin/sh)来解析你的命令字符串会产生额外的开销。
  • 何时使用:只有当你需要用到 shell 的特性时,例如通配符 (*)、管道 (|)、重定向 (><)、变量扩展 ($VAR) 等。
# 必要时使用 shell=True 的示例:使用管道
result = subprocess.run('ps aux | grep python', 
                        shell=True, 
                        capture_output=True, 
                        text=True)

总结与最佳实践

  1. 首选 subprocess.run():对于绝大多数情况,这个函数就足够了。
  2. 使用列表传递参数run(['cmd', 'arg1', 'arg2']) 而不是 run('cmd arg1 arg2', shell=True),以避免安全风险。
  3. 善用 capture_output=True 和 text=True:这是捕获输出并将其作为字符串处理的最简洁方式。
  4. 使用 check=True:如果你关心命令是否成功执行,这比手动检查 returncode 更 Pythonic。
  5. 谨慎使用 shell=True:清楚其风险,并仅在必要时使用。
  6. 需要高级控制时使用 Popen:例如需要与进程进行复杂的、实时的交互时。

subprocess 模块是 Python 与系统交互的瑞士军刀,掌握它将极大扩展你的脚本能力。