【Python基础】subprocess模块

Python执行Linux的Shell命令方法总结。

一、简介

  工作中经常会遇到写工具脚本的需求,需要我们通过Python来执行shell命令。
  Python中可以执行shell命令的相关模块和函数有:
  ●  os.system
  ●  os.spawn*
  ●  os.popen* –废弃
  ●  popen2.* –废弃
  ●  commands.* –废弃,3.x中被移除
  乍一看是不是很凌乱,怎么有那么多,选择恐惧症啊:到底哪个是主流呢?
  以前我比较常用的是 commands,用起来很方便,如下:

import commands

result = commands.getoutput('cmd')
result = commands.getstatus('cmd')
result = commands.getstatusoutput('cmd')

  随着Python版本的更新,上面有这么多模块,必然引起了代码的复杂与冗余。因此Python新引入了一个模块 subprocess。它将以上几个模块的功能集中到了一起。
  所以今后只需要 import subprocess 这一个即可。
  subprocess 的目的就是启动一个新的进程并且与之通信。

二、subprocess常用的封装函数

1. subprocess.call()

  父进程等待子进程执行命令,返回子进程执行命令的状态码,如果出现错误,不进行报错
  * 这里说的返回执行命令的状态码的意思是:如果我们通过一个变量 res = subprocess.call([‘dir’,shell=True]) 获取的执行结果,我们能获取到的是子进程执行命令执行结果的状态码,即res=0/1 执行成功或者不成功,并不代表说看不到执行结果,在Python的console界面中我们是能够看到命令结果的,只是获取不到。想获取执行的返回结果,就要看后面的check_output。
  * 不进行报错解释:如果我们执行的命令在执行时,操作系统不识别,系统会返回一个错误,如:abc命令不存在,这个结果会在console界面中显示出来,但是我们的Python解释器不会提示任何信息,如果想让Python解释器也进行报错,就要看后面的check_output。
  下面举一个例子,执行 cat /etc/issue 命令:

# -*- coding: utf-8 -*-
# !/usr/bin/env python

import subprocess

result = subprocess.call(['cat', '/etc/issue'], shell=False)
print(result)

  注:shell默认为False,在Linux下,shell=False时, Popen调用os.execvp()执行args指定的程序;shell=True时,如果args是字符串,Popen直接调用系统的Shell来执行args指定的程序,如果args是一个序列,则args的第一项是定义程序命令字符串,其它项是调用系统Shell时的附加参数。
  在Windows下,不论shell的值如何,Popen调用CreateProcess()执行args指定的外部程序。如果args是一个序列,则先用list2cmdline()转化为字符串,但需要注意的是,并不是MS Windows下所有的程序都可以用list2cmdline来转化为命令行字符串。在windows下,调用脚本时要写上shell=True。

  输出结果:

Ubuntu 14.04.3 LTS \n \l

0

2. subprocess.check_call()

  父进程等待子进程执行命令,返回执行命令的状态码,如果出现错误,进行报错【如果returncode不为0,则举出错误subprocess.CalledProcessError,该对象包含有returncode属性,可用try…except…来检查】
  下面举一个例子,分别执行一条正确的命令和一条错误的命令:

# -*- coding: utf-8 -*-
# !/usr/bin/env python

import subprocess

print('执行一条正确的命令:')
result = subprocess.check_call(['cat', '/etc/issue'], shell=False)
print(result)


print('执行一条错误的命令:')
result = subprocess.check_call(['abc'], shell=False)
print(result)

  输出结果:

执行一条正确的命令:
Ubuntu 14.04.3 LTS \n \l

0
执行一条错误的命令:
Traceback (most recent call last):
  File "subprocess_check_call.py", line 12, in <module>
    result = subprocess.check_call(['abc'], shell=False)
  File "/usr/lib/python2.7/subprocess.py", line 535, in check_call
    retcode = call(*popenargs, **kwargs)
  File "/usr/lib/python2.7/subprocess.py", line 522, in call
    return Popen(*popenargs, **kwargs).wait()
  File "/usr/lib/python2.7/subprocess.py", line 710, in __init__
    errread, errwrite)
  File "/usr/lib/python2.7/subprocess.py", line 1327, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory

3. subprocess.check_output()

  父进程等待子进程执行命令,返回子进程向标准输出发送输出运行结果,检查退出信息,如果returncode不为0,则举出错误subprocess.CalledProcessError,该对象包含有returncode属性和output属性,output属性为标准输出的输出结果,可用try…except…来检查。
  下面举一个例子,执行 ping -c 4 www.baidu.com 命令:

# -*- coding: utf-8 -*-
# !/usr/bin/env python

import subprocess

result = subprocess.check_output(['ping', '-c', '4', 'www.baidu.com'], shell=False)
print(result)

  输出结果:

PING www.a.shifen.com (180.97.33.107) 56(84) bytes of data.
64 bytes from 180.97.33.107: icmp_seq=1 ttl=55 time=5.52 ms
64 bytes from 180.97.33.107: icmp_seq=2 ttl=55 time=6.65 ms
64 bytes from 180.97.33.107: icmp_seq=3 ttl=55 time=6.18 ms
64 bytes from 180.97.33.107: icmp_seq=4 ttl=55 time=7.77 ms

--- www.a.shifen.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 5.529/6.537/7.779/0.824 ms

  可见,call/check_call 返回值均是命令的执行状态码,而 check_output 返回值是命令的执行结果
  如果在执行相关命令时,命令后带有参数,将命令和所带的参数一起放在一个列表中传递给相关方法即可。

4. subprocess.Popen()

  实际上,subprocess模块中只定义了一个类:Popen。上面的几个函数都是基于Popen()的封装(wrapper)。从Python2.4开始使用Popen来创建进程,用于连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。这些封装的目的在于让我们容易使用子进程。当我们想要更个性化我们的需求的时候,就要转向Popen类,该类生成的对象用来代表子进程。
  构造函数如下:

subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None,
                 close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None,
                 creationflags=0)

  与上面的封装不同,Popen对象创建后,主程序不会自动等待子进程完成。我们必须调用对象的wait()方法,父进程才会等待(也就是阻塞block)。
  ① 不等待的子进程

# -*- coding: utf-8 -*-
# !/usr/bin/env python

import subprocess

child = subprocess.Popen(['ping', '-c', '4', 'www.baidu.com'])
print('hello world')

  输出结果:

wenyuanblog@localhost:~/home/test/script$ python subprocess_sub.py 
hello world
wenyuanblog@localhost:~/home/test/script$ PING www.a.shifen.com (180.97.33.107) 56(84) bytes of data.
64 bytes from 180.97.33.107: icmp_seq=1 ttl=55 time=6.28 ms
64 bytes from 180.97.33.107: icmp_seq=2 ttl=55 time=6.61 ms
64 bytes from 180.97.33.107: icmp_seq=3 ttl=55 time=6.30 ms
64 bytes from 180.97.33.107: icmp_seq=4 ttl=55 time=6.45 ms

--- www.a.shifen.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 6.287/6.413/6.610/0.162 ms

  可以看出,Python并没有等到child子进程执行的Popen操作完成就执行了print操作。
  ② 添加子进程等待

# -*- coding: utf-8 -*-
# !/usr/bin/env python

import subprocess

# 创建一个子进程,进程名为child,执行操作ping -c 4 www.baidu.com
child = subprocess.Popen(['ping', '-c', '4', 'www.baidu.com'])
# 子进程等待
child.wait()
print('hello world')

  输出结果:

wenyuanblog@localhost:~/home/test/script$ python subprocess_sub.py 
PING www.a.shifen.com (180.97.33.108) 56(84) bytes of data.
64 bytes from 180.97.33.108: icmp_seq=1 ttl=55 time=6.50 ms
64 bytes from 180.97.33.108: icmp_seq=2 ttl=55 time=5.88 ms
64 bytes from 180.97.33.108: icmp_seq=3 ttl=55 time=6.48 ms
64 bytes from 180.97.33.108: icmp_seq=4 ttl=55 time=6.74 ms

--- www.a.shifen.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 5.887/6.404/6.740/0.325 ms
hello world

  看出Python执行print操作是在child子进程操作完成以后才进行的。

  此外,你还可以在父进程中对子进程进行其它操作,比如我们上面例子中的child对象:

child.poll()  # 检查子进程状态
child.kill()  # 终止子进程
child.send_signal()  # 向子进程发送信号
child.terminate()  # 终止子进程
ps: 子进程的PID存储在child.pid

  子进程文本流控制
  子进程的标准输入、标准输出和标准错误如下属性分别表示:
  child.stdin | child.stdout | child.stderr
  我们还可以在Popen()建立子进程的时候改变标准输入、标准输出和标准错误,并可以利用subprocess.PIPE将多个子进程的输入和输出连接在一起,构成管道(pipe),如下2个例子。
  例1:

# -*- coding: utf-8 -*-
# !/usr/bin/env python

import subprocess

# 将标准输出定向输出到subprocess.PIPE
child = subprocess.Popen(['cat','/etc/issue'],stdout=subprocess.PIPE)
# 使用 child.communicate() 也可
print(child.stdout.read())

  输出结果:

Ubuntu 14.04.3 LTS \n \l

  例2:

# -*- coding: utf-8 -*-
# !/usr/bin/env python

import subprocess

child1 = subprocess.Popen(['cat', '/etc/issue'], stdout=subprocess.PIPE)
child2 = subprocess.Popen(['date', '+%Y-%m-%d'], stdin=child1.stdout, stdout=subprocess.PIPE)

print(child2.communicate())

  输出结果:

('2017-06-08\n', None)

  subprocess.PIPE实际上为文本流提供一个缓存区。child1的stdout将文本输出到缓存区,随后child2的stdin从该PIPE中将文本读取走。child2的输出文本也被存放在PIPE中,直到communicate()方法从PIPE中读取出PIPE中的文本。
  注意:communicate()是Popen对象的一个方法,该方法会阻塞父进程,直到子进程完成。

  子进程命令解释
  在上面的例子中我们创建子进程时,全部是调用Python进行解释,但Python并没有将所有命令全部解释,当Python不能进行解释时,就需要调用系统来进行执行。
  上面说过,带参数的命令,要使用列表的形式,其实我们也可以传字符串,只不过这时要传入shell=True,大概就是这个意思。

# -*- coding: utf-8 -*-
# !/usr/bin/env python

import subprocess

subprocess.Popen(['ls', '-l'])
subprocess.Popen(['ifconfig|grep 127.0.0.1'], shell=True)

三、总结

  subprocess模块作为最新出的“集大成者”,还是很强大的。上面整理的是平时常用的功能。如果需要更进一步了解内部函数,可以查看官方文档。


  目录