从subprocess.communicate()读取流输入


83

我正在使用Pythonsubprocess.communicate()从运行约一分钟的进程中读取stdout。

如何stdout以流方式打印出该流程的每一行,以便可以看到生成的输出,但是仍然阻止该流程终止,然后再继续?

subprocess.communicate() 似乎一次给出所有输出。


相关:使用子
进程

Answers:


44

请注意,我认为JF Sebastian的方法(如下)更好。


这是一个简单的示例(不检查错误):

import subprocess
proc = subprocess.Popen('ls',
                       shell=True,
                       stdout=subprocess.PIPE,
                       )
while proc.poll() is None:
    output = proc.stdout.readline()
    print output,

如果ls结束太快,则while循环可能在您读取所有数据之前结束。

您可以通过以下方式在stdout中捕获其余部分:

output = proc.communicate()[0]
print output,

1
这种方案是否会成为python doc所引用的缓冲区阻塞问题的受害者?
海因里希·施密特林

@Heinrich,我不太了解缓冲区阻塞问题。我相信(只是从谷歌搜索而来),仅当您未在while循环内从stdout(和stderr?)读取数据时,才会出现此问题。所以我认为上面的代码还可以,但是我不能确定。
unutbu 2010年

1
实际上,这确实存在阻塞问题,几年前,我没有遇到麻烦,在这种情况下,即使proc结束,readline也会阻塞'直到换行。我不记得该解决方案,但我认为它与在工作线程上进行读取,只是循环while proc.poll() is None: time.sleep(0)或实现某种效果有关。基本上,您需要确保输出换行符是该过程要做的最后一件事(因为您不能让解释器有时间再次循环),或者您需要做一些“花哨的事情”。
dash-tom-bang 2010年

:@Heinrich:亚历克斯·马尔泰利写了关于如何避免此僵局stackoverflow.com/questions/1445627/...
unutbu

6
缓冲区阻塞比有时听起来更简单:等待孩子退出的父块+等待父母读取的子块并释放通信管道中的一些空间,该空间已满=死锁。就这么简单。管道越小,发生的可能性就越大。
MarcH 2013年

160

要在子进程刷新其标准输出缓冲区时逐行获取子进程的输出:

#!/usr/bin/env python2
from subprocess import Popen, PIPE

p = Popen(["cmd", "arg1"], stdout=PIPE, bufsize=1)
with p.stdout:
    for line in iter(p.stdout.readline, b''):
        print line,
p.wait() # wait for the subprocess to exit

iter()用于在编写行后立即读取行,以解决Python 2中的预读错误

如果子进程的stdout在非交互模式下使用块缓冲而不是行缓冲(这会导致输出延迟,直到子缓冲区已满或被子进程显式刷新),然后您可以尝试使用以下方式强制使用无缓冲输出 pexpectpty模块unbufferstdbufscript公用事业,见问:为什么不直接使用管道(popen方法())?


这是Python 3代码:

#!/usr/bin/env python3
from subprocess import Popen, PIPE

with Popen(["cmd", "arg1"], stdout=PIPE, bufsize=1,
           universal_newlines=True) as p:
    for line in p.stdout:
        print(line, end='')

注意:与Python 2不同,Python 2照原样输出子进程的字节串。Python 3使用文本模式(使用locale.getpreferredencoding(False)编码对cmd的输出进行解码)。


b''是什么意思?
亚伦

4
b''是一个bytes在Python 2.7和Python 3.字面
JFS

2
@JinghaoShi:bufsize=1如果您还向子进程写入(使用p.stdin),可能会有所不同,例如,它可以帮助避免在进行交互式(类似pexpect)交换时出现死锁-假设子进程本身没有缓冲问题。如果您只是在阅读,那么就像我说的那样,区别仅在于性能:如果不是,那么您能否提供一个最小的完整代码示例来说明这一点?
jfs

1
@ealeon:是的。除非您将stderr合并到stdout中(通过传递到),否则它需要能够同时读取stdout / stderr的技术。另请参阅链接到那里的线程异步解决方案stderr=subprocess.STDOUTPopen()
jfs

2
@saulspatz如果stdout=PIPE未捕获输出(您仍然在屏幕上看到它),则您的程序可能会打印到stderr或直接打印到终端。要合并stdout&stderr,请通过stderr=subprocess.STDOUT(请参阅我之前的评论)。要捕获直接打印到tty的输出,可以使用pexpect,pty解决方案。。这是一个更复杂的代码示例
jfs

6

我相信以流方式从流程收集输出的最简单方法是这样的:

import sys
from subprocess import *
proc = Popen('ls', shell=True, stdout=PIPE)
while True:
    data = proc.stdout.readline()   # Alternatively proc.stdout.read(1024)
    if len(data) == 0:
        break
    sys.stdout.write(data)   # sys.stdout.buffer.write(data) on Python 3.x

readline()read()函数只应在EOF返回一个空字符串,进程终止后-否则,如果没有什么阅读(它会阻止readline()包括换行,等空行,则返回“\ n”)。这样避免了communicate()在循环后进行笨拙的最终调用。

对于行长很长的文件,read()最好减少最大内存使用量-传递给它的数量是任意的,但如果排除该数量,则会导致立即读取整个管道输出,这可能是不希望的。


4
data = proc.stdout.read()阻止,直到读取所有数据。您可能将它与os.read(fd, maxsize)可以更早返回(一旦有任何可用数据)相混淆。
jfs

您是对的,我误会了。但是,如果将合理数量的字节作为参数传递给read()它,那么它将正常工作,并且readline()只要最大行长是合理的,同样可以正常工作。相应地更新了我的答案。
D Coetzee


3

如果您只是想尝试实时传递输出,那么很难做到比这更简单:

import subprocess

# This will raise a CalledProcessError if the program return a nonzero code.
# You can use call() instead if you don't care about that case.
subprocess.check_call(['ls', '-l'])

请参阅有关subprocess.check_call()文档

如果需要处理输出,请确保对其进行循环。但是,如果不这样做,请保持简单。

编辑: JF Sebastian指出,stdout和stderr参数的默认值都传递给sys.stdout和sys.stderr,并且如果替换了sys.stdout和sys.stderr,这将失败(例如,用于捕获输出。测试)。


它不会,如果工作sys.stdoutsys.stderr与文件一样,并没有真正的fileno对象是()取代。如果sys.stdoutsys.stderr没有那么取代它就更简单了:subprocess.check_call(args)
jfs

谢谢!我已经意识到了替换sys.stdout / stderr的麻烦,但是以某种方式却从未意识到,如果省略参数,它将把stdout和stderr传递到正确的位置。我喜欢call()结束,check_call()除非我想要CalledProcessError
内特2015年

python -mthis“错误绝不能静默传递。除非明确静默。” 这就是示例代码应优先check_call()于的原因call()
jfs 2015年

嘿。我准备的许多程序call()在非错误情况下都会返回非零错误代码,因为它们很糟糕。因此,在我们的案例中,非零错误代码实际上不是错误。
2015年

是。有些程序grep即使没有错误也可能返回非零退出状态-它们是例外。默认情况下,零退出状态表示成功。
jfs 2015年

1
myCommand="ls -l"
cmd=myCommand.split()
# "universal newline support" This will cause to interpret \n, \r\n and \r     equally, each as a newline.
p = subprocess.Popen(cmd, stderr=subprocess.PIPE, universal_newlines=True)
while True:    
    print(p.stderr.readline().rstrip('\r\n'))

1
总是最好解释一下您的解决方案是如何使人们更好地理解的
DaFois

2
您应该考虑使用shlex.split(myCommand)而不是myCommand.split()。它也尊重引号中的空格。
犹他州Jarhead

0

添加另一个具有一些小的更改的python3解决方案:

  1. 允许您捕获shell进程的退出代码(使用with构造时,我无法获取退出代码)
  2. 还可以实时输出stderr
import subprocess
import sys
def subcall_stream(cmd, fail_on_error=True):
    # Run a shell command, streaming output to STDOUT in real time
    # Expects a list style command, e.g. `["docker", "pull", "ubuntu"]`
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True)
    for line in p.stdout:
        sys.stdout.write(line)
    p.wait()
    exit_code = p.returncode
    if exit_code != 0 and fail_on_error:
        raise RuntimeError(f"Shell command failed with exit code {exit_code}. Command: `{cmd}`")
    return(exit_code)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.