subprocess模块是 Python 官方推荐用于创建和管理子进程的标准库,它统一并替代了os.system、os.popen等旧式方法。它能让你安全、灵活地执行外部命令,并获取其输入输出。
🚀 核心函数:run()与Popen()
subprocess提供了两个主要接口,你可以按需选用:
subprocess.run()(推荐): 这是 Python 3.5 之后的首选方式。它用于执行一个命令并等待其完成,返回一个包含执行结果的CompletedProcess对象。适合大多数一次性调用场景。subprocess.Popen()(高级): 这是更底层的接口,run()也是基于它实现的-。Popen对象代表一个正在运行的子进程,允许你在进程启动后与之持续交互、检查状态等。适合需要复杂交互、实时流式处理或非阻塞式管理的场景。
run()常用参数详解:
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,universal_newlines=None,**other_popen_kwargs)
运行被args描述的指令。等待指令完成,然后返回一个 CompletedProcess 实例。
| 参数 | 说明 | 示例 |
|---|---|---|
args | 要执行的命令,推荐使用列表形式(如["ls", "-l"]),更安全。 | subprocess.run(["echo", "Hello"]) |
capture_output | 若为True,则捕获子进程的stdout和stderr。 | subprocess.run(..., capture_output=True) |
text | 若为True,则输入输出以文本 (字符串)形式处理,否则为bytes字节流。 | subprocess.run(..., text=True) |
check | 若为True,当子进程返回非零退出码时抛出CalledProcessError异常。 | subprocess.run(..., check=True) |
timeout | 设置超时时间(秒),超时后会抛出TimeoutExpired异常。 | subprocess.run(..., timeout=10) |
input | 向子进程的标准输入发送数据。 | subprocess.run(..., input="y\n") |
env | 为子进程设置自定义的环境变量字典。 | subprocess.run(..., env={"PATH": "/custom/bin"}) |
cwd | 设置子进程的当前工作目录。 | subprocess.run(..., cwd="/path/to/dir") |
🔐 安全第一:警惕shell=True
这是使用subprocess时最需要警惕的风险点。
风险: 当
shell=True时,命令会通过系统的 shell(如/bin/sh或cmd.exe)执行。这意味着命令字符串中的特殊字符(如;,|,&,$等)会被 shell 解析,极易引发命令注入攻击。错误示例:
# 极度危险!切勿使用! user_input = "file.txt; rm -rf /" subprocess.run(f"cat {user_input}", shell=True) # 可能执行删除操作正确做法:尽量避免使用
shell=True。大多数情况下,直接传递参数列表是安全且有效的。# 安全做法 filename = "file with space.txt" subprocess.run(["cat", filename]) # 参数列表,安全可靠何时使用: 仅当命令必须依赖 shell 特性(如通配符
*.txt、管道|、环境变量$HOME)且命令本身是静态的、不包含任何用户输入时,才考虑使用。
🛠️ 核心技巧与进阶用法
1. 捕获与处理输出
分别捕获: 使用
capture_output=True可以分别获取stdout和stderr。合并输出: 如果你想将标准错误合并到标准输出中,可以设置
stderr=subprocess.STDOUT。实时输出: 对于耗时命令,
run()会等待进程结束才返回。如需实时获取输出,需使用Popen逐行读取stdout。
import subprocess # 启动进程 process = subprocess.Popen( ["ping", "-c", "5", "google.com"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1 # 行缓冲 ) # 实时读取输出(逐行) while True: output = process.stdout.readline() if output == '' and process.poll() is not None: break if output: print(output.strip()) # 获取剩余输出和返回码 stdout, stderr = process.communicate()2. 输入与管道
提供输入: 通过
input参数可以向子进程的标准输入发送数据。subprocess.run(["grep", "error"], input="line1\nerror line", text=True)构建管道: 如需实现 Shell 中的管道(如
cmd1 | cmd2),可以手动创建两个Popen对象,并将第一个的stdout连接到第二个的stdin。
使用subprocess实现 Shell 管道(cmd1 | cmd2)的核心原理是:将第一个进程的stdout连接到第二个进程的stdin。
在 Python 中,最标准且安全的方法是手动创建两个Popen对象,并利用subprocess.PIPE来连接它们。下面我将从最基础的实现开始,逐步深入到链式管道和错误处理。
核心实现:连接两个命令
下面以echo "hello world" | grep "world"为例:
import subprocess # 1. 启动第一个进程,将其 stdout 重定向到管道 (PIPE) p1 = subprocess.Popen( ["echo", "hello world"], stdout=subprocess.PIPE, text=True # 以文本模式处理 ) # 2. 启动第二个进程,将其 stdin 设置为 p1 的 stdout p2 = subprocess.Popen( ["grep", "world"], stdin=p1.stdout, # 关键连接点:从 p1 读取输出 stdout=subprocess.PIPE, text=True ) # ⚠️ 极其关键的一步:在父进程中关闭 p1 的 stdout p1.stdout.close() # 3. 获取最终结果(从 p2 读取) output, error = p2.communicate() print(f"输出: {output}") # 输出: hello world print(f"p1 返回码: {p1.wait()}") # 通常为 0 print(f"p2 返回码: {p2.returncode}") # 0为什么必须关闭p1.stdout.close()?
p1.stdout在父进程(你的 Python 脚本)中也持有管道的读取端。如果不关闭它,当
p2处理完数据退出后,p1可能仍在尝试写入数据。更严重的是,如果
p2提前退出(比如只匹配前几行就退出),p1会因为管道缓冲区写满而永久挂起,因为管道的另一端(父进程持有的那个)始终没有关闭。关闭后,当
p2退出时,p1会收到SIGPIPE信号并自动终止,或者正常写完所有数据后退出。
进阶实现:链式管道(cmd1 | cmd2 | cmd3)
假设我们要实现cat file.txt | grep "error" | wc -l,只需按顺序串联即可。核心原则是:始终关闭上一个进程的stdout。
import subprocess # 构建管道链 p1 = subprocess.Popen(["cat", "file.txt"], stdout=subprocess.PIPE, text=True) p2 = subprocess.Popen(["grep", "error"], stdin=p1.stdout, stdout=subprocess.PIPE, text=True) p1.stdout.close() # p1 的输出已交给 p2,父进程不再需要 p3 = subprocess.Popen(["wc", "-l"], stdin=p2.stdout, stdout=subprocess.PIPE, text=True) p2.stdout.close() # p2 的输出已交给 p3,父进程不再需要 # 获取最终输出 result, _ = p3.communicate() print(f"错误行数: {result.strip()}") # 确保所有进程都被回收(如果它们还没退出) p1.wait() p2.wait() p3.wait()封装为通用函数(便于复用):
import subprocess def safe_pipeline(commands): procs = [] for i, cmd in enumerate(commands): if i == 0: p = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) else: p = subprocess.Popen(cmd, stdin=procs[-1].stdout, stdout=subprocess.PIPE, text=True) procs[-1].stdout.close() procs.append(p) output, _ = procs[-1].communicate() # 等待所有进程结束并检查返回码 for p in procs: p.wait() if p.returncode != 0: # 抛出异常或进行错误处理,注意管道中 grep 无匹配时返回 1 是正常的 raise subprocess.CalledProcessError(p.returncode, p.args) return output try: # 故意让 grep 匹配不到(会返回 1) result = safe_pipeline([ ["echo", "abc"], ["grep", "xyz"] ]) except subprocess.CalledProcessError as e: print(f"管道中的命令失败: {e}")补充技巧:同时获取 stderr
如果你想分别获取每个命令的错误信息,可以为每个Popen设置stderr=subprocess.PIPE,然后通过p.communicate()或后续读取p.stderr来获取。
3. 超时控制
为run()或Popen.communicate()设置timeout参数,可以防止子进程永久挂起-。
try: subprocess.run(["sleep", "10"], timeout=5) except subprocess.TimeoutExpired: print("命令执行超时!")4. 环境变量与工作目录
环境变量: 使用
env参数可以为子进程设置独立的变量,而不是继承父进程的。工作目录: 使用
cwd参数可以指定子进程的执行路径。
5. 错误处理
设置check=True可以让run()在命令失败时抛出异常,这比手动检查returncode更简洁。
try: subprocess.run(["false"], check=True) except subprocess.CalledProcessError as e: print(f"命令失败,返回码: {e.returncode}")⚠️ 避坑指南
避免字符串拼接: 构建命令参数时,始终使用列表而非字符串拼接,以防止注入和参数解析错误。
注意文本与字节: 使用
text=True处理文本,使用bytes处理二进制数据。跨平台兼容性: Windows 和 Unix 的命令、路径风格不同,编写脚本时需注意。例如,Windows 上可执行文件通常有
.exe后缀。僵尸进程: 使用
Popen时,务必调用proc.wait()或proc.communicate()来回收子进程资源,避免产生僵尸进程-。PIPE死锁: 如果使用PIPE且数据量很大,必须及时读取,否则可能导致死锁。communicate()方法可以安全地处理这个问题-。
import subprocess # 启动进程 process = subprocess.Popen( ["ping", "-c", "5", "google.com"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1 # 行缓冲 ) # 实时读取输出(逐行) while True: output = process.stdout.readline() if output == '' and process.poll() is not None: break if output: print(output.strip()) # 获取剩余输出和返回码 stdout, stderr = process.communicate()Popen作为上下文管理器: 从 Python 3.2 开始,Popen支持上下文管理器,可以确保资源被正确清理-。with subprocess.Popen(["cmd"], stdout=subprocess.PIPE) as proc: print(proc.communicate()[0])
💎 总结
使用subprocess模块,记住以下三点:
优先使用
subprocess.run(),它足以应对 90% 的需求。默认使用参数列表,除非绝对必要,否则永远不要开启
shell=True。善用
capture_output、text和check等参数,编写更健壮的代码。
参考资料:
https://docs.python.org/zh-cn/3/library/subprocess.html