python多进程(四)利用subprocess执行shell命令

该文章基于 python3.7,部分功能只有python3.7能实现
分布式 进程 线程 系列文章 //www.greatytc.com/nb/39184979

在前面两章,我们介绍了multiprocessing模块的使用,它是一个跨平台的多进程模块,今天介绍的是另外一个创建多进程的模块subprocess.

这两个模块都可以在当前主进程中创建子进程,但不同的是multiprocessing中的进程主要是运行python代码,而subprocess中的进程是运行已经编写好的程序,或者说是shell命令。

举个栗子,我们在程序运行中想要知道当前主机的IP,那么使用subprocess模块运行ifconfig(Unix平台) 或 ipconfig(Windows平台),然后再获取子进程的输出信息,从输出信息中就可以解析出当前IP.

再或者,我用Java写了一套程序,现在在python中需要这部分功能,那么可以在python子进程中执行两个命令:

javac HelloWorld.java
java HelloWorld

并且获取Java程序的输出信息。

备注:前提是已经安装了Java,并且正确配置环境变量。

一、通过便捷函数run()执行 shell

首先我们需要了解文本流,不然后面的可能会看着很吃力。
linux 文本流 //www.greatytc.com/p/62abe469ecc6

计算机中有“万物皆文本流”的说法,任何标准的程序/进程都有标准输入流标准输出流错误输出流,在subprocess模块中尤为重要,只有获取了输入流、输出流,我们才能从中输入指令,获取打印信息。(如果你执行了ls命令,确不能获取输出信息,那有什么意义呢)。

【在multiprocessing模块中,进程主要是执行代码,所以输入、输出就不怎么重要,也没有设置的参数,默认重定向到父进程的输入、输出。】

1) 便捷函数 run()
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)

别怕,这个run()函数很长、很长,但并不是所有都需要的,我们必要设置的只有第一项args,也就是shell命令
例如运行ifconfig,如果你是Windows系统,可以使用ipconfig代替。

import subprocess

subprocess.run(args=['ifconfig',])

执行结果如下:

lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    nd6 options=201<PERFORMNUD,DAD>
……
  • args
    args参数传入一个列表或者元组,如['ls','-l'],python会自动拼接成shell命令.[第一个参数是执行的程序,其余的是参数]
    也可以直接就是一个str命令行,如果如果传入的是shell命令,则需要另外添加一个参数shell=True

  • input
    有的时候运行的程序需要输入参数,例如我们写个一个python程序,文件目录为当前目录下的test.py

#  test.py
print('请输入一个参数数字:')
p = input()
print(p * 2)

在子进程中使用shell命令执行

import subprocess

subprocess.run(args='python ./test.py', input=b'4', shell=True)

执行结果

请输入一个参数数字:
44

这里的input就是程序的输入,被传递给 Popen.communicate() 以及子进程的标准输入. 如果使用此参数, 它必须是一个字节序列.如果指定了 encodingerrors 或者将text 设置为 True, 那么也可以是一个字符串.

  • check
    进程正常运行完毕是以状态码为0退出。
    如果 check 设为 True, 则会检查退出状态码,进程以非零状态码退出时, 一个 CalledProcessError 异常将被抛出.
    例如:
subprocess.run("exit 1", shell=True, check=True)

则在主进程中会抛出异常

subprocess.CalledProcessError: Command 'exit 1' returned non-zero exit status 1
  • timeout
    参数将被传递给 Popen.communicate()。如果发生超时,子进程将被杀死并等待。 TimeoutExpired 异常将在子进程中断后被抛出。

  • encoding 被指定, 或者 text 被设为 True, 标准输入, 标准输出和标准错误的文件对象将通过指定的 encoding 以文本模式打开, 默认是通过二进制模式打开

import subprocess

subprocess.run(args='python ./test.py', input='4', shell=True, encoding='utf-8')

和上面传入input=b'4'效果相同。

  • stdinstdoutstderr 分别指定了执行的程序的标准输入、输出和标准错误文件句柄。合法的值有 PIPEDEVNULL 、 一个现存的文件描述符(一个正整数)、一个现存的文件对象以及 None。默认是None,标准输出、标准错误继承自父进程,也就是当前的终端。
    推荐使用PIPE,作为标准输出、错误输出流,这是一个信号,(它就是 -1),表明由程序自动创建管道,在run()运行完毕后,可以从从返回结果中获取信息,省去我们自己创建、关闭流的过程。
    stdininput只能选择一个,区别是input中能输入一次,而stdin是一个输入流,可以传入多个输入数据。
    这里采用一个文件流in.txt作为输入流【stdin只能自己创建输入流,不能使用PIPE信号】。
# in.txt
4

运行如下程序

import subprocess
from subprocess import PIPE

f = open('./in.txt', 'rb')

r = subprocess.run(args='python ./test.py', shell=True, encoding='utf-8', stdout=PIPE, stdin=f)
print(r.stdout)

f.close()

r.stdout 保存了子进程的输出信息,也就是请输入一个参数数字: 44

rsubprocess.run()的运行结果.

2) run()的返回对象 class subprocess.CompletedProcess

CompletedProcessrun() 的返回值, 代表一个进程已经结束.具有以下方法

  • args
    返回被用作启动进程的参数. 可能是一个列表或字符串.
  • returncode
    子进程的退出状态码. 通常来说, 一个为 0 的退出码表示进程运行正常.
    一个负值 -N 表示子进程被信号 N 中断 (仅 POSIX).
  • stdout
    从子进程捕获到的标准输出. 一个字节序列, 或一个字符串, 如果 run() 是设置了 encoding, errors 或者 text=True 来运行的. 如果未有捕获, 则为 None.
  • stderr
    捕获到的子进程的标准错误. 一个字节序列, 或者一个字符串, 如果 run() 是设置了参数 encoding, errors 或者 text=True 运行的. 如果未有捕获, 则为 None.
  • check_returncode()
    如果 returncode 非零, 抛出 CalledProcessError.

二、子进程对象 Popen

一般来说,我们执行一条shell命令,使用subprocess.run()这个便捷函数就可以了,它足以满足我们的大部分需求,如果你需要对子进程有更细致的控制,那么可以通过构造Popen对象来运行shell,run()函数也是通过构造Popen来运行shell命令的。

1)Popen构造函数

class 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)

此函数中大部分功能与 上面的run()是相同的,其中stdinrun()不同,这里的stdin可以使用PIPE信号,这样程序会自己创建一个输入流,并返回这个输入流。

想一想为什么这里可以,而run()不可以?

仔细的想,run()函数是不够透明的,它只呈现开始和结束状态,也就是说run()返回的是程序已经结束的状态,如果由程序自己创建一个输入流,并在程序结束后返回给你,那完全没有意义(程序都结束了还输入啥子)
所以在run()中要么你使用input直接输入一条输入,要么通过stdin指定一个已经存在的(并且有内容的)输入流。

通过子进程对象 Popen就不一样了,我们可以从始至终控制该子进程,进行输入输出,那这个时候让程序帮我们创建输入流(并管理流的关闭),我们就可以通过输入流和子进程交互了。

2)子进程Popen常用方法

  • args
    args 参数
  • kill()
    杀死子进程。在 Posix 操作系统上,此函数给子进程发送 SIGKILL 信号。在 Windows 上, kill()terminate() 的别名
  • terminate()
    停止子进程。在 Posix 操作系统上,此方法发送 SIGTERM。在 Windows,调用 Win32 API 函数 TerminateProcess() 来停止子进程。

  • poll()
    检查子进程是否已被终止。设置并返回 returncode 属性。否则返回 None

  • wait(timeout=None)
    等待子进程被终止。设置并返回 returncode 属性。

如果进程在 timeout 秒后未中断,抛出一个 TimeoutExpired 异常,可以安全地捕获此异常并重新等待。

  • communicate(input=None, timeout=None)
    与进程交互:向 stdin 传输数据。从 stdout 和 stderr 读取数据,直到文件结束符。等待进程终止。可选的 input 参数应当未被传输给子进程的数据,如果没有数据应被传输给子进程则为 None。如果流以文本模式打开, input 必须为字符串。否则,它必须为字节。
    communicate() 返回一个 (stdout_data, stderr_data) 元组。如果文件以文本模式打开则为字符串;否则字节。
    注意如果你想要向进程的 stdin 传输数据,你需要通过 stdin=PIPE 创建此 Popen 对象。类似的,要从结果元组获取任何非 None 值,你同样需要设置 stdout=PIPE 或者 stderr=PIPE。
    如果进程在 timeout 秒后未终止,一个 TimeoutExpired 异常将被抛出。捕获此异常并重新等待将不会丢失任何输出。
    如果超时到期,子进程不会被杀死,所以为了正确清理一个行为良好的应用程序应该杀死子进程并完成通讯。
from subprocess import Popen, TimeoutExpired, PIPE

# 这里必须使用信号 PIPE 而不是自己创建的文本流,这样才能使用 communicate 函数
proc = Popen(args='python ./test.py', shell=True, encoding='utf-8', stdin=PIPE, stdout=PIPE, stderr=PIPE)
try:
    outs, errs = proc.communicate(input='4',timeout=15)
    print(outs)
except TimeoutExpired:
    proc.kill()

打印结果

请输入一个参数数字:
44

注意communicate()只能使用一次,也就是只能输入一个数据,上面提到的便捷函数run()input参数就是在这里调用了communicate()

如果我们想入输入多个input,那就不能使用communicate(),而是自己控制输入流。
一种方法是在构造函数中将stdin设置为已经输入好的文本流/二进制流。

但有的时候我们需要输入的内容是不确定的,还要根据程序运行的情况动态输入多个数据,那么可以将stdin设置为PIPE,从进程中获取输入流句柄,自己往输入流中写数据。
(需要自己控制细节,如读取一行的结束符,何时将流中的数据从缓存中刷新到子进程中,关闭输入输出管道,而使用communicate()则不接触管道/流,就不需要关注这些)
如下:

#test.py
# 需要输入两个数据
print('请输入一个参数数字:')

p = input()
print(p * 2)

p2 = input()
print(p2 * 3)

执行子进程

from subprocess import Popen, TimeoutExpired, PIPE

with Popen(args='python ./test.py', shell=True, encoding='utf-8', stdin=PIPE, stdout=PIPE, stderr=PIPE) as proc:
    try:
        stdin = proc.stdin
        # 以\n结束,程序才能判断这是一次完整的读取 效果和`'1\n2\n'`相同
        stdin.write('1\n')
        stdin.write('2\n')
        # 将输入从缓存刷新到子进程中
        stdin.flush()
        s = proc.stdout.readlines()
        print(s)

    except TimeoutExpired:
        proc.kill()

执行结果如下:

['请输入一个参数数字:\n', '11\n', '222\n']

这里使用了上下文管理器//www.greatytc.com/p/a97e6aeca3fb
来管理管道的关闭,所以没有手动的去执行close()函数。
[这里Popen实现了上下文管理器协议,并且只有使用了PIPE 信号创建的管道才可以这样做]

  • stdin
    只有stdin 参数为 PIPE才可用,此属性是一个类似 open() 返回的可写的流对象。

  • stdout
    只有 stdout 参数是 PIPE才可用,此属性是一个类似 open() 返回的可读流。

  • stderr
    只有 stderr 参数是 PIPE才可用,此属性是一个类似 open() 返回的可读流。

  • pid
    子进程的进程号
    注意如果你设置了 shell 参数为 True,则这是生成的子 shell 的进程号。

  • returncode
    此进程的退出码,由 poll()wait() 设置(以及直接由 communicate() 设置)。一个 None 值 表示此进程仍未结束。
    [意思是说通过 poll()wait()判断进程状态时,我们可以杀死进程,这样进程的状态码就非 0]
    一个负值 -N 表示子进程被信号 N 中断 (仅 POSIX).

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容