深入解析 Python subprocess 模块

687 阅读3分钟

subprocess 是 Python 中用于创建和管理子进程的核心模块,它取代了旧的 os.system 和 os.spawn* 函数,提供了更强大、更灵活的子进程管理功能。

一、核心函数详解

1. subprocess.run()(推荐)

Python 3.5+ 引入的现代化接口,适合大多数场景

import subprocess

# 基本用法
result = subprocess.run(["git", "status"], 
                       capture_output=True,  # 捕获输出
                       text=True,            # 输出为文本而非字节
                       check=True,           # 非零退出码时抛出异常
                       timeout=30)           # 超时设置

print("退出码:", result.returncode)
print("标准输出:", result.stdout)
print("错误输出:", result.stderr)

关键参数说明:

  • args:命令列表或字符串(推荐使用列表避免 shell 注入)
  • stdin:输入源(默认 None,可选 PIPE 或文件对象)
  • stdout/stderr:输出目标(PIPE、文件对象或 DEVNULL)
  • cwd:设置工作目录
  • env:自定义环境变量
  • shell:是否通过系统 shell 执行(默认为 False)

2. subprocess.Popen()(高级控制)

提供更底层的进程控制

# 启动后台进程
process = subprocess.Popen(["python", "long_running.py"],
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE,
                          text=True)

# 实时读取输出
while True:
    line = process.stdout.readline()
    if not line: 
        break
    print(f"[实时输出] {line.strip()}")
    
# 等待完成并获取结果
return_code = process.wait(timeout=60)
print(f"进程结束,退出码: {return_code}")

常用方法:

  • poll():检查进程是否结束(返回 None 表示仍在运行)
  • wait(timeout):等待进程结束
  • communicate(input):交互式发送输入并获取输出
  • terminate():发送 SIGTERM 信号
  • kill():发送 SIGKILL 信号

3. 其他实用函数

# 1. 执行命令并获取退出码
exit_code = subprocess.call(["git", "pull"])

# 2. 检查执行结果(非零退出码时抛出异常)
subprocess.check_call(["python", "test.py"])

# 3. 获取命令输出(自动检查错误)
output = subprocess.check_output(["date", "+%Y-%m-%d"], text=True)

二、输入输出管道管理

1. 输入数据到子进程

# 发送输入数据
result = subprocess.run(["grep", "error"], 
                       input="line1\nerror: something\nline3",
                       stdout=subprocess.PIPE,
                       text=True)
print(result.stdout)  # 输出: error: something

2. 多级命令管道

# 执行: ps aux | grep python | awk '{print $2}'
ps = subprocess.Popen(["ps", "aux"], stdout=subprocess.PIPE)
grep = subprocess.Popen(["grep", "python"], stdin=ps.stdout, stdout=subprocess.PIPE)
awk = subprocess.Popen(["awk", "{print $2}"], stdin=grep.stdout, stdout=subprocess.PIPE)

ps.stdout.close()  # 关闭不需要的管道
grep.stdout.close()

pids = awk.communicate()[0].decode().split()
print("Python进程ID:", pids)

3. 输出重定向到文件

with open("output.log", "w") as f:
    subprocess.run(["ls", "-l"], stdout=f)
    
# 错误输出重定向
with open("errors.log", "w") as err_file:
    subprocess.run(["invalid_cmd"], stderr=err_file)

三、高级应用场景

1. 超时控制与错误处理

try:
    result = subprocess.run(["ping", "google.com"], 
                           timeout=5, 
                           capture_output=True,
                           text=True)
except subprocess.TimeoutExpired:
    print("命令执行超时!")
except subprocess.CalledProcessError as e:
    print(f"命令执行失败: {e.stderr}")

2. 跨平台处理(Windows 特殊处理)

import sys

# Windows 下隐藏控制台窗口
startupinfo = None
if sys.platform == "win32":
    startupinfo = subprocess.STARTUPINFO()
    startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
    
subprocess.run(["notepad"], startupinfo=startupinfo)

3. 环境变量管理

# 自定义环境变量
custom_env = {"PATH": "/usr/local/bin", "LANG": "en_US.UTF-8"}
subprocess.run(["echo", "$PATH"], env=custom_env, shell=True)

# 继承当前环境并添加新变量
new_env = {**os.environ, "DEBUG_MODE": "1"}
subprocess.run(["python", "app.py"], env=new_env)

四、安全最佳实践

1. 避免 Shell 注入

# 危险!用户输入可能执行恶意命令
user_input = "hello; rm -rf /"
subprocess.run(f"echo {user_input}", shell=True)  # 可能删除文件!

# 安全做法
subprocess.run(["echo", user_input])  # 仅输出字符串

2. 安全执行用户命令

def safe_execute(command):
    """安全执行用户提供的命令"""
    allowed_commands = {"ls", "date", "pwd"}
    
    if not command:
        raise ValueError("空命令")
    
    cmd_parts = command.split()
    if cmd_parts[0] not in allowed_commands:
        raise PermissionError(f"禁止的命令: {cmd_parts[0]}")
    
    return subprocess.run(cmd_parts, capture_output=True, text=True)

# 使用示例
result = safe_execute("date +%Y")
print("当前年份:", result.stdout.strip())

五、性能优化技巧

1. 批量执行减少进程创建

# 低效:多次创建进程
for file in files:
    subprocess.run(["gzip", file])
    
# 高效:单进程处理所有文件
subprocess.run(["gzip"] + files)

2. 使用 Popen 保持长连接

# 与数据库客户端保持交互
with subprocess.Popen(["mysql", "-u", "user", "-p"], 
                     stdin=subprocess.PIPE,
                     stdout=subprocess.PIPE,
                     text=True) as db:
    
    # 执行多条SQL
    db.stdin.write("SELECT VERSION();\n")
    db.stdin.write("SHOW DATABASES;\n")
    db.stdin.close()
    
    for line in db.stdout:
        print("结果:", line.strip())

3. 异步处理

import asyncio

async def run_async():
    process = await asyncio.create_subprocess_exec(
        "python", "worker.py",
        stdout=asyncio.subprocess.PIPE
    )
    
    # 实时读取输出
    async for line in process.stdout:
        print(f"收到: {line.decode().strip()}")

    await process.wait()

# 运行异步任务
asyncio.run(run_async())

六、常见问题解决方案

1. 处理特殊字符

# Windows 路径问题
path = "C:\Program Files\App"
subprocess.run(["dir", path], shell=True)  # 需要 shell=True

# Linux 特殊字符
subprocess.run(["echo", "含有$符的字符串"], shell=False)  # 安全输出

2. 获取实时输出

process = subprocess.Popen(["ping", "google.com"],
                          stdout=subprocess.PIPE,
                          text=True)

while True:
    output = process.stdout.readline()
    if output == '' and process.poll() is not None:
        break
    if output:
        print(output.strip())

3. 后台进程管理

# 启动后台进程
process = subprocess.Popen(["python", "daemon.py"],
                          stdout=subprocess.DEVNULL,
                          stderr=subprocess.DEVNULL)

# 稍后终止
process.terminate()  # 优雅终止
process.kill()       # 强制终止

七、调试技巧

# 1. 打印实际执行的命令
cmd = ["ls", "-l"]
print("执行命令:", " ".join(cmd))
subprocess.run(cmd)

# 2. 使用日志记录
import logging
logging.basicConfig(level=logging.INFO)

try:
    result = subprocess.run(cmd, capture_output=True, text=True, check=True)
    logging.info("命令成功: %s", result.stdout)
except subprocess.CalledProcessError as e:
    logging.error("命令失败: %s", e.stderr)

# 3. 交互式调试
subprocess.run(["bash"], shell=True)  # 进入交互式 shell