一、subprocess
在早期的 Python 版本中,我们主要是通过 os.system()、os.popen()、os.spawn() 等函数来执行命令行指令的,另外还有一个很少使用的 commands 模块。从 Python 2.4 开始, Python 引入 subprocess 模块来管理子进程,以取代一些旧模块的方法。 subprocess 不但可以调用外部的命令作为子进程,而且可以连接到子进程的 input/output/error 管道,获取相关的返回信息。
运行 Python 的时候,我们都是在创建并运行一个进程,像 Linux 进程那样,一个进程可以 fork 一个子进程,并让这个子进程 exec 另外一个程序。在 Python 中,我们通过标准库中的 subprocess 模块来 fork 一个子进程,并运行一个外部的程序。
subprocess 模块中定义有多个创建子进程的函数,这些函数分别以不同的方式创建子进程,所以我们可以根据需要来从中选取一个使用。另外 subprocess 还提供了一些管理标准流(standard stream)和管道(pipe)的工具,从而在进程间使用文本通信。
二、subprocess.run()
先来看看 subprocess.run() 方法,这也是使用较多的方法。在 Python 3.5 版本中添加,官方文档中提倡通过 subprocess.run() 方法替代其他函数来使用 subproccess 模块的功能。如果需要更高级的用例,可以直接使用基础 Popen 接口。
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)
运行 args 描述的命令,等待命令完成,然后返回 “CompletedProcess” 实例。
上面显示的参数只是最常见的参数,下面在常用参数中进行了描述。
- capture_output
如果 capture_output 为 true,则将捕获 stdout 和 stderr。将使用 stdout = PIPE 和 stderr = PIPE 自动创建内部 Popen 对象。stdout 和 stderr 参数也可能无法使用。
- timeout
很多脚本运行时会卡住,导致调用脚本一直等待,这很显然不是我们想看到的,因此执行命令的超时 Timeout 设置很有必要。好在 subprocess.run() 提供了 timeout 参数,当我们设置 timeout=30 时,表示如果在30秒内无法执行完毕,会抛出异常。
我们也可以使用 try、except 捕捉这个异常:
1 2 3 4 5 |
try: res = subprocess.run('/bin/bash /tmp/for.sh',stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, timeout=30) print('code: ',res.returncode,'stdout: ',res.stdout) except subprocess.TimeoutExpired as e: print(e) |
其超时参数传递给Popen.communicate()
。如果超时,子进程将被杀死并等待。子进程终止后,将重新引发 “TimeoutExpired” 异常。
- input
输入参数传递给 Popen.communicate()
,从而传递给子进程的 stdin。如果使用则必须是 bytes 序列,如果指定了编码或错误或文本为真, 则为字符串。使用时,将使用 stdin = PIPE 自动创建内部 Popen 对象,并且也可能无法使用 stdin 参数。
- check
如果 check 为 true,并且进程退出时具有非0退出码,则将引发CalledProcessError
异常。该异常的属性包含参数、退出码及 stdout 和 stderr,如果能捕获到。
- encoding、error、text
如果指定了 encoding 或 error 或 text=True,则使用指定的 encoding 和 error 或 io.TextIOWrapper
以文本模式打开 stdin、stdout 和 stderr 的文件对象。默认值 universal_newlines 参数等效于文本,是为向后兼容而提供的。默认情况下,文件对象以二进制模式打开。
示例:
1 2 3 4 5 6 7 8 9 10 11 |
>>> subprocess.run(["ls", "-l"]) # doesn't capture output CompletedProcess(args=['ls', '-l'], returncode=0) >>> subprocess.run("exit 1", shell=True, check=True) # CallProcessError Traceback (most recent call last): ... subprocess.CalledProcessError: Command 'exit 1' returned non-zero exit status 1 >>> subprocess.run(["ls", "-l", "/dev/null"], capture_output=True) # capture output CompletedProcess(args=['ls', '-l', '/dev/null'], returncode=0, stdout=b'crw-rw-rw- 1 root root 1, 3 Jan 23 16:23 /dev/null\n', stderr=b'') |
一种通用方式,把 subprocess.PIPE 赋值给 stdout 和 stderr 属性:
1 |
res = subprocess.run(["ls", "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, timeout=30) |
subprocess.run() 返回了一个 res 对象,其中包含了命令运行参数,运行返回状态码和输出内容。如下格式:
1 |
CompletedProcess(args='ls', returncode=0, stdout=b'aa.py\nanaconda-ks.cfg\nbb.py\ndebug.log\noriginal-ks.cfg\n', stderr=b'') |
所以我们可以很轻松可以获取到相关属性信息,比如通过 res.args 属性获取执行的命令和参数,通过 res.returncode 属性获取命令执行结果状态码(当命令执行完成后才可以获取到状态码,其中0表示命令执行成功,1-255都表示命令执行失败),通过 res.stdout 属性获取标准输出信息,通过 res.stderr 属性获取错误输出信息等。这些属性的重要性不言而喻。
我们可以看到,上面输出的结果是 b’xxxxx’,这是一个 bytes 类型的数据。实际使用中需要将其转换为字符串格式。
1 2 3 4 5 |
# Linux使用自动,或者uft8 print(res.stdout.decode()) # Windows下字符编码是GB2312 print(res.stdout.decode('GB2312')) |
既然转换成字符串了,那么自然就可以使用字符串相关的方法了。或者直接带上 universal_newlines=True ,这样输出就直接转成字符串了。
另外也可以直接把标准输出实时写入到文件中,如下代码:
1 2 3 |
stdout = open('/tmp/subprocess_stdout', 'wb') stderr = open('/tmp/subprocess_stderr', 'wb') popen = subprocess.run(['ping','www.baidu.com'], stdout=stdout.fileno(), stderr=stderr.fileno()) |
同样的方法可以用在 Popen 类上。
- shell
shell 默认为 False。在 Linux 下,shell=False 时,如果 args 是字符串,那么只能是命令,不能包含任何参数,否则报错;如果 args 是一个列表 list ,则 args 的第一项是定义程序命令字符串,其它项是调用系统 Shell 时的附加参数。
shell=True 时,如果 args 是字符串,Popen 对象是直接调用系统的 shell 来执行,字符串格式和 shell 终端书写格式一样;如果 args 是一个列表 list,则 args 的第一项是定义程序命令字符串,其它项是调用系统 shell 时的附加参数。官方推荐 shell=True 时,使用字符串方式传递参数。
如果想使用 shell 中的管道,重定向,文件通配符,环境变量等功能,例如 ”ifconfig | grep eth0 > mm”,那么只能使用 shell=True,并且使用字符串来传递。
综上, shell=True 功能最强大的,但因为强大也存在安全风险,需要谨慎的对待传递的参数。特别是当参数来自于用户输入时。 这时候可以使用 shlex.quote()
函数来将参数正确的用双引用引起来。
New in version 3.5.
Changed in version 3.6: Added encoding and errors parameters
Changed in version 3.7: Added the text parameter, as a more understandable alias of universal_newlines. Added the capture_output parameter.
三、subprocess.CompletedProcess
从 run() 执行完成后返回的对象,表示已完成的进程。
return CompletedProcess(process.args, retcode, stdout, stderr)
args
用于启动进程的参数,这可能是列表或字符串。
returncode
子进程的退出状态。通常,退出状态为0表示它已成功运行。负值 -n 表示子进程已被信号 n 终止 (仅限posix)。
stdout
从子进程中捕获的 stdout ,bytes 序列,或字符串。如果 run() 调用 encoding、errors or text = True。如果没有捕获 stdout,则为 ”None“。
如果你使用 stderr=subprocess.STDOUT 运行该进程。stdout、stderr 将在此属性中组合,stderr 将为 “None”。
stderr
从子进程中捕获的 stderr,bytes 序列,或字符串。如果 run() 调用 encoding、errors or text = True。如果没有捕获 stderr,则为 ”None“。
check_returncode()
如果返回代码为非0,则引发 calledtprocess 异常。
四、subprocess.Popen
实际上,上面的几个函数都是基于 Popen 对象的封装(wrapper)。这些封装的目的在于让我们容易使用子进程。当我们想要更个性化我们的需求的时候,就要转向 Popen 类,该类生成的对象用来代表子进程。与上面的封装不同,Popen 对象创建后,主程序不会自动等待子进程完成。我们必须调用对象的 wait() 方法,父进程才会等待(也就是阻塞)。
示例:
1 |
popen = subprocess.Popen(['ping','www.baidu.com'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
然后我们如果想重定向标准输出,需要使用while来循环判断 poll() 方法返回值,如下:
1 2 3 |
while popen.poll() is None: out = popen.stdout.readline().decode() sys.stdout.write(out) |
另外也可定义输出方式为文件等。
1 2 3 4 |
while res.poll() is None: out = res.stdout.readline().decode() with open('/tmp/all.log', 'a+') as f: f.write(out) |
如果想重定向错误输出,同样也是判断 poll() 方法返回值,如下:
1 2 3 |
if popen.poll() != 0: err = popen.stderr.read().decode() sys.stdout.write(err) |
其中 poll 方法只有 Popen 对象才有(由于 run 方法是同步的所以不需要 poll 方法),其返回值不为 None 表示命令执行完成了,而返回值不为0表示命令执行失败,poll 方法内部返回的也是 popen.returncode 值,在 Linux 中通过 $? 也是可以获取命令执行成功与否的状态码,0表示执行成功,其他状态码均表示执行失败。
注意点:
run() 方法执行命令时必须等待执行完成才可以得到执行结果,而 Popen 对象类似异步执行,父进程无须等待子进程执行完成后才返回结果,父进程执行完就可以去通过 while 获取执行结果。他们的区别决定你使用该如何正确使用。
另外,run() 方法返回的标准输出是 bytes 类型,直接可以 decode。而 Popen 返回的是 _io.BufferedReader 对象,是一个流,可以使用 read 或者 readlines 方法解析,关于 IO 可以看看官方说明。
Popen类的实例具有以下方法:
Popen.poll()
检查子进程是否已终止。设置并返回 returncode 属性。否则,返回None。
Popen.wait(timeout=None)
等待子进程终止。设置并返回 returncode 属性。
如果进程在超时时间后没有终止,则引发 TimeoutExpired 异常。捕获此异常并重试等待则是安全的。
Popen.communicate(input=None, timeout=None)
与进程交互:将数据发送到 stdin,从 stdout 和 stderr 读取数据,直到达到文件结尾。等待进程终止。可选的 input 参数应该是要发送到子进程的数据,如果没有数据发送给子进程,则应该是None。如果在文本模式下打开流,则输入必须是字符串。否则,它必须是字节。
communicate() 返回一个元组(stdout_data,stderr_data)。如果在文本模式下打开流,则数据将是字符串;否则将是字节。
另外请注意,如果要将数据发送到进程的 stdin,则需要使用 stdin=PIPE 创建 Popen 对象。同样,要在结果元组中获取除 None 之外的任何内容,你还需要提供 stdout=PIPE 和 stderr=PIPE。
如果进程在超时后没有终止,则会引发 TimeoutExpired 异常。捕获此异常并重试通信则不会丢失任何输出。
如果超时到期,则子进程不会被终止,因此为了正确清理,行为良好的应用程序应该主动终止子进程并完成通信:
1 2 3 4 5 6 |
proc = subprocess.Popen(...) try: outs, errs = proc.communicate(timeout=15) except TimeoutExpired: proc.kill() outs, errs = proc.communicate() |
注意,读取的数据缓冲在内存中,因此如果数据很大或不受限制,请不要使用此方法。
Popen.send_signal(signal)
发送信号给子进程。
Popen.terminate()
停止子进程,在Posix操作系统上,该方法将 SIGTERM 信号发送给子进程。在Windows上,调用Win32 API函数 TerminateProcess() 来停止子进程。
Popen.kill()
杀死子进程,在Posix操作系统上,该函数将 SIGKILL 信号发送给子进程。在Windows上,kill() 是 terminate() 的别名。
Popen.args
返回被执行的命令和选项,是一个列表或字符串。
Popen.stdin
如果 stdin 参数是 PIPE ,则此属性是 open() 方法返回的可写流对象。如果指定了 encoding 或 errors 参数或者 universal_newlines=True,则流是文本流,否则是字节流。如果 stdin 参数不是 PIPE ,则此属性为 None。
Popen.stdout
如果 stdout 参数是 PIPE,则此属性是 open() 方法返回的可读流对象。从流中读取子进程提供的标准输出。如果指定了 encoding 或 errors 参数或者 universal_newlines=True ,则流是文本流,否则是字节流。如果 stdout 参数不是 PIPE,则此属性为 None。
Popen.stderr
如果 stderr 参数是 PIPE,则此属性是 open() 方法返回的可读流对象。从流中读取子进程提供的错误输出。如果指定了 encoding 或 errors 参数或者 universal_newlines=True,则流是文本流,否则它是字节流。如果 stderr 参数不是 PIPE,则此属性为 None。
Popen.pid
子进程的进程ID。请注意,如果将 shell 参数设置为 True,则这是生成的 shell 的进程ID。
Popen.returncode
子进程返回码,由 poll() 和 wait() 设置(间接通过communic())。 “None”值表示该进程尚未终止。
四、subprocess.PIPE
可以用作 Popen 的 stdin,stdout or stderr 参数的特殊值,表示打开标准流的管道。最适用于 Popen.communicate() 。
可以在 Popen 对象建立子进程的时候改变标准输入、标准输出和标准错误,并可以利用 subprocess.PIPE 将多个子进程的输入和输出连接在一起,构成管道(pipe),如下例子:
1 2 3 4 |
>>> import subprocess >>> child1 = subprocess.Popen(["cat", "/etc/passwd"], stdout=subprocess.PIPE) >>> child2 = subprocess.Popen(["grep", "0:0"], stdin=child1.stdout, stdout=subprocess.PIPE) >>> print(child2.communicate()) |
subprocess.PIPE 实际上为文本流提供一个缓存区。child1 的 stdout 将文本输出到缓存区,随后 child2 的 stdin 从该PIPE中将文本读取走。 child2 的输出文本也被存放在 PIPE 中,直到 communicate() 方法从 PIPE 中读取出 PIPE 中的文本。
注意:communicate() 方法是 Popen 对象的一个方法,该方法会阻塞父进程,直到子进程完成。
subprocess
模块对于依赖TTY的外部命令不合适用。 例如,你不能使用它来自动化一个用户输入密码的任务(比如一个ssh会话)。 这时候,你需要使用到第三方模块了,比如基于著名的 expect
家族的工具(pexpect或类似的)。
<参考>
https://www.jianshu.com/p/8e582146bd4c
https://mp.weixin.qq.com/s/2xm_rNb2Vqyb61JKUoHrAg
http://blog.chinaunix.net/uid-23504396-id-4661783.html
http://www.10tiao.com/html/160/201708/2649640336/1.html#collapseOne
https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess