python subprocess- 更优雅的创建子进程
本文首发于jeffery0207 csdn
简介
如PEP324所言,在任何编程语言中,启动进程都是非常常见的任务,python 也是如此,而不正确的启动进程方式会给程序带来很大安全风险。Subprocess 模块开发之前,标准库已有大量用于进程创建的接口函数(如os.system
、os.spawn*
),但是略显混乱使开发者难以抉择,因此 Subprocess 的目的是打造一个“统一”模块来提供之前进程创建相关函数的功能实现。与之前的相关接口相比,提供了以下增强功能:
一个“统一”的模块来提供以前进程创建相关函数的所有功能;
跨进程异常优化:子进程中的异常会在父进程再次抛出,以便检测子进程执行情况;
提供用于在
fork
和exec
之间执行自定义代码的钩子;没有隐式调用
/bin/sh
,这意味着不需要对危险的 shell meta characters 进行转义;支持文件描述符重定向的所有组合;
使用 subprocess 模块,可以控制在执行新程序之前是否应关闭所有打开的文件描述符;
支持连接多个子进程 ;
支持 universal newline;
支持
communication()
方法,它使发送 stdin 数据以及读取 stdout 和 stderr 数据变得容易,而没有死锁的风险;
subprocess 基础
subprocess.run
subprocess
推荐使用run
函数来处理它所能够处理的一切 cases, 如果需要更高级灵活的定制化使用,则可以使用其底层的popen
接口来实现。run
函数 signature 为:
上面写的参数只是最常见的参数,完整的函数列表在很大程度上与popen
函数的参数相同,即这个函数的大多数参数都被传递到该接口。(timeout
、input
、check
和capture_output
除外)。该函数常用参数如下:
*
args
*, 必选参数,数据类型应为一个 string 或则 一个 sequence(list, tuple 等等)。通常最好传递一个 sequence,因为它允许模块处理任何必需的参数转义和引用; 如果传递的是字符串,则 shell 必须为True
,否则该字符串必须简单地为要执行的程序的名字,而不能指定任何参数。
在复杂情况下,构建一个 sequence-like 的参数可以借助shlex.split()
来实现
shell 模式执行等同于:Popen(['/bin/sh', '-c', args[0], args[1], ...])
当使用shell=True
时,要注意可能潜在的安全问题,需要确保所有空格和元字符都被适当地引用,以避免 shell 注入漏洞。如下面的例子:
capture_output
, 如果capture_output=True
,则将捕获 stdout 和 stderr,调用时内部的 Popen 对象将自动使用stdout=PIPE
和stderr = PIPE
创建标准输出和标准错误对象;传递stdout
和stderr
参数时不能同时传递capture_output
参数。如果希望捕获并将两个 stream 合并为一个,使用stdout=PIPE
和stderr = STDOUT
。
check
,如果check=True
,并且进程以非零退出代码退出,则将抛出CalledProcessError
异常。
input
,该参数传递给Popen.communicate()
,然后传递给子进程的 stdin。该参数数据类型应为字节序列(bytes);但如果指定了encoding
,errors
参数或则text=True
,参数则必须为字符串。使用该参数时,内部 Popen 对象,将使用 stdin = PIPE 自动创建该对象,不能同时使用 stdin 参数。
timeout
,该参数传递给Popen.communicate()
,如果指定时间之后子进程仍未结束,子进程将被 kill,并抛出TimeoutExpired
异常。
stdin
,stdout
和stderr
分别指定执行程序的标准输入,标准输出和标准错误文件的 file handles。如subprocess.PIPE
,subprocess.DEVNULL
, 或者None
。此外,stderr 可以设定为subprocess.STDOUT
,这表示来自子进程的 stderr 数据应重定向到与 stdout 相同的 file handle 中。默认情况下,stdin
,stdout
和stderr
对应的 file handle 都是以 binary 的方式打开。
encoding
,errors
,text
。当传递encoding
,errors
参数 或text=True
时,stdin
,stdout
和stderr
对应的 file handle 以 text 的模式打开。universal_newlines
和text
同义,为了保持向下兼容而保留。默认情况下,文件对象以二进制的方式打开。
env
,通过传递 mappings 对象,给子进程提供环境变量,该参数直接传递给Popen
函数。
shell
, 如果shell=True
,则将通过 Shell 执行指定的命令。当使用shell=True
时,shlex.quote()
函数可用于正确地转义字符串中的空格和 Shell 元字符。
函数返回数据类型为
subprocess.CompletedProcess
, 该对象包含以下属性或方法:
* args
, 调用该进程的参数,同subprocess.run(args,***)
中的args
;
* returncode
,当值为 0 时,代表子进程执行成功;负值 -N
指示进程被 signal N
所终止 (POSIX only); None
代表未终止;
* stdout
,stderr
,代表子进程的标准输出和标准错误;
* check_returncode()
, check 子进程是否执行成功,若执行失败将抛出异常;
old high level interfaces
run
函数在 Python 3.5 新增,之前使用该模块的 high level interface 包括三个函数: call()
, check_call()
, check_output()
。这三个函数参数和subprocess.run()
的函数参数含义相同。但需要注意的是,这三个函数的参数列表略微不同,函数 signature 如下:
subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)
执行args
参数所指定的程序并等待其完成。当shell=True
, 无论子进程执行成功与否,返回值为 return code;当shell=False
,子进程如果执行失败,将会抛出异常;<u>该函数旨在对os.system()
进行功能增强,同时易于使用</u>。
subprocess.check_call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)
执行args
参数所指定的程序并等待其完成,如果子进程返回 0,则函数返回;若子进程失败,则抛出异常;
subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, cwd=None, encoding=None, errors=None, universal_newlines=None, timeout=None, text=None, **other_popen_kwargs)
执行args
参数所指定的程序并返回其输出,如果子进程执行失败将抛出异常; 该函数的返回值默认为bytes
注意: 请勿在
subprocess.call
及`subprocess.check_call
中使用stdout=PIPE
或stderr=PIPE
。如果子进程输出信息过大将会耗尽 OS 管道缓冲区的缓冲,该子进程将阻塞; 要禁止这两个函数的 stdout 或 stderr,可以通过subprocess.DEVNULL
设置。
subprocess.Popen
Popen 构造函数
上面四个 high level interfaces 底层的进程创建及进程管理实际上都是基于subprocess.Popen
类来实现,当需要定制化更灵活的进程调用时,这个函数会是一个更好的选择。首先看该类的构造函数如下:
此时,你会发现很多熟悉的参数。因为 high level function 本身大部分参数也确实传递给了Popen
类。该类的作用即是创建 (fork) 并执行 (exec) 子进程。在 POSIX,该类使用类似os.execvp()
的方式执行子进程;在 windows 上,则使用 windows 系统的CreateProcess()
函数执行子进程。Popen
构造函数参数十分丰富,除了上面介绍的还有一大堆参数需要注意。close_fds
, pass_fds
与 file handle 相关;restore_signals
与 POSIX 信号相关;startnewsession
, startupinfo
, creationflags
则和子进程的创建相关;另外,以下参数可能需要特别关注:
bufsize
, 在创建 stdin / stdout / stderr 管道文件对象时,bufsize
将作为open()
函数的相应参数
* 0 代表 unbuffered
* 1 代表 line buffered
* 其他正数代表 buffer size
* 负数代表使用系统默认的 buffer size (io.DEFAULT_BUFFER_SIZE
)
excutable
,这个参数不常见,当shell=True
时, 在 POSIX 上,可使用该参数指定不同于默认的/bin/sh
shell 来执行子进程;而当shell=False
时,这个用法似乎不太常见,我能想到的一个例子可能如下:
> From 官方文档: executable replaces the program to execute specified by args. However, the original args is still passed to the program. Most programs treat the program specified by args as the command name, which can then be different from the program actually executed.
preexec_fn
, 该参数可绑定一个 callable 对象,该对象将在子进程执行之前在子进程中调用。需要注意的是,在应用程序中存在线程的情况下,该参数应该避免使用,可能会引发死锁。
cwd
, 指定该参数时,函数在执行子进程之前将会将工作目录设置为cwd
。
Popen 方法与属性
Popen.poll()
check 子进程是否已终止,如果结束则返回 return code,反之返回 None
Popen.wait(timeout=None)
等待子进程终止,如果 timeout 时间内子进程不结束,则会抛出TimeoutExpired
异常
> 当使用stdout=PIPE
或则 stderr=PIPE
时,避免使用该函数,使用`Popen.communicate()
以避免死锁的发生。
Popen.communicate(input=None, timeout=None)
与进程交互:将input
指定数据发送到 stdin;从 stdout 和 stderr 读取数据,直到到达文件末尾,等待进程终止。所以,返回值是一个 tuple: (stdout_data, stderr_data)
。如果 timeout 时间内子进程不结束,则会抛出TimeoutExpired
异常。其中需要注意的是,捕获异常之后,可以再次调用该函数,因为子进程并没有被 kill。因此,如果超时结束程序的话,需要现正确 kill 子进程:
Popen.send_signal(signal)
向子进程发送信号
Popen.terminate()
停止子进程,在 POSIX 上,实际上即是向子进程发送SIGTERM
信号;在 windows 上则是调用TerminateProcess()
函数
Popen.kill()
杀掉子进程,在 POSIX 上,实际上即是向子进程发送SIGKILL
信号;在 windows 上则是调用terminate()
函数
属性包括
.args
:子进程命令;.returncode
:子进程终止返回值;.pid
:子进程进程号;.stdin
,.stdout
,.stderr
分别代表标准输入输出,标准错误,默认为 bytes,这几个属性类似于open()
函数返回值,是一个可读的 stream 对象
异常处理
subprocess
模块共包含三个异常处理类: 基类SubprocessError
, 及其两个子类TimeoutExpired
,CalledProcessError
,前者在在等待子进程超时时抛出;后者在调用check_call()
或check_output()
返回非零状态值时抛出。他们共同的属性包括:
cmd
, 该子进程的命令output
, 子进程所 capture 的标准输出 (如调用run()
或则check_output()
),否则为None
stdout
,output
的别名stderr
, 子进程所 capture 的标准错误 (如调用run()
) ,否则为None
TimeoutExpired
还包括timeout
,指示所设置的timeout
的值;CalledProcessError
则还包括属性returncode
;
Subprocess 应用
在官方文档中给出很多例子指导我们如何使用 subprocess 替代旧的接口,具体例子如下:
shell 命令行, 比如要实现一个简单的 shell command line 命令
ls -lhrt
,可以有以下几种等价的方式:
1. shell 管道操作,比如想看一个文件前 100 行中哪些数据包含关键字'python',shell cmd 可以这样写:cat test.txt | head -n 100 | grep python
,使用 subprocess 可以这样写:
替代
os.system()
,前面有提到subprocess.call()
是为os.system
设置的增强版,应用如下:
替代
os.spawn*()
,该家族包括八个变体,os.spawnl()
,os.spawnle()
,os.spawnlp()
,os.spawnlpe()
,os.spawnv()
,os.spawnve()
,os.spawnvp()
,os.spawnvpe()
,l
和v
变体分别代表 fixed parameters 和 variable parameters,p
变体函数默认使用环境变量$PATH
寻找 program file (如ls
,cp
),e
变体则是函数增加一个env
mappings 参数来指定子进程执行的环境变量,不使用当前进程的环境变量,具体见官方文档 os.spawn*。官方建议这些函数都可用 subprocess 替代,如常见的两个场景如下:
替代
os.popen*()
,该系列一共包括 4 个变体,分别是os.popen()
,os.popen2()
,os.popen3()
,os.popen()4
,首先需要理解的是os.popen()
是基于subprocess.Popen
实现的一个方法,用于从一个命令打开一个管道,存在r
或w
两种模式。比如:
而剩下三个变体其实不是基于 subprocess 来实现的,并且功能差别仅仅在于返回值,三者返回值依次是:(child_stdin, child_stdout)
, (child_stdin, child_stdout, child_stderr)
, (child_stdin, child_stdoutandstderr)
, 因此,我们自然也可以使用subprocess
模块函数来替代它:
其他
subprocess
中还提供另外两个 python2.x 中 commands
模块中的旧版 shell 调用功能getstatusoutput
和getoutput
,查看 python 源码可以看到它的实现其实也非常简单,就是借助subprocess.check_output()
函数捕获 shell 命令的输出,最终返回return_code
以及output
:
写在篇尾
subprocess
是基于 python2 中popen2
模块发展而来,专门为替代 python 中众多繁杂的子进程创建方法而设计,平时使用的过程中,subprocess.run()
以及subprocess.call
可以满足我们大多数的使用需求,但是更深入的了解该 package 的设计思想可以让我们更加灵活的控制复杂场景下的子进程任务。
参考
版权声明: 本文为 InfoQ 作者【jeffery】的原创文章。
原文链接:【http://xie.infoq.cn/article/1424678f656909d49815a7cab】。文章转载请联系作者。
评论