美文网首页
python多进程(四)利用subprocess执行shell命

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

作者: eeert2 | 来源:发表于2019-08-23 00:28 被阅读0次

    该文章基于 python3.7,部分功能只有python3.7能实现
    分布式 进程 线程 系列文章 https://www.jianshu.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 文本流 https://www.jianshu.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']
    

    这里使用了上下文管理器https://www.jianshu.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).

    相关文章

      网友评论

          本文标题:python多进程(四)利用subprocess执行shell命

          本文链接:https://www.haomeiwen.com/subject/mitosctx.html