Python 内置模块之 subprocess

官方文档:https://docs.python.org/3/library/subprocess.html

0、背景

在用python编程时,我们经常会需要调用外部命令,比如用调用一些 Linux 的命令。这个时候就可以用到subprocess这个模块了。

subprocess 模块是 python 从2.4版本开始引入的模块。主要用来取代一些旧的模块方法,如os.system、os.spawn、os.popen、commands.*等。subprocess通过子进程来执行外部指令,并通过input/output/error管道,获取子进程的执行的返回信息。

1、介绍

subprocess模块可以生成新的进程,连接到它们的input/output/error管道,同时获取它们的返回码。

2、写在前面,因为参数 shellarg 有坑

2.1、重要参数 shellarg, 不但重要而且有坑

  • args:表示要执行的命令。

    • args参数可以接收一个类似 du -sh 的字符串。要执行的程序就是字符串本身
    • args参数也可以传递一个类似['du', '-sh']的字符串分割列表。要执行的程序一般就是这个列表的第一项。
  • shell:如果该参数为True,将通过操作系统的shell执行指定的命令

    • shell=True

      • 如果args是个字符串类,则调用shell去执行这个字符串。

      • 如果args是个列表,则第一项被视为命令,其余的都视为是给 shell本身 的参数。这会导致在 linux 和 windows 下面会有不同的后果

        • Linux:等效于:subprocess.Popen(['/bin/sh', '-c', args[0], args[1], ...])

        • Windows:subprocess.Popen(["notepad.exe", "test.txt"]),subprocess.Popen("notepad.exe test.txt") 效果一样。
          这是由于windows下的api函数CreateProcess接受的是一个字符串。即使是列表形式的参数,也需要先合并成字符串再传递给api函数

          1
          2
          3
          subprocess.Popen(["notepad.exe", "test.txt"]shell=True)
          等同于
          subprocess.Popen("cmd.exe /C "+"notepad.exe test.txt" shell=True
    • shell=False 时,subprocess.Popen只接受数组变量作为命令,并将数组的第一个元素作为命令,剩下的全部作为 该命令 的参数。

      • 在 Unix下,当shell=False(默认)时,Popen使用os.execvp()来执行子程序。

通常subprocess 会和 shlex 一块使用。shlex.split()可以被用于序列化复杂的命令参数

1
2
3
4
5
6
7
8
9
>>> shlex.split('ls ps top grep pkill')
['ls', 'ps', 'top', 'grep', 'pkill']
>>>import shlex, subprocess
>>>command_line = raw_input()
/bin/cat -input test.txt -output "diege.txt" -cmd "echo '$MONEY'"
>>>args = shlex.split(command_line)
>>> print args
['/bin/cat', '-input', 'test.txt', '-output', 'diege.txt', '-cmd', "echo '$MONEY'"]
# 可以看到,空格分隔的选项(如-input)和参数(如test.txt)会被分割为列表里独立的项,但引号里的或者转义过的空格不在此列。这也有点像大多数shell的行为。

2.2、其他参数

  • check:check为True时,表示执行命令的进程以非0状态码退出时会抛出;subprocess.CalledProcessError异常;check为False时,状态码为非0退出时不会抛出异常;

  • bufsize: 如果指定了bufsize参数,作用就和内建函数open()一样:

    • 0表示不缓冲,默认是0。
    • 1表示行缓冲
    • 其他正数表示近似的缓冲区字节数,负数表示使用系统默认值。
  • executable: 指定要执行的程序。它很少会被用到:一般程序可以由args 参数指定。如果shell=True ,executable 可以用于指定用哪个shell来执行(比如bash、csh、zsh等)。

    • *nix下,默认是 /bin/sh。
    • windows下,就是环境变量 COMSPEC 的值。windows下,只有当你要执行的命令确实是shell内建命令(比如dir ,copy 等)时,你才需要指定shell=True,而当你要执行一个基于命令行的批处理脚本的时候,不需要指定此项。
  • stdin stdout和stderr:分别表示子程序的标准输入、标准输出和标准错误。可选的值有PIPE或者一个有效的文件描述符(其实是个正整数)或者一个文件对象,还有None。

    • 如果是PIPE,则表示需要创建一个新的管道。
    • 如果是None,不会做任何重定向工作,子进程的文件描述符会继承父进程的。
    • 另外,stderr的值还可以是STDOUT,表示子进程的标准错误也输出到标准输出。
  • preexec_fn参数:如果把preexec_fn设置为一个可调用的对象(比如函数),就会在子进程被执行前被调用。(仅限*nix)

  • close_fds参数:如果把close_fds设置成True,*nix下会在开子进程前把除了0、1、2以外的文件描述符都先关闭。在 Windows下也不会继承其他文件描述符。

  • cwd参数:如果cwd不是None,则会把cwd做为子程序的当前目录。注意,并不会把该目录做为可执行文件的搜索目录,所以不要把程序文件所在目录设置为cwd 。

  • env参数:如果env不是None,则子程序的环境变量由env的值来设置,而不是默认那样继承父进程的环境变量。注意,即使你只在env里定义了某一个环境变量的值,也会阻止子程序得到其他的父进程的环境变量(也就是说,如果env里只有1项,那么子进程的环境变量就只有1个了)。例如:

    1
    2
    3
    4
    >>> subprocess.Popen('env', env={'test':'123', 'testtext':'zzz'})
    test=123

    testtext=zzz
  • universal_newlines参数:如果把universal_newlines 设置成True,则子进程的stdout和stderr被视为文本对象,并且不管是*nix的行结束符(’/n’ ),还是老mac格式的行结束符(’/r’ ),还是windows 格式的行结束符(’/r/n’ )都将被视为 ‘/n’ 。

  • startupinfo和creationflags参数:如果指定了startupinfo和creationflags,将会被传递给后面的CreateProcess()函数,用于指定子程序的各种其他属性,比如主窗口样式或者是子进程的优先级等。(仅限Windows)

3、基本操作方法

3.1、subprocess 运行命令的方法:runcallcheck_callcheck_output

subprocess.run(args[, stdout, stderr, shell ...])

  • 执行args命令,返回值为CompletedProcess类;
  • 若未指定stdout,则命令执行后的结果输出到屏幕上,函数返回值CompletedProcess中包含有args和returncode;
  • 若指定有stdout,则命令执行后的结果输出到stdout中,函数返回值CompletedProcess中包含有args、returncode和stdout;
  • 若执行成功,则returncode为0;若执行失败,则returncode为1;
  • run函数返回值为CompletedProcess类,若需获取执行结果,可通过获取返回值的stdout和stderr来捕获;
    若想获取args命令执行后的输出结果,命令为:output = subprocess.run(args, stdout=subprocess.PIPE).stdout

subprocess.call(args[, stdout, ...]) 执行命令,并返回执行状态,其中shell参数为False时,命令需要通过列表的方式传入,当shell为True时,可直接传入命令

  • 执行args命令,返回值为命令执行状态码;
  • 若未指定stdout,则命令执行后的结果输出到屏幕;
  • 若指定stdout,则命令执行后的结果输出到stdout;
  • 若执行成功,则函数返回值为0;若执行失败,则函数返回值为1;
  • (类似os.system)

subprocess.check_call(args[, stdout, ...])

  • 执行args命令,返回值为命令执行状态码;
  • 若未指定stdout,则命令执行后的结果输出到屏幕;
  • 若指定stdout,则命令执行后的结果输出到stdout;
  • 若执行成功,则函数返回值为0;若执行失败,抛出异常;
  • 用法与subprocess.call()类似,区别是,当返回值不为0时,直接抛出异常
  • (类似subprocess.run(args, check=True))

subprocess.check_output(args[, stderr, ...])

  • 执行args命令,返回值为命令执行的输出结果;
  • 若执行成功,则函数返回值为命令输出结果;若执行失败,则抛出异常;
  • (类似subprocess.run(args, check=True, stdout=subprocess.PIPE).stdout
  • 若需捕获错误信息,可通过stderr=subprocess.STDOUT来获取;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# ################### subprocess.run 
def subprocess_run():
result1 = subprocess.run(["adb", "devices"])
print("result1:", result1)
print("----------")
result2 = subprocess.run("adb devices", shell=True, check=True)
print("result2:", result2)
print("----------")
result3 = subprocess.run(["adb", "devices"], stdout=subprocess.PIPE)
print("result3:", result3)
print(type(result3))
subprocess_run()

""" 输出如下
List of devices attached
338b123f0504 device
result1: CompletedProcess(args=['adb', 'devices'], returncode=0)
----------
List of devices attached
338b123f0504 device
result2: CompletedProcess(args='adb devices', returncode=0)
----------
result3: CompletedProcess(args=['adb', 'devices'], returncode=0, stdout=b'List of devices attached \r\n338b123f0504\tdevice\r\n\r\n')
<class 'subprocess.CompletedProcess'>
"""


# ################### subprocess.call
def subprocess_call():
result1 = subprocess.call(["adb", "devices"])
print("result1:", result1)
print("----------")
result2 = subprocess.call(["adb", "devices"], stdout=subprocess.PIPE)
print("result2:", result2)
subprocess_call()

"""结果
List of devices attached
338b123f0504 device
result1: 0
----------
result2: 0
"""


# ################### subprocess.check_call
def subprocess_check_call():
result1 = subprocess.check_call(["adb", "devices"])
print("result1:", result1)
print("----------")
result2 = subprocess.check_call(["adb", "devices"], stdout=subprocess.PIPE)
print("result2:", result2)
subprocess_check_call()

"""结果
List of devices attached
338b123f0504 device
result1: 0
----------
result2: 0
"""


# ################### subprocess.check_output
def subprocess_check_output():
result1 = subprocess.check_output(["adb", "devices"])
print("result1:", result1)
print("----------")
result2 = subprocess.run(["adb", "devices"], stdout=subprocess.PIPE).stdout
print("result2:", result2)
subprocess_check_output()

"""结果
result1: b'List of devices attached \r\n338b123f0504\tdevice\r\n\r\n'
----------
result2: b'List of devices attached \r\n338b123f0504\tdevice\r\n\r\n'
"""

3.2、subprocess 获取输出 getoutputgetstatusoutput

subprocess.getoutput(cmd)

  • 执行cmd命令,返回值为命令执行的输出结果(字符串类型);
  • 执行失败,不会抛出异常(类似os.popen(cmd).read());
  • cmd:参数,字符串类型;

subprocess.getstatusoutput(cmd)

  • 执行cmd命令,返回值为元组类型(命令执行状态, 命令执行的输出结果);
  • 元组中命令执行状态为0,表示执行成功;命令执行状态为1,表示执行失败;
  • cmd:参数,字符串类型;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# subprocess.getoutput或getstatusoutput使用
def subprocess_get_output():
result1 = subprocess.getoutput("adb devices")
print("result1:", result1)
print(type(result1))

print("**** subprocess.getstatusoutput ****")
result2 = subprocess.getstatusoutput("adb devices")
print("result2:", result2)
print(type(result2))
subprocess_get_output()
"""结果
**** subprocess.getoutput ****
result1: List of devices attached
338b123f0504 device
<class 'str'>
**** subprocess.getstatusoutput ****
result2: (0, 'List of devices attached \n338b123f0504\tdevice\n')
<class 'tuple'>
"""

4、subprocess.Popen类

subprocess.Popen类用于在一个新进程中执行一个子程序,上述subprocess函数均是基于subprocess.Popen类;

语法

subprocess.Popen(args[, bufsize, stdin, stdout, stderr, ...])

Popen类的构造函数,返回结果为subprocess.Popen对象;

参数

  1. args:需要执行的系统命令,可为字符串序列(列表或元组,shell为默认值False即可,建议为序列),也可为字符串(使用字符串时,需将shell赋值为True);
  2. shell:默认False,若args为序列时,shell=False;若args为字符串时,shell=True,表示通过shell执行命令;
  3. stdout、stdin、stderr:分别表示子程序标准输出、标准输入、标准错误,可为subprocess.PIPE、一个有效的文件描述符、文件对象或None。
    若为subprocess.PIPE:代表打开通向标准流的管道,创建一个新的管道;
    若为None:表示没有任何重定向,子进程会继承父进程;
    stderr也可为subprocess.STDOUT:表示将子程序的标准错误输出重定向到了标准输出
  4. bufsize:默认0;指定缓冲策略,0表示不缓冲,1表示行缓冲,其它整数表示缓冲区大小,负数表示使用系统
  5. cwd:默认None;若非None,则表示将会在执行这个子进程之前改变当前工作目录;
  6. env:用于指定子进程的环境变量。若env为None,那么子进程的环境变量将从父进程中继承;若env非None,则表示子程序的环境变量由env值来设置,它的值必须是一个映射对象。
  7. universal_newlines: 不同系统的换行符不同。若True,则该文件对象的stdin,stdout和stderr将会以文本流方式打开;否则以二进制流方式打开
  8. preexec_fn:只在Unix平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用。
  9. Close_sfs:在windows平台下,如果close_fds被设置为True,则新创建的子进程将不会继承父进程的输入、输出、错误管道。我们不能将close_fds设置为True同时重定向子进程的标准输入、输出与错误(stdin, stdout, stderr)。
  10. startupinfo与createionflags只在windows下有效,它们将被传递给底层的CreateProcess()函数,用于设置子进程的一些属性,如:主窗口的外观,进程的优先级等等。

subprocess.Popen对象常用方法

下列 PopenObject 为 subprocess.Popen 对象

  • PopenObject.poll() :用于检查命令是否已经执行结束,若结束返回状态码;若未结束返回None;
  • PopenObject.wait([timeout, endtime]):等待子进程结束,并返回状态码;若超过timeout(s)进程仍未结束,则抛出异常;
  • PopenObject.send_signal(signal):发送信号signal给子进程;
  • PopenObject.terminate():停止子进程;在windows平台下,该方法将调用Windows API TerminateProcess()来结束子进程。
  • PopenObject.kill():杀死子进程;
  • PopenObject.communicate([input, timeout]):与进程进行交互(如向stdin发送数据,或从stdout和stderr中读取数据),它会阻塞父进程,直到子进程完成;注意:如果希望通过进程的stdin向其发送数据,在创建Popen对象的时候,参数stdin必须被设置为PIPE。同样,如果希望从stdout和stderr获取数据,必须将stdout和stderr设置为PIPE。
    • input:表示将发送到子进程的字符串数据,默认为None;
    • timeout:超时判断,若超过timeout秒后仍未结束则抛出TimeoutExpired异常;
    • communicate返回值:一个元组(stdout_data, stderr_data)
    • 要注意的是,subprocess.PIPE实际上为文本流提供一个缓存区。直到communicate()方法从PIPE中读取出PIPE中的文本.此方法比wait()好用。https://www.jianshu.com/p/8e582146bd4c
    • Linux下此方法可能有BUG,慎用。
  • PopenObject.pid:获取子进程的进程ID。
  • PopenObject.returncode:获取进程的返回值。如果进程还没有结束,返回None。

subprocess.Popen 对象的文本或字节流控制

  • PopenObject.stdin:
    若PopenObject中stdin为PIPE,则返回一个可写流对象;若encoding或errors参数被指定或universal_newlines参数为True,则此流是一个文件流,否则为字节流。
    若PopenObject中stdin不是PIPE,则属性为None。
    stdin输入流非None,可执行写操作即PopenObject.stdin.write(s)

  • PopenObject.stdout:
    若PopenObject中stdout为PIPE,则返回一个可读流对象;若encoding或errors参数被指定或universal_newlines参数为True,则此流是一个文件流,否则为字节流。
    若PopenObject中stdout不是PIPE,则属性为None。
    stdout输出流非None,可执行读操作即PopenObject.stdout.read()或.readlines()

  • PopenObject.stderr:
    若PopenObject中stderr为PIPE,则返回一个可读流对象;若encoding或errors参数被指定或universal_newlines参数为True,则此流是一个文件流,否则为字节流。
    若PopenObject中stderr不是PIPE,则属性为None。
    stderr错误流非None,可执行读操作即PopenObject.stderr.read()或.readlines()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def subprocess_Popen1():
print("***通过communicate函数分别输出PopenObject对象的输出流和错误流***")
args = [["adb", "devices"], ["adb", "devices11"]]
for arg in args:
popen_object = subprocess.Popen(arg, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
object_stdout, object_stderr = popen_object.communicate()
output = {"popen_object": popen_object,
"object_stdout": object_stdout,
"object_stderr": object_stderr}
print(output)
"""
{'popen_object': <subprocess.Popen object at 0x0000000002212400>, 'object_stdout': b'List of devices attached \r\n106D111805005938\tdevice\r\n\r\n', 'object_stderr': b''}
{'popen_object': <subprocess.Popen object at 0x0000000002577C18>, 'object_stdout': b'', 'object_stderr': b'Android Debug Bridge version 1.0.31\r\n\r\n -a .....}
"""

print("***通过stdout和stderr方法输出PopenObject对象输出流和错误流***")
p0 = subprocess.Popen(["adb", "devices"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
object_stdout = p0.stdout.read()
p0.stdout.close()
object_stderr = p0.stderr.read()
p0.stderr.close()
print(object_stdout) # 结果:b'List of devices attached \r\n338b123f0504\tdevice\r\n\r\n'
print(object_stderr) # 结果:b''

print("***Popen对象stdin写入功能:使用stdout和stderr输出")
args = ["python", "python1"]
for arg in args:
p4 = subprocess.Popen([arg], shell=True, stdout=subprocess.PIPE,
stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
p4.stdin.write("print('hello')")
p4.stdin.close()
out = p4.stdout.read()
p4.stdout.close()
err = p4.stderr.read()
p4.stderr.close()
print("out:%s err:%s" % (out, err))
"""
***Popen对象stdin写入功能
out:hello
err:
out: err:'python1' 不是内部或外部命令,也不是可运行的程序或批处理文件。
"""

print("***Popen对象stdin写入功能:使用communicate输出")
p4 = subprocess.Popen(["python"], stdout=subprocess.PIPE,
stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
p4.stdin.write("print('hello')")
output = p4.communicate()
print(output) # 结果:('hello\n', '')

print("***不含encoding参数***")
p1 = subprocess.Popen("adb devices", shell=True, stdout=subprocess.PIPE)
out1 = p1.stdout.readlines()
print(out1) # 结果: [b'List of devices attached \r\n', b'106D111805005938\tdevice\r\n', b'\r\n']

print("***含encoding参数***")
p2 = subprocess.Popen("adb devices", shell=True, stdout=subprocess.PIPE, encoding="utf-8")
out2 = p2.stdout.readlines()
print(out2) # 结果: ['List of devices attached \n', '106D111805005938\tdevice\n', '\n']

print("***Popen对象检查命令是否结束,等待进程结束")
print(p2.poll()) # 结果: None
print(p2.wait()) # 结果: 0
print(p2.poll()) # 结果: 0

print("***Popen对象communicate函数,它会阻塞父进程直至子进程完成")
p3 = subprocess.Popen("adb devices", shell=True, stdout=subprocess.PIPE)
out = p3.communicate()[0]
print(out) # 结果:b'List of devices attached \r\n338b123f0504\tdevice\r\n\r\n'
print(p3.poll()) # 结果:0
subprocess_Popen1()


def subprocess_Popen2():
"""
1. 通过管道功能,实现adb shell ps | findstr top功能
2. 直接为args赋值为一个字符串,实现adb shell ps | findstr top功能
:return:
"""
print("***通过管道方式***")
p1 = subprocess.Popen(["adb", "shell", "ps"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["findstr", "top"], stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p2.communicate()
print(out, err) # 结果:b'shell 8508 8504 2600 1044 c004e5f8 b6f40938 S top\r\r\n' b''
print("***通过传一个字符串方式***")
p3 = subprocess.Popen("adb shell ps | findstr top", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p3.communicate()
print(out, err) # 结果:b'shell 8508 8504 2600 1044 c004e5f8 b6f40938 S top\r\r\n' b''
subprocess_Popen2()

报错解决

报错信息

1
2
3
4
5
6
7
8
Traceback (most recent call last):
File "C:\Users\fenglepeng\.conda\envs\py37\lib\threading.py", line 926, in _bootstrap_inner
self.run()
File "C:\Users\fenglepeng\.conda\envs\py37\lib\threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
File "C:\Users\fenglepeng\.conda\envs\py37\lib\subprocess.py", line 1267, in _readerthread
buffer.append(fh.read())
UnicodeDecodeError: 'gbk' codec can't decode byte 0xac in position 421: illegal multibyte sequence

解决

1
2
# 参数中增加 encoding='utf-8'
sub = subprocess.Popen(command, shell=True, cwd=cwd, universal_newlines=True,stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.PIPE, encoding='utf-8')

Python 内置模块之 subprocess
https://flepeng.github.io/021-Python-31-Python-标准库-Python-标准库之-subprocess-子进程管理/
作者
Lepeng
发布于
2021年3月17日
许可协议