一、subprocess模块基础知识
subprocess模块是Python用于创建和管理子进程(不是线程)、执行外部命令、并能够与创建的子进程的Stdin,Stdout,Stderr(标准输入 / 输出 / 错误流)连接通信,获取子进程执行结束后的返回码,在执行超时或执行错误时得到异常的一个核心模块;替代了老旧的 os.system、os.spawn*、commands.* 等接口,提供更灵活、安全的进程控制能力,让进程间的文本通信和控制更加便捷
1.1、核心概念
# 子进程与父进程:
Python主进程通过subprocess创建的新进程为子进程,父进程可控制子进程的输入、输出、错误流,以及等待子进程结束、获取退出码等
# 管道(Pipe):
父进程与子进程通过标准输入(stdin)、标准输出(stdout)、标准错误(stderr)交换数据的核心机制
# 参数格式:
1. 字符串形式(cmd = "ls -l"): 需依赖系统 shell 解析,存在注入风险
2. 列表形式(cmd = ["ls", "-l"]): 直接传递给操作系统,更安全(推荐)
1.2、核心设计理念
# 统一接口:
通过 Popen 类封装所有进程创建逻辑,高层函数(如run/call/check_call等)基于Popen实现
# 流控能力:
支持管道、重定向、输入输出交互、超时控制、信号发送
# 安全优先:
默认避免shell注入(需显式指定shell=True才启用shell解析)
1.3、核心组件分类
| 类型 | 成员 | 作用 |
|---|---|---|
| 高层函数 | run()、call()、check_call()、check_output()、getoutput()、getstatusoutput() | 简化常见场景的进程调用(推荐优先使用 run()) |
| 核心类 | Popen | 底层进程控制类,支持自定义所有进程参数 |
| 异常类 | CalledProcessError、TimeoutExpired、SubprocessError | 进程执行异常、超时、通用错误 |
| 常量 / 工具 | PIPE、STDOUT、DEVNULL、os.pipe() | 流重定向标记、管道工具 |
二、常用方法
从Python3.5版本开始,subprocess模块内部又进行了一次整合 ,最后就剩下官方推荐的两个接口函数,分别是:
1. subprocess.run()
2. subprocess.Popen()
函数call,check_call,check_output,getoutput,getstatusoutput这些都被run函数代替,它们存在只是为了保持向下兼容
# 同步函数:
* 当一个函数是同步执行时,那么当该函数被调用时不会立即返回,直到该函数所要做的事情全都做完了才返回。
# 异步函数:
* 如果一个异步函数被调用时,该函数会立即返回尽管该函数规定的操作任务还没有完成。
2.1、subprocess.run()函数
run() 是最核心的高层函数,执行指定命令并返回 CompletedProcess 对象(包含执行结果)
* 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, startupinfo=None, creationflags=0, restore_signals=True,
start_new_session=False, pass_fds=(), user=None, group=None, extra_groups=None, umask=-1,
preexec_fn=None, close_fds=True, pipe_size=-1)
调用run方法创建一个进程执行指定的命令,等待命令执行完成后返回一个包含执行结果的CompletedProcess类的实例。是一个同步函数
# 常用参数:
* args: 表示要执行的命令。必须是一个字符串或者字符串参数列表
- 字符串形式的命令支持执行多个,用";"分割。如: cmd = "ls -l /opt/zkc;pwd"
* stdin、stdout 和 stderr: 将子进程的标准输入、输出和错误流进行重定向到指定的管道/文件对象中
- 值为subprocess.PIPE、subprocess.DEVNULL、一个已存在的文件描述符、已打开的文件对象或者None
- subprocess.PIPE: 表示为子进程创建新的管道,可通过返回值读取
- subprocess.STDOUT: 将stderr重定向到 stdout
- subprocess.DEVNULL 表示等同于/dev/null。默认是None,表示什么都不做
* timeout: 设置命令超时时间。如果命令执行时间超时,子进程将被杀死,并弹出TimeoutExpired 异常
* check: 如果该参数设置为True,并且进程退出状态码不是0,则弹出CalledProcessError异常
* encoding: 若指定该参数,则stdin、stdout和stderr可以接收字符串数据,并以该编码方式编码。否则只接收 bytes类型的数据
* shell: 表示是否通过shell来执行命令(Linux下默认为/usr/bin/sh)
- False: args只能是一个命令和参数组成的列表
- True: args可以是一个命令字符串,也可以是命令和参数组成的列表,将通过操作系统的shell执行指定的命令
* cwd: 子进程的工作目录,即执行命令的工作目录(默认当前目录)
* capture_output:
- 为True则等价于stdout=subprocess.PIPE+stderr=subprocess.PIPE,优先于手动设置
* text: 等价于universal_newlines,默认False,为True时启用文本模式(同encoding)
* input: 参直接传递字符串/字节串作为子进程输入,自动隐含stdin=PIPE
# CompletedProcess对象:
* args:执行的命令参数;
* returncode:退出码(0 表示成功);
* stdout:标准输出(字节/字符串,若捕获);
* stderr:标准错误(字节/字符串,若捕获)
# CalledProcessError类属性:
* returncode: 子进程的退出码(非0,符合操作系统退出码规则:0成功,非0失败)。
* cmd: 执行失败的命令(传参是列表则为列表,传参是字符串则为字符串,如 ['ls', 'test.txt'])。
* output: 命令的标准输出(stdout),仅在使用 check_output/run(check=True) 且捕获输出时赋值,否则为 None;(注:check_output会填充该字段,run优先填充 stdout/stderr)
# Python3.5引入 subprocess.run()后,为了更精细化捕获输出,新增了两个属性,替代了单一的output字段,下面两个字段只在run()中能使用
* stdout: 子进程的标准输出,仅当使用run(check=True)且指定 stdout=PIPE/capture_output=True时赋值,否则为None
* stderr: 子进程的标准错误,仅当使用run(check=True)且指定 stderr=PIPE/capture_output=True时赋值,否则为None
2.1.1、shell=True/False
为True时,使用系统新打开的shell来执行命令

| 特性 | shell=True | shell=False(默认) |
|---|---|---|
| 执行方式 | 先启动系统 shell(如 /bin/sh/cmd),由 shell 解析并执行命令 | 直接启动指定的程序,不经过 shell 中转 |
| 命令参数格式 | 可传字符串(如 ls -l /tmp),也可传列表 | 必须传列表(如 [‘ls’, ‘-l’, ‘/tmp’]),字符串会被当作单个程序名 |
| shell 特性支持 | 支持管道、通配符、环境变量、重定向等 shell 语法 | 不支持 shell 语法,需手动实现(如管道用 subprocess.PIPE) |
| 安全性 | 存在命令注入风险(若命令含用户输入) | 无命令注入风险(参数独立传递) |
# shell=True
# 示例1:使用通配符(*)
subprocess.run("ls -l /tmp/*.txt", shell=True) # 正确:shell 解析通配符
# 示例2:使用管道(|)
subprocess.run("ps aux | grep python", shell=True) # 正确:shell 处理管道
# 示例3:使用环境变量($PATH)
subprocess.run("echo $PATH", shell=True) # 正确:shell 替换环境变量
# shell=False
# 示例1:简单命令(列表传参)
subprocess.run(["ls", "-l", "/tmp"]) # 正确:直接启动 ls 程序
# 示例2:错误用法(字符串传参)
# subprocess.run("ls -l /tmp", shell=False) # 报错:系统会尝试找名为 "ls -l /tmp" 的程序(不存在)
# 示例3:替代 shell 管道(手动用 PIPE)
p1 = subprocess.run(["ps", "aux"], stdout=subprocess.PIPE)
p2 = subprocess.run(["grep", "python"], stdin=p1.stdout, stdout=subprocess.PIPE)
print(p2.stdout.decode()) # 等效于 "ps aux | grep python",但更安全
# 安全性:
shell=True 容易引发命令注入,若命令包含用户输入,会导致严重安全问题,例如:
1. 危险示例: 用户输入被拼接进命令
user_input = "; rm -rf /" # 恶意输入
subprocess.run(f"echo {user_input}", shell=True) # 执行:echo ; rm -rf /(删除系统文件)
2. 安全示例: shell=False(参数独立,输入仅作为 echo 的参数)
subprocess.run(["echo", user_input], shell=False) # 仅输出 "; rm -rf /",无风险
# 错误处理:
shell=True 时,命令的返回码是shell的返回码(如sh -c "false"返回1)
shell=False 时,返回码是目标程序的返回码
2.1.2、基础执行(无输出捕获)
空输出处理:若命令执行无错误,stderr为则空字符串(text=True)或空字节串(未指定 text),而非None
# 执行ls -l,不捕获输出
result = subprocess.run(['ls', '-l', '/tmp'])
print(f"退出码: {result.returncode}") # 0表示成功
2.1.3、capture_output/stdout捕获输出 + 文本模式
capture_output=True等价于stdout=subprocess.PIPE+stderr=subprocess.PIPE,一次性捕获标准输出和错误输出,设置了capture_output=True,就不能再设置stdout和stderr
"""
capture_output=True不能和stdout=subprocess.PIPE+stderr=subprocess.PIPE同时出现
"""
#!/usr/bin/python3
import subprocess
res = subprocess.run(
args=["echo", "hello subprocess"],
capture_output=True,
# stdout=subprocess.PIPE,
text=True)
print("code:", res.returncode)
print("stdout:", res.stdout.strip())
2.1.4、check检查退出码 + timeout超时控制
| 特性 | check=True | check=False(默认) |
|---|---|---|
| 退出码检查逻辑 | 主动检查:若子进程退出码非 0(表示执行失败),立即抛出 subprocess.CalledProcessError 异常 | 不检查:无论退出码是 0(成功)还是非 0(失败),均不抛异常,仅将退出码存入返回对象的 returncode 属性 |
| 异常触发条件 | 子进程返回非 0 退出码 | 永不触发(除非进程启动失败,如文件不存在) |
| 错误处理方式 | 强制捕获异常(try/except)或程序崩溃 | 需手动判断 returncode 来处理失败 |
# check=True: 强制捕获异常
#!/usr/bin/python3
import subprocess
# res = ""
try:
res = subprocess.run(
args=["ls", "/teadakbjd"],
check=True,
# capture_output=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=5,
)
except subprocess.CalledProcessError as e:
print(f"命令执行失败: {str(e)}")
print(f"错误输出: {e.stderr}")
print(f"退出码: {e.returncode}")
except subprocess.TimeoutExpired as e:
print(f"命令执行超时: {str(e)}")
else:
print('code:', res.returncode)
print('Have {} bytes in stdout: {!r}'.format(len(res.stdout),res.stdout.decode('utf-8')))
# check=False: 手动判断返回码
#!/usr/bin/python3
import subprocess
res = subprocess.run(
args=["ls", "/teadakbjd"],
check=False,
# capture_output=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=5,
)
if res.returncode != 0:
print(f"命令执行失败: {res.stderr}")
else:
print(f"命令执行成功: {res.stdout}")
2.1.5、捕获错误输出
捕获错误输出的关键是指定 stderr=PIPE(或 capture_output=True),并通过 text=True/encoding 简化字符串处理;结合 check=True 时,需从异常对象中读取 stderr/stdout,而非返回对象。
| 参数值 | 作用 |
|---|---|
| subprocess.PIPE | 捕获错误输出到返回对象的 stderr 属性(最常用) |
| subprocess.STDOUT | 将错误输出合并到标准输出(stdout)中,仅需读取 stdout 即可 |
| subprocess.DEVNULL | 丢弃错误输出(不捕获) |
| 文件对象 / 文件描述符 | 将错误输出写入指定文件(如本地文件) |
| 配置方式 | 错误输出流向 | 核心特点 |
|---|---|---|
| 不捕获 | (默认,stderr=None) 直接输出到父进程的终端 / 控制台 | 错误信息直接显示,程序无法获取这些信息 |
| 捕获 | (stderr=subprocess.PIPE) 被重定向到管道,存入返回对象 | 错误信息「静默存储」,可通过代码解析 |
| 合并捕获 | (stderr=subprocess.STDOUT) 合并到标准输出(stdout)管道 | 统一处理正常 / 错误输出 |
| 场景 | 参数组合 |
|---|---|
| 单独捕获错误输出 | stderr=PIPE + text=True |
| 合并输出(不区分对错) | stdout=PIPE + stderr=STDOUT + text=True |
| 同时捕获 | stdout=PIPE+stderr=PIPE或capture_output=True + text=True |
| 异常时捕获错误输出 | check=True + capture_output=True + try/except |
| 持久化错误输出 | stderr=文件对象 |
2.1.5.1、基础捕获(仅获取错误输出)
使用 stderr=subprocess.PIPE 单独捕获错误输出,需要区分标准输出和错误输出
#!/usr/bin/python3
import subprocess
res = subprocess.run(
['rm', '/asdadasdqw'],
stderr=subprocess.PIPE,)
print("code", res.returncode)
print("错误输出: ", res.stderr.decode('utf-8'))
print("标准输出: ", res.stdout)
"""
# 输出:
退出码: 1
错误输出: rm: cannot remove '/non_existent_file': No such file or directory
标准输出: None
"""
2.1.5.2、合并标准输出和错误输出
使用 stderr=subprocess.STDOUT 将错误输出合并到标准输出,无需区分两类输出
#!/usr/bin/python3
import subprocess
res = subprocess.run(
['ls', '/opt', '/owweqq'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
print("合并输出: ", res.stdout)
"""
# 输出:
合并输出: ls: cannot access /owweqq: No such file or directory
/opt:
zkc
"""
2.1.5.3、结合 check=True 捕获异常时的错误输出
check=True 触发 CalledProcessError 异常时,需先捕获输出,再从异常对象中获取错误信息
#!/usr/bin/python3
import subprocess
try:
completed = subprocess.run(
['grep', 'zkc', '/sdada.txt'],
check=True,
capture_output=True, # 等价于 stdout=subprocess.PIPE + stderr=subprocess.PIPE
text=True,
)
except subprocess.CalledProcessError as err:
print("异常退出码:", err.returncode)
print('标准输出:', err.stdout) # 从异常对象获取标准输出
print('错误输出:', err.stderr) # 从异常对象获取错误输出
"""
# 输出:
异常退出码: 2
标准输出:
错误输出: grep: /sdada.txt: No such file or directory
"""
2.1.5.4、将错误输出写入文件
若需持久化错误输出,可将 stderr 指定为文件对象
import subprocess
# 打开文件(追加模式),将错误输出写入
with open("error_log.txt", "a", encoding="utf-8") as f:
subprocess.run(
["ping", "-c", "1", "invalid_host"],
stderr=f, # 错误输出写入文件
text=True
)
2.1.6、text、universal_newlines和encoding
三者的基础定义,universal_newlines 是旧写法,text 是新别名,encoding 是编码规则。
universal_newlines参数的存在是为了向下兼容(Python3.7开始有text参数,3.5和3.6都是universal_newlines)
| 参数 | 本质作用 | 关联 |
|---|---|---|
| universal_newlines | 开启通用换行符模式,同时将 stdout/stderr/stdin 从字节流转为文本流 | 开启后默认用系统编码(如 utf-8) |
| text | 相当于universal_newlines 的别名,功能完全一致 | text=True 等价于 universal_newlines=True |
| encoding | 指定文本流的编码格式(如 utf-8、gbk)仅在文本模式下生效 | 需配合 text=True/universal_newlines=True 使用 |
- 需要字符串就开 text=True,有编码差异就加 encoding,无需文本处理就用默认字节模式
# subprocess.run()处理输出/输入有两种核心模式,这三个参数的作用就是切换模式
# 默认模式(字节模式)
当text=False(默认)、universal_newlines=False 时:
1. stdout/stderr 的返回值是字节串(bytes),需手动用 decode(encoding) 转字符串
2. stdin 需传入字节串(若需输入)
3. 换行符保留子进程的原始格式(如 \n/\r\n 不转换)
# 文本模式(开启 text/universal_newlines)
当text=True 或 universal_newlines=True 时:
1. stdout/stderr自动按指定编码(或系统默认编码)转为字符串(str);
2. stdin需传入字符串(自动编码为字节流);
3. 换行符会通用化: 子进程输出的 \r\n(Windows)、\r(旧 Mac)会统一转为 \n(这也是universal_newlines 名称的由来)
* text和universal_newlines参数的作用是将stdin,stdout,stderr修改为string模式
- 若不指定text=True或encoding,捕获的stdout/stderr是字节串(bytes类型),需手动解码
- text=True(universal_newlines=True): 将捕获的字节流转为字符串(无需手动 decode())
- 优先使用text
* encoding="utf-8":指定编码,必须在文本模式下生效(text=True/universal_newlines=True)
- 作用是指定字节流<-->字符串的转换编码,解决乱码问题,不存在单独仅使用encoding生效的情况
- 只有当text=True开启文本模式时,encoding才会生效
- text=True + encoding='utf-8': 文本模式开启,encoding 用于字节→字符串解码
- text=True 不传 encoding: 使用系统默认编码(如 utf-8/gbk)解码
- python版本<3.9时,仅传encoding不生效
- python版本>3.9时,会隐性开启文本模式,仅传encoding也生效
- 显式写text=True + encoding='utf-8'
import subprocess
# 默认字节模式
result = subprocess.run(["echo", "你好"], stdout=subprocess.PIPE)
print(type(result.stdout)) # <class 'bytes'>
print(result.stdout.decode("utf-8")) # 需手动解码
# 开启文本模式(自动转字符串)
result = subprocess.run(["echo", "你好"], stdout=subprocess.PIPE, text=True)
print(type(result.stdout)) # <class 'str'>
print(result.stdout) # 你好(直接可用,无需decode)
2.1.7、input 和 stdin
子进程的 stdin(标准输入)是程序接收输入的通道,subprocess.run() 通过 stdin/input 参数向子进程传递输入,替代手动在终端输入内容。
若子进程输入/输出量极大,避免仅用PIPE(可能导致缓冲区满而阻塞),建议用文件作为输入/输出
# stdin:用于告诉子进程 “从哪里读输入”,而非直接传递输入内容
# 参数类型:
* PIPE、
* 文件对象、
* 文件描述符(整数)
* 其他子进程的stdout/stderr(仅限 Popen创建的进程对象,而非run的返回值)、
* None(默认,继承父进程stdin),
# input:是stdin=PIPE+ 向子进程写入数据的简化写法,设置input时会自动隐含 stdin=PIPE
# 参数类型:
* 字节串(bytes)
* 字符串(str, text=True, 会自动按encoding 编码)
# stdin 和 input 参数不能同时使用
ValueError: stdin and input arguments may not both be used
Python3.7~3.9: 允许同时指定stdin和input(仅冗余但不报错)
Python 3.10+: 严格校验,直接抛出 ValueError,禁止同时使用
1. 什么时候用 stdin?
需要从本地文件读取输入(如 grep < file.txt)
需要关联其他 Popen 进程的输出
需复用已有文件 / 管道资源(大文件场景,避免内存拷贝)
2. 什么时候用 input?
直接传递文本 / 字节数据给子进程(90% 的场景)
承接其他 subprocess.run() 的输出(避免管道报错)
2.1.7.1、stdin用法—从文件读取输入到子进程
将文件内容作为子进程的输入
"""
如用cat读取文件,再传给grep,grep python < text.txt
"""
import subprocess
with open('text.txt', w, encoding='utf-8') as f:
f.write("Hello python\nhello java\n")
with open('text.txt', r, encoding='utf-8') as f:
completed = subprocess.run(
['grep', 'python'],
capture_output=True,
stdin=f,
text=True,
encoding='utf-8',
)
print("标准输出: ", completed.stdout)
2.1.7.2、stdin用法—子进程间管道传递, 关联Popen管道(stdin接另一子进程的 stdout)
实现类似 cat test.txt | grep banana 的管道效果,通过 stdin 接收前一个子进程的输出
import subprocess
"""
这里的子进程仅限Popen创建的进程对象,不能是run()的返回值
注意:
❌ 给 stdin 传字符串:会报 AttributeError: 'str' object has no attribute 'fileno'(字符串无文件描述符);
❌ 给 stdin 传字节串:会报 AttributeError: 'byte' object has no attribute 'fileno'(字符串无文件描述符);
❌ 给 stdin 传 subprocess.run() 的返回值:run 返回的 CompletedProcess 无 stdout 管道(已关闭),需用 Popen;
❌ 未关闭管道:可能导致资源泄漏(需手动关闭 Popen 进程的 stdout)
"""
# 用 Popen 创建未阻塞的进程(保留管道)
cat_process = subprocess.Popen(
["cat", "text.txt"],
stdout=subprocess.PIPE # 输出管道(文件对象)
)
# stdin 关联另一个进程的 stdout(仅 Popen 进程对象支持)
result = subprocess.run(
["grep", "python"],
stdin=cat_process.stdout, # 子进程从 cat 的输出管道读取
stdout=subprocess.PIPE,
text=True
)
cat_process.stdout.close() # 释放资源
print("结果:", result.stdout)
2.1.7.3、stdin用法—忽略子进程的输入请求(stdin=DEVNULL)
若子进程要求输入但无需传递内容,用 DEVNULL 避免阻塞
import subprocess
# 示例:执行需要输入的程序,但忽略输入(如 read 命令)
result = subprocess.run(
["bash", "-c", "read name; echo $name"], # bash读取输入后输出
stdin=subprocess.DEVNULL, # 忽略输入请求
stdout=subprocess.PIPE,
text=True
)
print("输出:", result.stdout) # 输出为空(无输入)
2.1.7.4、stdin用法—继承父进程输入(stdin=None,默认)
子进程直接使用终端(父进程)的输入
比如执行 cat 命令,等待用户输入,然后直接输出。其他命令类似
"""
调用系统的cat命令,接收你在终端输入的内容,读取并捕获这些输入,最后把你输入的内容打印出来
1. 执行后终端会进入等待输入状态
2. 比如你输入内容:
hello world
测试内容
3. 按Ctrl+D结束输入(Windows 按 Ctrl+Z 再按回车)
4. 代码会打印
你输入的内容: hello world
测试内容
如果去掉 capture_output=True 会怎样?
1. cat 的输出会直接打印到终端,而 result.stdout 会是 None;
2. 最终代码打印的会是 你输入的内容: None
"""
import subprocess
# 执行 cat 命令,子进程继承父进程 stdin(终端输入)
# 输入内容后按 Ctrl+D(Linux/Mac)或 Ctrl+Z(Windows)结束
"""
相当于Python帮你打开终端执行cat命令→你输入内容→cat把内容返回给Python→Python把内容打印出来
"""
result = subprocess.run(
["cat"],
stdin=None, # 默认值,可省略
capture_output=True,
text=True
)
print("你输入的内容:", result.stdout)
2.1.7.5、input用法—基础字符串
import subprocess
completed = subprocess.run(
['grep', 'python'],
capture_output=True,
input='Hello python\nhello java\n',
text=True,
encoding='utf-8',
)
print("标准字符输出: ", completed.stdout.strip())
2.1.7.6、input用法—基础字节串
completed = subprocess.run(
['grep', 'python'],
capture_output=True,
input=b'Hello python\nhello java\n',
)
print("标准字节输出: ", completed.stdout.strip().decode("utf-8"))
2.1.7.7、input用法—子进程间管道传递(stdin接另一子进程的 stdout)
import subprocess
# 第一步:获取 cat 命令的输出(字符串)
cat_result = subprocess.run(
["cat", "text.txt"],
stdout=subprocess.PIPE,
text=True
)
# 第二步:用 input 传递给 grep(核心:避免 stdin 传字符串)
grep_result = subprocess.run(
["grep", "python"],
input=cat_result.stdout, # 直接传字符串输入
stdout=subprocess.PIPE,
text=True
)
print("管道结果:", grep_result.stdout)
2.1.8、多参数列表
| 特性 | 多参数列表(shell=False) | 字符串(shell=True) |
|---|---|---|
| 参数拆分 | 由 subprocess 直接拆分,按列表顺序传递 | 由 shell 按空格拆分字符串 |
| 空格 / 特殊字符 | 天然支持,无需转义 / 引号 | 需手动加引号 / 转义(如 “my dir”) |
2.1.8.1、参数列表传递给自定义程序多个参数
# 假设写一个简单的Python脚本 test.py,打印所有传入的参数:
# test.py
import sys
print("程序名:", sys.argv[0])
print("参数列表:", sys.argv[1:])
# 用 subprocess 传多参数列表调用
subprocess.run(["python3", "test.py", "arg1", "123", "hello world"], shell=False)
# 相当于执行 python3 test.py arg1 123 'hello world'
# 输出
程序名: test.py
参数列表: ['arg1', '123', 'hello world']
2.1.8.2、带空格的路径
# 列表形式传递含空格的路径,无需转义
subprocess.run(["ls", "-l", "/opt/zkc/my dir"], shell=False)
# 字符串形式不加引号会解析失败(shell=True时)
# shell会把"my"和"dir" 当作两个参数
subprocess.run("ls -l /opt/zkc/my dir", shell=True)
# 加引号
subprocess.run("ls -l /opt/zkc/'my dir'", shell=True)
2.2、subprocess.call()函数
用于执行外部命令的基础函数,会等待命令执行完成并返回退出码(0 表示成功,非 0 通常表示错误),类似于 Linux 中的exit code
1. call()是run()的简化版,仅返回退出码(没有CompletedProcess对象)
2. 等价于subprocess.run(..., check=False).returncode,python3.5+版本推荐用run()
* subprocess.call(args, *, stdin=None, stdout=None, stderr=None,
shell=False, cwd=None, timeout=None, **other_popen_kwargs)
- 参数和run()函数中含义一致
- stdin/stdout/stderr: 重定向输入/输出/错误流(默认继承父进程,即控制台)
import subprocess
# 1. 执行 ls -l,列表形式传参(shell=False)
exit_code = subprocess.call(['ls', '-l'])
print(f"退出码:{exit_code}") # 成功返回 0
等价于
result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
print("退出码:", result.returncode)
# 2. 执行`ls *.py | wc -l`(统计py文件数量),需shell=True
exit_code = subprocess.call('ls *.py | wc -l', shell=True)
# 3. 指定工作目录 + 超时
try:
# 在/tmp 目录执行`sleep 5`,超时3秒
exit_code = subprocess.call(
['sleep', '5'],
cwd='/tmp',
timeout=3
)
except subprocess.TimeoutExpired:
print("命令执行超时!")
# 4. 将输出重定向到文件,错误重定向到 stdout
with open('output.txt', 'w') as f:
exit_code = subprocess.call(
['echo', 'Hello subprocess'],
stdout=f, # 标准输出写入文件
stderr=subprocess.STDOUT # 标准错误合并到标准输出
)
# 5. Windows 示例
exit_code = subprocess.call(['dir'], shell=True) # dir是shell内置命令,需shell=True
2.3、subprocess.check_call()函数
执行命令并检查退出码,若命令执行成功(退出码 0)则返回 0;若退出码非 0,直接抛出 subprocess.CalledProcessError 异常(无需手动判断退出码)
等价于 run(…, check=True),Python 3.5+ 推荐用法:subprocess.run() 加 check=True
* subprocess.check_call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)
- 参数和run()函数中含义一致
- stdin/stdout/stderr: 重定向输入/输出/错误流(默认继承父进程,即控制台)
# 使用subprocess.check_call()时:
1. 即便指定了stdout/stderr=PIPE,该方法也不会捕获管道输出并赋值给异常对象
2. 即便给check_call()传了stdout=PIPE/stderr=PIPE,管道的输出会被操作系统缓存,但check_call不会读取这些输出,也不会将其赋值给异常对象的stdout/stderr属性
# 1. Python3.5+推荐用法:subprocess.run()加check=True(功能等价于 check_call(),且更灵活
try:
# run + check=True 等价于 check_call
result = subprocess.run(['ls', '不存在的文件.txt'], check=True)
except subprocess.CalledProcessError as e:
print(f"失败退出码:{e.returncode}")
print(f"命令输出:{e.stdout}") # 需提前用 capture_output=True捕获
# 2. 若需捕获命令的stdout/stderr,check_call()需手动重定向到文件/管道
with open('output.txt', 'w') as f:
subprocess.check_call(['echo', 'hello'], stdout=f)
# 而run()可通过capture_output=True直接捕获
# run + check=True 捕获输出
result = subprocess.run(['echo', 'hello'], capture_output=True, text=True, check=True)
print(result.stdout) # 直接打印输出
# 3. 执行失败,触发异常
try:
# 执行无效命令(ls 不存在的文件),退出码非 0
subprocess.check_call(['ls', '不存在的文件.txt'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as e:
print(f"命令执行失败!退出码:{e.returncode}")
print(f"执行的命令:{e.cmd}")
print(f"标准输出:{e.stdout}") # None, 不捕获输出
print(f"标准错误:{e.stderr}") # None, 不捕获错误
# 4. 指定工作目录+超时
try:
# 在 /tmp 执行 sleep 5,超时 3 秒
subprocess.check_call(['sleep', '5'], cwd='/tmp', timeout=3)
except subprocess.TimeoutExpired:
print("命令执行超时!")
except subprocess.CalledProcessError as e:
print(f"命令执行失败:{e}")
# 5. 结合shell=True(支持管道/通配符)
try:
# 统计 py 文件数量,若目录无 py 文件,wc -l 返回 0(仍算成功)
subprocess.check_call('ls *.py | wc -l', shell=True)
except subprocess.CalledProcessError as e:
print(f"命令失败:{e}")
2.4、subprocess.check_output()函数
用于获取子进程的标准输出,返回输出内容。若进程退出码非零,则抛出 CalledProcessError 异常
* subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, cwd=None, timeout=None, encoding=None, errors=None, text=None, universal_newlines=None, **other_popen_kwargs)
等价于run(..., check=True, capture_output=True).stdout,仅返回标准输出(字节 / 字符串),命令失败则抛出异常。优先使用run()方法
- 参数和run()函数中含义一致
- stdin/stderr: 重定向输入/错误流
- 没有stdout参数,因为check_output()函数相当于stdout参数
2.4.1、执行简单命令 和 等价实现
# 执行ls -l(Linux/Mac),返回bytes类型输出
result = subprocess.check_output(["ls", "-l"])
print(type(result)) # <class 'bytes'>
print(result.decode("utf-8")) # 解码为字符串
# 等价实现
def my_check_output(args):
result = subprocess.run(
args,
shell=False,
stdout=subprocess.PIPE,
check=True # 非0退出码抛异常
)
return result.stdout # 仅返回stdout(字节串)
2.4.2、通过 shell 执行复杂命令
# 执行shell管道命令(需shell=True)
result = subprocess.check_output(
"ps aux | grep python",
shell=True,
encoding="utf-8"
)
print(result)
2.4.3、捕获标准错误,合并到输出
#!/usr/bin/python3
import subprocess
try:
# 执行错误命令,捕获stderr并合并到stdout
result = subprocess.check_output(
["pings", "-c", "2", "google.com"],
timeout=3,
stderr=subprocess.STDOUT, # 合并stderr到stdout
encoding="utf-8"
)
print("执行成功:", result)
except subprocess.CalledProcessError as called:
print(f"命令执行失败,退出码:{called.returncode}")
print(f"错误输出:{called.output}") # 包含stderr的内容
except subprocess.TimeoutExpired as te:
print(f"命令超时: {str(te)}")
except FileNotFoundError as fn:
print(f"命令不存在: {str(fn)}")
2.5、subprocess.getoutput(cmd)
执行系统命令并返回命令的输出结果(包含标准输出和标准错误),无论命令执行成功 / 失败,都会返回输出字符串(而非抛出异常),无需手动处理异常
* 输入: 仅接受字符串形式的命令(如 `"ls -l|grep .py"`),无需拆分参数
- 可以执行多个命令,用";"分割
* 输出: 返回命令执行的所有输出(stdout+stderr),类型为字符串,会自动编码输出
* 执行方式: 强制通过系统 Shell 执行(等价于 shell=True),支持管道
- 等价于subprocess.run(..., shell=True),有命令恶意注入风险
* 错误处理: 命令执行失败(返回非0退出码)不会抛异常,仅返回错误输出(如 ls xxx)
2.5.1、getoutput使用run()等层等价实现
def my_getoutput(cmd):
import subprocess
result = subprocess.run(
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # 将stderr重定向到stdout
text=True
)
return result.stdout
def getoutput(cmd):
with Popen(cmd, shell=True,
stdout=PIPE, stderr=STDOUT,
text=True) as proc:
output = proc.communicate()[0]
return output.rstrip('\n')
2.5.2、getoutput简单示例
# 执行成功的命令
cmd = "echo 'Hello, subprocess'"
output = subprocess.getoutput(cmd)
print("输出结果:", output) # 输出:Hello, subprocess
# 执行多命令,用 ; 分隔
cmd = "pwd; ls -l" # 先查当前目录,再列文件
output = subprocess.getoutput(cmd)
print(output)
# 执行失败的命令(无报错,返回错误输出)
cmd_error = "ls non_exist_file"
output_error = subprocess.getoutput(cmd_error)
print("错误输出:", output_error) # 输出:ls: 无法访问 'non_exist_file': 没有那个文件或目录
# 执行带管道的复杂命令(依赖shell,所以能正常运行)
cmd_pipe = "ps aux | grep python | wc -l"
output_pipe = subprocess.getoutput(cmd_pipe)
print("Python进程数:", output_pipe)
# 命令注入风险
user_input = "; rm -rf /" # 恶意输入
cmd = f"echo {user_input}"
subprocess.getoutput() # 执行 `echo ; rm -rf /` 删除系统文件!
# 安全:参数拆分,仅执行 echo “; rm -rf /“(作为普通字符串输出)
cmd2 = ["echo", "; rm -rf /"]
res = subprocess.check_output(cmd2 )
print(res) # b'; rm -rf /\n'
2.6、subprocess.getstatusoutput(cmd)
用于执行命令并返回命令的退出状态码和输出内容(标准输出+标准错误),底层默认通过Shell执行命令(等价于shell=True),返回一个二元数组
cmd: 要执行的系统命令:
- 字符串形式: 直接交给Shell解析。如 "ls -l"
- 列表形式: 仍会通过Shell执行。(如 ["ls", "-l"]),不推荐这种方式
# 返回一个二元元组 (exitcode, output):
1. exitcode: 命令退出状态码,0 表示执行成功,非 0 表示失败
2. output: 命令的标准输出+标准错误拼接后的字符串(自动解码为文本,默认编码依赖系统)
# 执行成功
exitcode, output = subprocess.getstatusoutput("ls -l")
print("退出状态码:", exitcode) # 0(成功)
print("输出内容:\n", output)
# 执行失败
exitcode, output = subprocess.getstatusoutput("abc123")
print("退出状态码:", exitcode) # 非0(如127)
print("错误输出:\n", output) # 包含 "command not found" 等信息
# 安全替代方案
# 内部默认shell=True,如果cmd包含用户输入,可能导致Shell注入攻击
user_input = "somefile; rm -rf /"
cmd = f"ls {user_input}"
status, output = subprocess.getstatusoutput(cmd)
# 推荐用run方法替代
result = subprocess.run(
["ls", "-l"], # 参数列表形式,不通过 Shell 解析
capture_output=True,
text=True,
encoding="utf-8"
)
exitcode = result.returncode
output = result.stdout + result.stderr
print(exitcode, output)
2.6.1、使用run()等价实现
def my_getstatusoutput(cmd):
import subprocess
result = subprocess.run(
cmd,
shell=True,
capture_output=True, # 捕获 stdout/stderr
text=True # 输出为字符串(而非字节)
)
return (result.returncode, result.stdout + result.stderr)
2.7、subprocess.Popen()函数
subprocess 模块的底层核心类,用于手动创建和管理子进程,相比高层封装的 subprocess.run(),它提供了对进程生命周期的完全掌控(异步执行、实时读写输出、自定义进程环境 / 行为等),是处理复杂子进程场景的核心工具
2.7.1、Popen函数参数 和 Popen对象方法
subprocess.Popen(
args, bufsize=-1, executable=None, stdin=None,stdout=None,
stderr=None, preexec_fn=None, close_fds=True, shell=False,
cwd=None, env=None, universal_newlines=None, startupinfo=None,
creationflags=0, restore_signals=True, start_new_session=False,
pass_fds=(), *, encoding=None, errors=None, text=None)
# 一、必选核心参数:
* args: 要执行的命令。可以是一个字符串,也可以是一个字符串列表
- shell=True: args可以是一个命令字符串,也可以是命令和参数组成的字符串列表,将通过操作系统的shell执行指定的命令
- shell=False: args必须是一个命令和参数组成的列表。[程序路径, 参数1, 参数2]
* shell: 是否通过系统shell执行命令(Linux:bash/sh;Windows:cmd.exe)
- False: 直接调用操作系统的exec函数执行程序,无shell解析,安全高效
- True: 先启动shell,再通过shell解析并执行命令,支持shell语法,但有注入风险
# 二、I/O控制参数
* stdin: 子进程的标准输入(stdin)重定向到的目标
- None: 默认值,等同于stdin=0,继承父进程的stdin,即终端输入
- 常用取值:
- subprocess.PIPE: 创建管道,父进程可通过p.stdin向子进程写入数据
- subprocess.DEVNULL: 重定向到空设备,则子进程读取不到任何输入
- 文件对象: 重定向到已打开的文件,需以r/w/a等模式open打开
- 整数: 文件描述符,如0表示标准输入
* stdout: 子进程的标准输出(stdout)重定向到的目标
- None: 默认值,等同于stdout=1,继承父进程的stdout,即输出到控制台
- 常用取值:
- subprocess.PIPE: 创建管道,父进程可通过p.stdout读取子进程输出
- subprocess.DEVNULL: 重定向到空设备(丢弃输出)
- 文件对象: 重定向到已打开的文件,需以r/w/a等模式打开
- 整数: 文件描述符,如1表示标准输出
* stderr: 子进程的标准错误(stderr)重定向到的目标
- None: 默认值,等同于stderr=2,继承父进程的stderr,即输出到控制台
- 常用取值:
- subprocess.PIPE: 创建管道,父进程可通过p.stderr读取错误信息
- subprocess.DEVNULL: 丢弃错误输出
- subprocess.STDOUT: 将错误流合并到标准输出流(统一处理输出/错误)
- 文件对象/整数: 同stdout规则
# 三、缓冲区与I/O模式参数
* bufsize: 设置子进程I/O缓冲区大小(字节),控制数据读写的缓冲策略,默认-1
- 0: 不使用缓冲区,仅二进制模式有效,实时读写
- 1: 表示行缓冲,仅当文本模式时有效,按行读写
- 正数: 表示缓冲区大小(字节)
- 负数: 表示使用系统默认的缓冲区大小,通常为4096/8192字节
* text(别名:universal_newlines): 控制子进程I/O的数据类型,是否启用文本模式
- None: 默认值,等价于False
- True: I/O以字符串处理,自动按encoding参数编解码
- False: I/O以字节串处理,需手动编解码
* encoding: 仅text=True时生效,指定字符串I/O的编码格式
- None: 默认值,使用系统默认编码。如Linux:utf-8;Windows:gbk
* errors: 仅text=True时生效,指定编码/解码错误的处理方式
- None: 默认值,使用系统默认规则,通常为strict
- strict: 编码错误时默认抛出UnicodeError异常
- replace: 用?替换错误字符
- backslashreplace: 用转义字符替换
* pipesize: 设置PIPE管道的缓冲区大小(字节),替代bufsize对管道的控制
- Python3.10+ 新增该参数
- -1: 默认值,采用系统默认
# 四、环境与路径参数
* cwd: 设置子进程的工作目录
- None: 表示继承父进程的工作目录
- 如: cwd='/home/user/projects',表示args中的命令去该目录下执行
* env: 设置子进程的环境变量
- None: 默认值,继承父进程的环境变量,即os.environ
- 取值规则: 字典形式,若需保留原有环境,需先拷贝os.environ再修改
# 自定义环境(仅保留指定变量)
env={'PATH': '/usr/bin', 'MY_VAR': '123'}
# 保留原有环境并新增变量(推荐)
env = os.environ.copy()
env['MY_VAR'] = '123'
# 继承当前环境
custom_env = {
**os.environ, # 继承当前环境变量
"CUSTOM_VALUE": "my_value",
"JAVA_HOME": "/opt/jdk/jdk8-15u",
"PATH": f"/custom/bin:{os.environ.get('PATH', '')}" # 在PATh变量中添加新的bin路径
}
# 五、进程执行控制参数
* executable: 自定义执行器路径,覆盖args中的程序路径
- 仅 shell=False 时有效
- None: 默认值,使用args[0]作为执行器路径
- 适用场景: 需指定非默认的执行器,如用/bin/bash执行脚本,而非/bin/sh
- executable='/bin/bash': 强制用bash执行脚本
* close_fds: 是否关闭父进程中除stdin/stdout/stderr外的所有文件描述符
- 目的: 避免子进程继承无关文件
- 默认值:
- Linux/macOS: True
- Windows: False,但会关闭除标准流外的文件描述符
- 注意:
- close_fds=True时,stdin/stdout/stderr不能指向普通文件,需用PIPE/DEVNULL
# 六、平台专属参数
* preexec_fn: 子进程启动前执行的可执行对象(回调函数)
- 设置子进程的信号处理、进程组、资源限制等
- 仅Unix平台下有效,Windows不支持
- 仅在fork后、exec前调用
- None: 默认值
* start_new_session: 是否为子进程创建新的会话
- 作用: 子进程脱离父进程的会话组,避免被父进程的信号影响(如Ctrl+C不会终止子进程)
- False: 默认值,不为子进程创建新的回话
- True: 子进程将在新的会话中运行
* restore_signals: 是否将子进程的信号处理恢复为系统默认,覆盖 Python 父进程的信号设置
- True: 默认值,所有在子进程中设置为默认值的信号,Python3.8+的参数
- 作用: 避免子进程继承父进程的自定义信号处理逻辑
# Popen 对象方法:
* communicate(input=None, timeout=None): 与子进程交互
- 使用input发送数据到stdin
- 从stdout和stderr读取数据,直到EOF
- 等待进程退出
- 返回: 一个元组(stdout_data, stderr_data)
* wait(timeout=None): 等待子进程终止,返回返回码。如果超时,则抛出TimeoutExpired异常
* poll(): 检查子进程是否已终止
- 终止: 终止返回returncode
- 未终止: 则返回None
* send_signal(singnal): 向子进程发送信号(Unix专属,如signal.SIGKILL)
* terminate(): 终止子进程,也就是发送SIGTERM信号到子进程
* kill(): 强制终止子进程,也就是发送SIGKILL信号到子进程
# Popen 对象属性:
* returncode(属性): 退出码,未结束为None
* pid: 子进程PID
# 1. 检查进程状态
proc = subprocess.Popen(["sleep", "5"])
while proc.poll() is None:
print("Process is still running...")
import time
time.sleep(1)
print(f"Process finished with code: {proc.returncode}")
# 2. 等待进程结束
proc = subprocess.Popen(["sleep", "3"])
# 阻塞等待
returncode = proc.wait()
print(f"Return code: {returncode}")
# 带超时的等待
try:
returncode = proc.wait(timeout=2)
except subprocess.TimeoutExpired:
print("Process timed out!")
2.7.2、stdin/stdout/stderr重定向到文件描述符的说明
Popen的stdin/stdout/stderr参数,只决定子进程的输入从哪来、正常和错误输出到哪里去,和父进程的输入/输出无关,只是这个来源或者去向可能是父进程里的某个管道
在 Unix/Linux/Windows 系统中,进程启动时会默认打开 3 个标准文件描述符:
| 整数 | 名称 | 含义 | 读写属性 |
|---|---|---|---|
| 0 | STDIN_FILENO | 标准输入(键盘 / 终端等) | 只读 |
| 1 | STDOUT_FILENO | 标准输出(终端 / 屏幕等) | 只写 |
| 2 | STDERR_FILENO | 标准错误(终端 / 屏幕等) | 只写 |
| 管道名 | 类比 | 对应数字FD |
|---|---|---|
| stdin | 进程的输入口 (只进不出) | 0 |
| stdout | 进程的输出口 (只出不进) | 1 |
| stderr | 进程的错误输出口(只出不进) | 2 |
| 参数 | 传入值 | 效果 (子进程的输入来源 / 输出 / 错误去向) | 实际用途 | 补充说明 |
|---|---|---|---|---|
| stdin | 0 / None | 子进程从父进程的终端(键盘)读取输入(默认) | 直接给子进程手动输入内容 | Linux 终端下 1/2 也会复用终端设备,效果同 0,但非通用写法 |
| stdin | 1 | 通用场景:报错(接父进程只写的输出口)Linux 终端:复用终端设备,能读输入 | 错误用法,绝对别传 | 仅终端场景偶然可用,移植性极差 |
| stdin | 2 | 通用场景:报错(接父进程只写的错误输出口) Linux 终端:复用终端设备,能读输入 | 错误用法,绝对别传 | 仅终端场景偶然可用,移植性极差 |
| stdin | 其他可读 FD | 子进程从父进程打开的可读文件 / 管道读取输入 | 复用父进程已打开的输入源(少用) | 需确保 FD 是可读的,否则报错 |
| stdin | PIPE | 父进程通过 p.stdin.write() 给子进程传输入 | 程序自动给子进程输入 | 最常用的编程场景 |
| stdout | 1 / None | 子进程正常输出到父进程的屏幕(默认) | 直接看子进程运行结果 | - |
| stdout | 0 | 报错(接父进程只读的输入口) | 错误用法,别传 | - |
| stdout | 2 | 子进程正常输出走父进程错误输出通道 | 几乎不用 | 输出仍显示在屏幕,无实际意义 |
| stdout | 其他可写 FD | 子进程正常输出写入父进程打开的文件 | 替代直接传文件对象(少用) | 需确保 FD 是可写的 |
| stdout | PIPE | 父进程能读取子进程的正常输出 | 处理 / 解析子进程的运行结果 | 最常用的编程场景 |
| stderr | 2 / None | 子进程错误输出到父进程的屏幕(默认) | 直接看子进程的错误提示 | - |
| stderr | 0 | 报错(接父进程只读的输入口) | 错误用法,别传 | - |
| stderr | 1 | 子进程错误输出走父进程正常输出通道 | 正常 / 错误混输(不如用 STDOUT) | 语义不清晰,不推荐 |
| stderr | 其他可写 FD | 子进程错误输出写入父进程打开的文件 | 记录错误日志(少用) | 需确保 FD 是可写的 |
| stderr | PIPE | 父进程单独读取子进程的错误输出 | 区分正常输出和错误输出 | 需分别处理 stdout/stderr |
| stderr | STDOUT | 子进程错误输出合并到正常输出中 | 一次性读取所有输出(正常 + 错误) | 最常用的统一处理场景 |
2.7.2.1、stdin=整数
子进程的标准输入从哪里来
# 第一步:先分清 父进程 和 子进程
1. 父进程:就是你写的Python脚本本身(比如test.py),你运行python test.py,这个Python程序就是父进程
2. 子进程: 是Python脚本里通过Popen启动的另一个程序(比如cat、ls、cmd等)
# 第二步:每个进程都有 输入口 和 输出口
不管父、子进程,都有两个最基础的"管道":
1. 子进程cat的stdin输入口: cat命令要读取的内容,就得从这个口进
2. 子进程cat的stdout输出口: cat读完内容后,要把内容打印出来,就从这个口出
3. 父进程的stdin: 比如你在终端给Python脚本输入内容,就从父进程的0号口进
4. 父进程的stdout: Python脚本打印print()内容,就从父进程的1号口出
# 第三步:Popen(stdin=数字)
* 本质: 将子进程的"标准输入"重定向到父进程中该整数对应的文件描述符(子进程会复用父进程的这个FD),相当于接父进程的几号水管
* 指定"子进程的输入口(stdin)要接父进程这边的哪根水管" —— 数字就是父进程里 "水管的编号"(FD)
情况1: stdin=0 → 子进程的输入口,接父进程的 0 号水管(父的输入口)
(1) 父进程的0号水管: 是父进程自己的输入来源(比如终端键盘)
(2) 子进程接这个水管: 子进程的输入来源 = 父进程的输入来源(键盘)
# 子进程cat的输入口 → 接父进程的0号水管(键盘)
# 运行后,你可以在终端敲任何内容,回车
# 子进程cat会从键盘(父的0号口)读到终端输入的内容,并立刻打印出来
# 和直接在终端输cat XXX效果一样
p = subprocess.Popen(["cat"], stdin=0)
p.wait()
情况2: stdin=1 → 子进程的输入口,接父进程的 1 号水管(父的输出口)
(1) 父进程的1号水管: 是父进程的输出口(只出不进,比如屏幕),只能往外写,不能往里读
(2) 子进程接这个水管 → 子进程想从"只出不进"的管道里读东西,直接报错
try:
# 子进程cat的输入口 → 接父进程的1号水管(屏幕)
p = subprocess.Popen(["cat"], stdin=1)
p.wait()
except OSError as e:
print("报错原因:", e) # 坏文件描述符Bad file descriptor
情况3: stdin=2 → 和stdin=1完全一样
父进程的 2 号水管是 “错误输出口”,也是 “只出不进” 的,子进程的输入口接这里,同样报错
# 第四步、别纠结 “父/子输入输出”,只盯一个点
1. 不用管父进程的输入/输出,只问自己: 我想让子进程从哪拿输入?
2. 想让子进程从键盘拿 → 不用传stdin(默认None等价于stdin=0)
3. 想让子进程从文件拿 → 用stdin=open("文件.txt", "r")
4. 想让父进程给子进程传输入 → 用stdin=subprocess.PIPE(父进程通过p.stdin.write()写内容,子进程读)
5. 绝对别传1/2(接错水管,必报错)
import subprocess
# 优雅写法:
# 给子进程cat的输入口接“管道”,父进程往管道里写内容
p = subprocess.Popen(["cat"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
p.stdin.write(b"我是父进程给子进程的输入\n") # 父进程写内容到管道
p.stdin.close()
print("子进程的输出:", p.stdout.read().decode()) # 子进程读管道内容并输出
# 结果:子进程的输出:我是父进程给子进程的输入
举例:stdin重定向到非 0/1/2
比如父进程先打开一个文件(拿到fd号),子进程的输入口接这个水管
import subprocess
import os
"""
子进程cat的输入来源是test.txt文件(父进程的 {fd} 号水管)
运行后,cat会直接打印test.txt的内容(相当于你在终端输cat test.txt)
"""
# 父进程打开test.txt文件,拿到“可读”的水管号fd(比如fd=3)
fd = os.open("test.txt", os.O_RDONLY)
# 子进程cat的输入口 → 接父进程的3号水管(test.txt文件)
p = subprocess.Popen(["cat"], stdin=fd)
p.wait()
os.close(fd) # 父进程用完水管,关掉
2.7.2.2、stdout=整数
子进程的正常输出去向,即处理完的正常结果要放到哪
情况1: stdout=1(默认行为,等价于stdout=None)
(1) 子进程的正常输出口重定向到父进程的正常结果标准输出口,如终端屏幕
(2) 通俗讲就是子进程的正常输出口 --> 接父进程的 1 号水管(屏幕)
p = subprocess.Popen(["ls"], stdout=1)
p.wait()
# 运行后,ls列出的文件列表直接打印到终端(和你直接输 ls 效果一样)
情况2: stdout=0,子进程的正常输出口 → 接父进程的 0 号水管
(1) 子进程的正常输出重定向到父进程的标准输入口,如键盘,键盘是只读的,不能输出内容
(2) 通俗讲就是把子进程的输出内容"放到键盘里",但键盘是"只进不出",所以直接报错
try:
p = subprocess.Popen(["ls"], stdout=0)
p.wait()
except OSError as e:
print("报错:", e) # 坏文件描述符
情况3: stdout=2,子进程的正常输出口 → 接父进程的 2 号水管(父的错误输出口,屏幕)
(1) 子进程的正常输出重定向到父进程的标准错误输出口,如终端屏幕
(2) 通俗来讲就是把子进程的"标准输出"和"标准错误"都放到同一屏幕位置
p = subprocess.Popen(["ls"], stdout=2)
p.wait()
# 运行后,文件列表依然打印到屏幕(因为父进程的1/2号水管默认都连屏幕),只是底层走的是 “错误输出通道”。
情况4: stdout=其他有效FD(父进程打开的可写文件)
(1) 子进程的正常输出重定向到父进程已打开的文件
# 父进程打开文件(可写),拿到水管号fd(比如fd=3)
fd = os.open("ls_result.txt", os.O_WRONLY | os.O_CREAT)
# 子进程ls的正常输出 → 写入这个文件
p = subprocess.Popen(["ls"], stdout=fd)
p.wait()
os.close(fd) # 关闭水管
# 优雅写法:
(1) 子进程的正常输出写到"父进程能读取的管道"(父进程可以拿子进程的输出)
p = subprocess.Popen(["ls"], stdout=subprocess.PIPE)
output = p.stdout.read().decode() # 父进程读取子进程的输出
(2) stdout=open("file.txt", "w"): 直接写入文件
2.7.2.3、stderr=整数
子进程的标准错误输出去向
情况1: stderr=2(默认行为,等价于stderr=None)
(1) 子进程的标准错误输出重定向到父进程的标准错误输出口,如终端屏幕
p = subprocess.Popen(["ls", "不存在的文件.txt"], stderr=2)
p.wait()
# 终端屏幕会打印错误:ls:不存在的文件.txt: No such file or directory
情况2: stderr=1,子进程的错误输出口 → 接父进程的1号水管(父进程的正常输出口)
(1) 子进程的标准错误输出重定向到父进程的标准输出
(2) "标准错误"和"标准输出"混合在一起在终端/屏幕显示
p = subprocess.Popen(["ls", "不存在的文件.txt"], stderr=1)
p.wait()
# 运行后,错误提示依然打印到屏幕,和默认行为无区别,但底层走"正常输出通道"
情况3: stderr=0,子进程的错误输出口 → 接父进程的0号水管(只读的输入口)
(1) 和stdout=0一样,直接报错,不能往只读的管道输入内容
情况4: stderr=subprocess.STDOUT
(1) 子进程的标准错误输出和标准输出合并到正常输出里
情况5: stderr = 其他有效 FD(写入文件)
(1) 子进程的错误提示 → 写入父进程打开的文件
# 父进程打开文件(可写)
fd = os.open("error_log.txt", os.O_WRONLY | os.O_CREAT)
# 子进程的错误输出 → 写入这个文件
p = subprocess.Popen(["ls", "不存在的文件.txt"], stderr=fd)
p.wait()
os.close(fd)
# 运行后,错误提示会写到error_log.txt里,而非屏幕
2.7.2.4、stdin输入1/2没有报错的情况
因为Linux终端(tty/pty)的文件描述符 0/1/2 是特殊的 —— 它们默认都指向同一个终端设备,且终端设备本身是可读写的(和普通文件/管道的只读 / 只写不同),这才导致stdin=1/stdin=2不仅不报错,还和stdin=0效果一致
# 核心原因:Linux 终端的 FD 0/1/2 共享同一个 “可读写” 的终端设备
1. Linux中,当你在交互式终端(如ssh登录的终端、本地终端)运行Python 脚本时:
父进程的FD 0/1/2并不是"独立的只读/只写管道",而是都指向同一个终端设备文件(比如/dev/pts/0)
且这个终端设备支持同时读写(既可以从键盘读输入,也可以往屏幕写输出)。
2. 普通文件/管道: 0 号水管(stdin)是"只读的进水管",1/2号是"只写出水管",接错必报错
3. 终端设备: 0/1/2号水管其实是连到同一个终端,所以既可以从键盘读入,也可以写到屏幕
你在终端里运行脚本时,stdin=0/1/2最终都指向同一个终端设备,所以子进程都能从键盘读输入,效果完全一样
# 验证终端FD指向的脚本:check_fd.py
import os
# 打印 FD 0/1/2 对应的设备路径
for fd in [0, 1, 2]:
try:
# 获取 FD 对应的设备路径
path = os.readlink(f"/proc/self/fd/{fd}")
print(f"FD {fd} → {path}")
except Exception as e:
print(f"FD {fd} → {e}")
# 输出:
# FD 0/1/2 都指向/dev/pts/0(同一个终端设备),所以子进程stdin=1本质还是连到终端,能正常读输入
FD 0 → /dev/pts/0
FD 1 → /dev/pts/0
FD 2 → /dev/pts/0
2.7.3、案例: 合并捕获标准输出和标准错误
#!/usr/bin/python3
"""
在/opt路径下执行命令
"""
try:
res = subprocess.Popen(
"echo hello && sleep 10 && ls noexist",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
shell=True,
cwd="/opt"
)
output, _ = res.communicate(timeout=5)
print(f"输出: {output}")
print(f"退出码: {res.returncode}")
except subprocess.TimeoutExpired:
res.kill()
print("命令执行超时")
proc = subprocess.Popen(
'cat -; echo "to stderr" 1>&2',
shell=True,
# 输入管道
stdin=subprocess.PIPE,
# 输出管道
stdout=subprocess.PIPE,
# 错误输出
stderr=subprocess.STDOUT,
)
msg = 'through stdin to stdout\n'.encode('utf-8')
stdout_value, stderr_value = proc.communicate(msg)
print('combined output:', repr(stdout_value.decode('utf-8')))
print('stderr value :', repr(stderr_value))
2.7.4、案例: 实时读取子进程输出(异步)
read():读取文本全部的内容,返回值类型是str
readline()方法:读取文件中的下一行,直到到达文件的末尾。每次调用返回一个字符串,包含读取到的一行内容(包括换行符\n)
readlines()方法:读取整个文件,并将文件的每一行作为一个列表的元素返回。每个元素都是一个字符串,包括换行符\n
#!/usr/bin/python3
import subprocess
import sys
res = subprocess.Popen(
["ping", "-c", "5", "www.baiduss.com"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
)
while True:
line = res.stdout.readline()
if not line and res.poll() is not None: # 无输出且进程退出
break
if line:
print(f"实时输出: {line.strip()}")
# 读取剩余错误信息
stderr = res.stderr.read()
if stderr:
print(f'错误:{stderr}', file=sys.stderr)
#!/usr/bin/python3
import subprocess
from threading import Thread
res = subprocess.Popen(
["ping", "-c", "5", "www.baiduss.com"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
)
def read_output(stream, prefix):
for line in iter(stream.readline, ''):
print(f"{prefix}: {line.strip()}")
if __name__ == '__main__':
# 获取输出的进程
output_p = Thread(target=read_output, args=(res.stdout, "STDOUT"))
# 获取错误的进程
err_p = Thread(target=read_output, args=(res.stderr, "STDERR"))
output_p.start()
err_p.start()
res.wait()
output_p.join()
err_p.join()
2.7.4.1、communicate和p.stdout的获取输出的区别
Popen.communicate() 和直接访问 p.stdout 是两种完全不同的子进程输出处理方式,核心差异在于安全性、易用性、资源管理
区别一:死锁风险
子进程的stdout/stderr管道有固定大小的缓冲区,如果子进程输出超过缓冲区且父进程未及时读取,子进程会卡在write()调用上(阻塞),导致死锁
# communicate():
内部通过select/poll或多线程处理stdout/stderr的并发读取,确保两个管道的缓冲区都能及时清空,从根本避免死锁。
# 直接p.stdout:
如果只读stdout而忽略stderr(或反过来),当stderr缓冲区满时,子进程会阻塞,父进程也会卡在stdout.read(),永远无法继续
# 子进程同时输出大量stdout和stderr(超过缓冲区)
p = subprocess.Popen(
['bash', '-c', 'for i in {1..10000}; do echo "out$i"; echo "err$i" >&2; done'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# 错误:先读stdout,此时stderr缓冲区满,子进程阻塞,这里永远卡着
out = p.stdout.read() # 死锁!子进程卡在stderr的write(),父进程卡在read()
err = p.stderr.read()
p.wait()
# 正确:
out, err = p.communicate() # 无死锁,正常读取所有输出
print(f"stdout长度:{len(out)}, stderr长度:{len(err)}")
区别二:数据读取与子进程管理
# communicate():
一次性读取stdout/stderr直到 EOF(子进程结束),返回(stdout_data, stderr_data)(如果设置stderr=STDOUT,则 stderr 为None)
自动调用p.wait()等待子进程结束,不会产生僵尸进程。
# 直接p.stdout:
p.stdout是一个类文件对象(继承自io.BufferedReader),需要手动调用read()/readline()/iter(line)读取;
读取后子进程可能仍在运行,必须手动调用p.wait()回收子进程,否则会残留僵尸进程
p = subprocess.Popen(['echo', 'hello\nworld'], stdout=subprocess.PIPE, text=True)
# 逐行读取实时输出
for line in iter(p.stdout.readline, ''):
print(f"实时输出:{line.strip()}")
p.stdout.close() # 手动关闭句柄
p.wait() # 手动等待子进程结束,避免僵尸进程
区别三:与标准输入交互能力
communicate()支持通过input参数向子进程的stdin传递数据(比如给需要交互的命令传输入),而直接操作需要手动管理stdin:
# communicate()传输入--简洁安全
p = subprocess.Popen(['grep', 'hello'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
out, err = p.communicate(input='hello world\nfoo bar\n')
print(out) # 输出:hello world
# 直接操作stdin--繁琐且易出错
p = subprocess.Popen(['grep', 'hello'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
p.stdin.write('hello world\nfoo bar\n')
p.stdin.flush() # 强制刷入缓冲区
p.stdin.close() # 关闭stdin,让子进程知道输入结束
out = p.stdout.read()
p.wait()
print(out)
区别四:资源释放
communicate()执行后会自动关闭stdout/stderr/stdin的管道句柄,无需手动操作;而直接访问p.stdout如果不手动关闭,会导致文件句柄泄漏
2.7.5、案例: 向子进程输入数据
2.7.5.1、使用 communicate(input=) 一次性写入(推荐)
"""
1. input参数:Python3中若未指定encoding,需传入bytes类型(如 b"Hello");指定encoding后可传字符串。
2. communicate()只能调用一次,调用后stdin管道会被关闭
"""
# 启动子进程,指定 stdin=PIPE(可写入)、stdout=PIPE(可读取)
proc = subprocess.Popen(
["grep", "python"], # 筛选包含"python"的行
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
encoding="utf-8"
)
# 手动向子进程stdin写入数据
input_data = "python is cool\njava is good\npython is powerful"
stdout, stderr = proc.communicate(input=input_data, timeout=5)
print("筛选结果:", stdout.strip())
proc = subprocess.Popen(
['cat', '-'],
# 输入和输出设置为管道,进行通信
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
msg = 'through stdin to stdout'.encode('utf-8')
# stdout_value = proc.communicate(input=msg)[0].decode('utf-8')
stdout_value = proc.communicate(msg)[0].decode('utf-8')
print('pass through:', repr(stdout_value))
2.7.5.2、结合上下文管理器(with 语句)优化流式写入
"""
死锁避免:
1. 若子进程会输出大量数据(stdout/stderr缓冲区满),仅写stdin不读输出会导致死锁
2. 推荐优先使用communicate(),或通过线程异步读取stdout/stderr
"""
# 与 bash 交互,执行多条命令
proc = subprocess.Popen(
["bash"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8"
)
# 发送命令
with proc.stdin as stdin:
stdin.write("echo 'first command'\n")
stdin.write("ls -l | grep .py\n")
stdin.close() # 关闭输入,触发子进程执行
# 读取输出
stdout, stderr = proc.communicate()
print("输出: ", stdout)
print("错误: ", stderr)
print("退出码: ", proc.returncode)
2.7.5.3、直接操作 Popen.stdin 文件对象(流式写入)
Popen.stdin是一个可写的文件对象,可通过 write()/writelines() 分块/逐行写入数据,适合大数据量或实时流式交互
"""
1. 缓冲问题:
写入后需调用 flush(),否则数据可能滞留在内核缓冲,子进程无法立即读取。
2. 死锁风险:
若子进程输出较多,仅写 stdin 不读 stdout/stderr 会导致管道满,触发死锁。
解决方式:
(1) 用线程/多进程异步读取 stdout/stderr
(2) 使用communicate()(优先)
3. 关闭stdin:
写完数据后必须关闭proc.stdin,否则子进程会一直等待输入(如 cat 命令会阻塞)
"""
# 启动子进程(开启 stdin 管道)
proc = subprocess.Popen(
["cat"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8"
)
try:
# 流式写入数据(分多次)
proc.stdin.write("第一行数据\n")
proc.stdin.write("第二行数据\n")
proc.stdin.writelines(["第三行\n", "第四行\n"]) # 批量写入列表
# 强制刷新缓冲(避免数据滞留在内核缓冲)
proc.stdin.flush()
# 关闭 stdin(告知子进程数据已写完)
proc.stdin.close()
# 读取输出(需手动读取,避免死锁)
stdout = proc.stdout.read()
stderr = proc.stderr.read()
# 等待子进程结束
proc.wait()
print("子进程输出: ")
print(stdout)
print("错误信息: ", stderr)
except Exception as e:
print("写入失败:", e)
finally:
# 确保资源释放
proc.stdout.close()
proc.stderr.close()
2.7.6、案例: 进程间管道通信
实现类似 cat test.txt | grep banana 的管道效果,通过 stdin 接收前一个子进程的输出
import subprocess
# 第一步:用 Popen 执行 cat(非阻塞,保留管道)
cat_process = subprocess.Popen(
["cat", "text.txt"],
stdout=subprocess.PIPE, # 输出管道(字节流),不转字符串
# 不设置 text=True,避免转为字符串
)
# 第二步:用 Popen 创建 grep 进程,连接 cat 的输出管道
grep_process = subprocess.Popen(
["grep", "python"],
stdin=cat_process.stdout, # 直接连接 cat 的输出管道(文件对象)
stdout=subprocess.PIPE, # 捕获 grep 输出
text=True # 最终输出转字符串
)
# 等待 grep 执行完成,获取输出
grep_output, _ = grep_process.communicate()
# 关闭管道(释放资源)
cat_process.stdout.close()
# 输出结果
print("管道结果:", grep_output)
import subprocess
# 第一个进程:ls -l
p1 = subprocess.Popen(['ls', '-l'], stdout=subprocess.PIPE)
# 第二个进程:grep .txt(输入为p1的输出)
p2 = subprocess.Popen(
['grep', '.txt'],
stdin=p1.stdout,
stdout=subprocess.PIPE,
text=True,
encoding='utf-8'
)
p1.stdout.close() # 关闭p1的stdout,避免阻塞
output, _ = p2.communicate()
print(f'包含.txt的文件:{output}')
import subprocess
# ps aux | grep python | head -5
ps = subprocess.Popen(["ps", "aux"], stdout=subprocess.PIPE)
grep = subprocess.Popen(["grep", "python"], stdin=ps.stdout, stdout=subprocess.PIPE)
head = subprocess.Popen(["head", "-5"], stdin=grep.stdout, stdout=subprocess.PIPE)
ps.stdout.close()
grep.stdout.close()
output = head.communicate()[0].decode()
print(output)
2.7.7、案例: 超时杀死子进程并清理管道
import subprocess
import shlex
def run_command_with_timeout(cmd, timeout=30):
"""运行命令并设置超时"""
try:
proc = subprocess.Popen(shlex.split(cmd),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True)
stdout, stderr = proc.communicate(timeout=timeout)
return proc.returncode, stdout, stderr
except subprocess.TimeoutExpired:
proc.kill() # 强制杀死
print("子进程已杀死,PID: ", proc.pid)
return -1, "", f"Command timed out after {timeout} seconds"
exit_code = proc.wait()
print("退出码:", exit_code) # Unix 下为 -9(SIGKILL)
- 清理僵尸进程:父进程退出前需确保子进程终止
p = subprocess.Popen(...)
try:
p.communicate(timeout=10)
except subprocess.TimeoutExpired:
p.kill() # 杀死进程
p.communicate() # 清理管道,避免僵尸进程
2.7.8、communicate() 清理管道的作用
communicate() 不仅有清理管道的作用,还是其核心设计目标之一 —— 它会彻底清空子进程与父进程之间的 stdin/stdout/stderr 管道,避免数据残留和死锁,是 subprocess 中最安全的管道清理方式
| 步骤 | 操作(管道清理核心) | 作用 |
|---|---|---|
| 1 | 写入并关闭 stdin | 若传入 input,先将数据写入子进程 stdin 管道,随后立即关闭 stdin(告知子进程 “输入已结束”),避免子进程阻塞等待输入 |
| 2 | 清空 stdout/stderr | 持续读取 stdout 和 stderr 管道的所有数据(直到子进程退出、管道关闭),将数据缓存到内存并最终返回;这一步是 “清理管道” 的核心 —— 彻底清空内核中的管道缓冲区,避免数据残留 |
| 3 | 等待 + 释放资源 | 等待子进程退出,然后关闭 stdout/stderr 管道,释放文件描述符,确保无管道资源泄漏 |
# “清理管道” 的核心价值:解决死锁和数据残留
1. 避免管道满导致的死锁(最关键)
子进程的stdout/stderr管道有固定大小的内核缓冲区(通常几KB~几十KB):
若只向stdin写数据,却不读取stdout/stderr,缓冲区会被占满,子进程会因无法继续输出而阻塞
父进程若还在等待子进程响应,就会形成死锁(父等子、子等父)
communicate()强制读取完stdout/stderr的所有数据(清空管道),从根本上避免了这种死锁。
2. 避免管道数据残留
如果不清理管道,内核缓冲区中未读取的残留数据会:
被后续对该子进程的操作(如stdout.read())读取到"脏数据";
占用内核文件描述符,导致资源泄漏。
communicate() 确保管道被完全清空,所有数据要么被返回给父进程,要么被彻底清理
# 关键注意点(清理管道的边界)
1. 仅清理当前子进程的管道: communicate()只处理当前Popen实例关联的 stdin/stdout/stderr,不会影响其他进程的管道
2. 只能调用一次: 调用后stdin/stdout/stderr管道会被关闭,再次调用会抛出 ValueError(管道已关闭)
3. 大数据量的限制: communicate()会将stdout/stderr的所有数据加载到内存,若子进程输出GB级数据,可能导致内存溢出(此时需改用流式读取,手动清理管道)
2.7.8.1、communicate () 自动清理
import subprocess
# 1. 定义子进程命令(Python单行脚本)
child_cmd = [
"python3", "-c",
"""
import sys
# 读取父进程传入的stdin
input_data = sys.stdin.read()
# 向stdout输出(含输入内容)
sys.stdout.write(f"收到输入:{input_data}\\n")
# 向stderr输出测试信息
sys.stderr.write("这是子进程的错误输出\\n")
"""
]
# 2. 启动子进程,开启管道
proc = subprocess.Popen(
child_cmd,
stdin=subprocess.PIPE, # 开启stdin管道
stdout=subprocess.PIPE, # 开启stdout管道
stderr=subprocess.PIPE, # 开启stderr管道
encoding="utf-8" # 统一编码,避免bytes处理
)
# 3. communicate() 一键完成:写入+读取+清理+等待
# input:传入子进程的stdin数据
# 返回值:(stdout全部数据, stderr全部数据)
stdout_res, stderr_res = proc.communicate(input="Hello 子进程!")
# 4. 输出结果(验证管道已清理)
print("=== communicate() 结果 ===")
print("stdout:", stdout_res.strip())
print("stderr:", stderr_res.strip())
print("子进程退出码:", proc.returncode) # 0表示正常退出,无管道阻塞/残留
# 再次读取管道,验证无残留
"""
执行下面会报错:
ValueError: I/O operation on closed file.
communicate() 执行后会自动关闭 stdout/stderr 管道文件对象,
此时调用 proc.stdout.read() / proc.stderr.read() 本质是对 “已关闭的文件” 做 I/O 操作
"""
# print("清理后stdout剩余:", repr(proc.stdout.read())) # 输出 ''
# print("清理后stderr剩余:", repr(proc.stderr.read())) # 输出 ''
2.7.8.2、手动清理管道
手动写 stdin、手动读 stdout/stderr、手动关闭管道
import subprocess
child_cmd = [
"python", "-c",
"""
import sys
input_data = sys.stdin.read()
sys.stdout.write(f"收到输入:{input_data}\\n")
sys.stderr.write("这是子进程的错误输出\\n")
"""
]
# 2. 启动子进程
proc = subprocess.Popen(
child_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8"
)
try:
# ===== 手动写入stdin =====
proc.stdin.write("Hello 子进程!") # 写入数据
proc.stdin.flush() # 强制刷新缓冲(避免数据滞留)
proc.stdin.close() # 关闭stdin,告知子进程输入结束
# ===== 手动读取stdout/stderr(清理管道) =====
# 注意:先读stdout/stderr,再wait(),避免管道满导致死锁
stdout_res = proc.stdout.read() # 读取stdout所有数据(清空管道)
stderr_res = proc.stderr.read() # 读取stderr所有数据(清空管道)
# ===== 手动等待子进程退出 =====
proc.wait()
# ===== 验证清理结果 =====
print("=== 手动清理结果 ===")
print("stdout:", stdout_res.strip())
print("stderr:", stderr_res.strip())
# 再次读取,验证无残留
print("清理后stdout剩余:", repr(proc.stdout.read())) # 输出 ''
print("清理后stderr剩余:", repr(proc.stderr.read())) # 输出 ''
finally:
# ===== 手动释放资源 =====
proc.stdout.close()
proc.stderr.close()
2.7.9、案例: 超时杀死子进程
import signal
import time
p = subprocess.Popen(['sleep', '10']) # 休眠10秒
print(f"进程PID: {p.pid}")
# 5秒后终止进程
time.sleep(5)
p.terminate() # 等价于p.send_signal(signal.SIGTERM)
p.wait() # 等待进程终止
print(f"进程退出码: {p.returncode}") # -15(SIGTERM的退出码)
2.7.10、案例: 使用with进行资源管理
# 正确:使用上下文管理器
with subprocess.Popen(["command"], stdout=subprocess.PIPE) as proc:
output = proc.stdout.read()
# 或确保清理
proc = subprocess.Popen(["sleep", "10"])
try:
# 处理进程
pass
finally:
if proc.poll() is None: # 如果仍在运行
proc.terminate() # 尝试正常终止
proc.wait(timeout=5) # 等待
if proc.poll() is None:
proc.kill() # 强制杀死
2.7.11、Popen执行shell脚本
2.7.11.1、基本执行方式
执行shell脚本文件
file_path = "/opt/zkc/zkc.sh"
# 方法1:直接执行(需要执行权限)
os.chmod(file_path, 0o755)
proc = subprocess.Popen([file_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8')
stdout, stderr = proc.communicate()
# 方法2:通过shell解释器执行
proc = subprocess.Popen(["sh/bash", file_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
执行 Shell 命令字符串
# 使用 shell=True 执行
cmd = "ls -la | grep '*.py' && echo 'Done'"
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True)
2.7.11.2、基本的脚本执行
如果需要带参数,那么执行脚本的时候在Popen方法的args参数中,可以使用脚本和参数组成的列表
import subprocess
import tempfile
import os
# 创建临时脚本
script_content = """#!/usr/bin/env bash
run_role=$(whoami)
echo "【${run_role}】 Starting script..."
ip=$(ifconfig enp0s3| grep -E "^[[:space:]]+inet .*$" | awk '{print $2}')
echo "The host ip is ${ip}"
echo "【${run_role}】 Script Complete!"
"""
# 写入临时文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
f.write(script_content)
script_path = f.name # f.name获取临时文件路径,生成的文件在/tmp目录下 /tmp/tmpb2py14d5.sh
# 给与执行权限
os.chmod(script_path, 0o755)
# 执行脚本
try:
proc = subprocess.Popen(
[script_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
cwd='/tmp'
)
output_result, err_result = proc.communicate(timeout=10)
print("Output: \n%s" %output_result)
if err_result:
print("Error Log: %s" %err_result)
finally:
os.unlink(script_path)
2.7.11.3、带参数的脚本执行
import subprocess
import os
script_path = "/tmp/test_args.sh"
# 脚本内容
script_content = """#!/usr/bin/env bash
echo "Script name: $0"
echo "First Parameter: $1"
echo "Second Parameter: $2"
echo "All Parameters: $@"
echo "Arguments count: $#"
echo "Process id: $$"
"""
# 将脚本内容写入文件
with open(script_path, 'w') as f:
f.write(script_content)
# 修改脚本权限
os.chmod(script_path, mode=0o755)
proc = subprocess.Popen(
['sh', script_path, 'zkc', 19, '男'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
)
output, error = proc.communicate()
print(output)
2.7.11.4、创建自定义环境变量并在脚本中引用
新增自定义的环境变量,并在脚本中引用
bash -c “sh zkc.sh”
bash -c “cat text.txt”
import subprocess
import os
# 方法一:通过 env 参数
custom_env = {
**os.environ, # 继承当前环境
"CUSTOM_VALUE": "my_value",
"JAVA_HOME": "/opt/jdk/jdk8-15u",
"PATH": f"/custom/bin:{os.environ.get('PATH', '')}" # 在PATh变量中添加新的bin路径
}
script = """#!/bin/bash
echo "Custom var: ${CUSTOM_VALUE}"
echo "Java mode: ${JAVA_HOME}"
echo "PATH: ${PATH}"
"""
proc = subprocess.Popen(
["bash", "-c", script],
env=custom_env,
stdout=subprocess.PIPE,
text=True
)
print(proc.communicate()[0])
# 方法2:在命令中设置(临时)
script = 'CUSTOM_TEMP="temp_value" && echo $CUSTOM_TEMP'
proc = subprocess.Popen(script, shell=True, stdout=subprocess.PIPE, text=True)
print(proc.communicate()[0])
2.7.11.5、使用 Shell 特性(管道、重定向、变量等)
#!/usr/bin/env python3
import subprocess
complex_cmd = """#!/usr/bin/env bash
COUNT=5
TARGET_DIR="/tmp"
if [ ! -d $TARGET_DIR ]; then
echo "Directory $TARGET_DIR does not exist."
exit 1
fi
echo "Processing ${COUNT} files in ${TARGET_DIR}"
# 使用管道,对head命令的输出进行逐行读取,每次读取一行到变量file中
ls -l $TARGET_DIR | awk 'NR>1{print $p}' | head -n $COUNT | while read file; do
echo "File: $file"
# 判断文件类型
if [[ $file == *.txt ]]; then
echo " -> Text file"
else
echo " -> Other file"
fi
done
# 重定向输出到变量
file_count=$(ls $TARGET_DIR | wc -l)
echo "Total files: $file_count"
"""
proc = subprocess.Popen(
complex_cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
executable="/bin/bash",
text=True,
)
output, error = proc.communicate()
print("Output:\n", output)
if error:
print("Errors:\n", error)
2.7.11.6、实时监控脚本并读取输出
#!/usr/bin/env python3
import subprocess
import time
from threading import Thread
script = """#!/bin/bash
for i in {1..10}; do
echo "Interation: $i"
sleep 1
if [ $i -eq 5 ]; then
echo "Error: Something Happened ${i}" >&2 # 将错误日志重定向到标准错误流
fi
done
echo "Script Finished"
"""
def monitor_output(proc, stream_type):
"""
实时监控输出
:param proc: 子程序对象
:param stream_type: 子程序输出类型
"""
stream = proc.stdout if stream_type == "stdout" else proc.stderr
for line in iter(stream.readline, ""):
if line:
print(f"[{stream_type.upper()}] {line.strip()}]")
def main():
proc = subprocess.Popen(
["bash", "-c", script],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
# 创建线程
stdout_proc = Thread(target=monitor_output, args=(proc, "stdout"))
stderr_proc = Thread(target=monitor_output, args=(proc, "stderr"))
stdout_proc.daemon = True
stderr_proc.daemon = True
stdout_proc.start()
stderr_proc.start()
stdout_proc.join()
stderr_proc.join()
while proc.poll() is None:
time.sleep(0.1)
print(f"\nProcess finished with return code: {proc.returncode}")
if __name__ == '__main__':
main()
2.7.11.7、交互式脚本输入输出
适用于需要实时处理子进程输出的场景,如日志监控或长时间运行的任务,能有效避免缓冲区溢出和主线程挂起
#!/usr/bin/env python3
import subprocess
script = """#!/bin/bash
echo "what's your name?"
read -p "Please input your name: " name
echo "Hello, $name!"
echo "How old are you?"
read -p "Please input your age: " age
if [ $age -lt 18 ]; then
echo "Your are a minor."
else
echo "Your are a major."
fi
"""
proc = subprocess.Popen(
["/bin/bash", "-c", script],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# 读取第一个提示
first_prompt = proc.stdout.readline()
print(f"Script: {first_prompt.strip()}")
# 响应第一个提示
name = input("Please enter your name: ")
proc.stdin.write(f"{name}\n")
proc.stdin.flush()
print(f"You: {name}")
# 读取响应
response_and_second_prompt = proc.stdout.readline() # Hello消息
print(f"Script: {response_and_second_prompt.strip()}")
# 读取第二个提示
second_prompt = proc.stdout.readline() # 年龄提示
print(f"Script: {second_prompt.strip()}")
# 响应第二个提示
age = input("Please enter your age: ")
proc.stdin.write(f"{age}\n")
proc.stdin.flush()
print(f"You: {age}")
# 获取最终输出
final_output = proc.stdout.readline()
print(f"Script: {final_output.strip()}")
# 关闭管道
proc.stdin.close()
# 等待进程结束
stdout, stderr = proc.communicate()
# 打印任何剩余输出
if stdout:
for line in stdout.strip().split('\n'):
if line:
print(f"Script: {line}")
if stderr:
print(f"Script Error: {stderr.strip()}")
下面这种写法,只输出到 Script: Hello, zkc!,然后就阻塞在这句输出,未找到问题原因…
#!/usr/bin/env python3
import subprocess
import time
import select
script = """#!/bin/bash
echo "what's your name?"
read -p "please input real name: " name
echo "Hello, $name!"
echo "what's your age?"
read -p "please input real age: " age
if [ $age -lt 18 ]; then
echo "Your are a minor."
else
echo "Your are a major."
fi
"""
proc = subprocess.Popen(
["/bin/bash", "-c", script],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
output_lines = []
while True:
"""
实现非阻塞读取子进程输出的核心代码,它检查子进程的标准输出(stdout)和标准错误(stderr)流是否可读,避免程序因等待 I/O 而阻塞
select.select 的第一个参数是等待可读的文件描述符列表(此处为 proc.stdout 和 proc.stderr),
第二个参数是等待可写的文件描述符列表(空列表 [] 表示不关注写入),
第三个参数是等待异常条件的文件描述符列表(空列表 [] 表示忽略异常),
第四个参数是超时时间(0.1 秒),表示最多等待 0.1 秒后返回。
如果流中有数据可读,select 返回包含可读流的列表赋值给 ready_to_read,否则返回空列表
"""
ready_to_read, _, _ = select.select([proc.stdout, proc.stderr], [], [], 0.1)
for stream in ready_to_read:
line = stream.readline()
if line:
output_lines.append(line.strip())
print(f"Script: {line.strip()}")
if "name?" in line.lower():
input_name = input("Please enter your name: ")
proc.stdin.write(f"{input_name}\n")
proc.stdin.flush()
print(f"You: {input_name.strip()}")
time.sleep(0.05)
elif "age?" in line.lower() or "please input real age" in line.lower():
input_age = input("Please enter your age: ")
proc.stdin.write(f"{input_age}\n")
proc.stdin.flush()
print(f"You: {input_age.strip()}")
if proc.poll() is not None:
# 读取剩余的标准输出和标准错误(替代communicate(),无阻塞冗余)
remaining_stdout = proc.stdout.read()
remaining_stderr = proc.stderr.read()
# 打印剩余输出
if remaining_stdout:
for leftover_line in remaining_stdout.strip().split('\n'):
if leftover_line:
print(f"Script: {leftover_line}")
# 打印错误信息(如有)
if remaining_stderr:
print(f"Script Error: {remaining_stderr.strip()}")
break
# 规范关闭管道,释放资源
proc.stdin.close()
proc.stdout.close()
proc.stderr.close()
2.7.11.8、创建脚本执行器类,处理错误和超时控制
#!/usr/bin/env python3
import os
import signal
import subprocess
class ShellScriptExecutor:
def __init__(self, timeout):
self.timeout = timeout
self.process = None
def execute_script(self, script_content, shell="/bin/bash"):
try:
self.process = subprocess.Popen(
[shell, '-c', script_content],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
preexec_fn=os.setsid # 创建新的进程组,便于终止整个进程树
)
try:
output, error = self.process.communicate(timeout=self.timeout)
return {
"return_code": self.process.returncode,
"stdout": output.strip(),
"stderr": error.strip(),
"timeout": False
}
except subprocess.TimeoutExpired:
self._terminate_process()
return {
"return_code": -1,
"stdout": "",
"stderr": f"Script timed out after {self.timeout} seconds",
"timeout": True
}
except FileNotFoundError:
return {"error": f"shell not found: {shell}"}
except Exception as e:
return {"error": f"Unexpected error: {e}"}
def _terminate_process(self):
"""终止进程及其子进程"""
if self.process:
try:
# 发送信号给整个进程组
os.killpg(os.getpid(self.process.pid), signal.SIGTERM)
self.process.wait(timeout=5)
except:
try:
# 如果SIGTERM无效,使用SIGKILL
os.killpg(os.getpid(self.process.pid), signal.SIGKILL)
except:
pass
# 创建执行器对象
executor = ShellScriptExecutor(timeout=10)
# 正常脚本
script = """
echo "Start"
sleep 2
echo "End"
"""
result = executor.execute_script(script)
print(f"Result: {result}")
# 超时脚本
script = """
echo "Start long process"
sleep 20 # 超过超时时间
echo "This won't be reached"
"""
result = executor.execute_script(script)
print(f"Result: {result}")
# 错误脚本
script = """
echo "Start"
invalid_command # 不存在的命令
echo "This won't be reached"
"""
result = executor.execute_script(script)
print(f"Result: {result}")
2.7.11.9、完整的 Shell 脚本执行器
在这里插入代码片
2.7.11.10、脚本模板引擎和参数替换
#!/usr/bin/env python3
import string
import subprocess
class ScriptTemplate:
def __init__(self, template):
self.template = template
self.params = {}
def set_param(self, key, value):
self.params[key] = value
def render(self):
"""
使用string.template渲染模板:
string.Template类用于创建可替换的字符串模板,其中占位符(如$var)会被实际值替换。safe_substitute()方法执行替换时,
如果某些占位符在提供的数据中没有对应值,它会保留原占位符不变,而不是抛出错误
假设模板字符串为"Hello, $name! Today is $day.",调用safe_substitute()并传入{'name': 'Alice'}时,
会返回"Hello, Alice! Today is $day.",即未匹配的$day保持原样
"""
template = string.Template(self.template)
return template.safe_substitute(self.params)
def execute(self, **kwargs):
"""执行渲染后的脚本"""
for key, value in kwargs.items():
self.set_param(key, value)
script = self.render()
proc = subprocess.Popen(
["bash", "-c", script],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
return proc.communicate()
# 使用模板,模板中的变量会在执行时被替换
template = """
#!/bin/bash
# Backup script
BACKUP_DIR="${backup_dir}"
SOURCE_DIR="${source_dir}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_${TIMESTAMP}.tar.gz"
echo "Backing up ${SOURCE_DIR} to ${BACKUP_DIR}/${BACKUP_NAME}"
tar -czf "${BACKUP_DIR}/${BACKUP_NAME}" "${SOURCE_DIR}" 2>/dev/null
if [ $? -eq 0 ]; then
echo "Backup successful: ${BACKUP_NAME}"
echo "Size: $(du -h "${BACKUP_DIR}/${BACKUP_NAME}" | cut -f1)"
else
echo "Backup failed!"
exit 1
fi
"""
script_tmpl = ScriptTemplate(template)
stdout, stderr = script_tmpl.execute(
backup_dir="/tmp/backups",
source_dir="/opt/zkc"
)
print(stdout)
6万+

被折叠的 条评论
为什么被折叠?



