如何打印和显示子进程的标准输出和标准错误输出而不出现扭曲?

8

也许有人能在这个领域帮助我。 (我在SO上看到了许多类似的问题,但没有一个涉及标准输出和标准错误,或者处理像我这样的情况,因此提出了这个新问题。)

我有一个Python函数,它打开一个子进程,等待其完成,然后输出返回代码以及标准输出和标准错误管道的内容。 在进程运行时,我还想显示两个管道的输出,因为它们被填充。 我的第一次尝试结果是这样的:

process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

stdout = str()
stderr = str()
returnCode = None
while True:
    # collect return code and pipe info
    stdoutPiece = process.stdout.read()
    stdout = stdout + stdoutPiece
    stderrPiece = process.stderr.read()
    stderr = stderr + stderrPiece
    returnCode = process.poll()

    # check for the end of pipes and return code
    if stdoutPiece == '' and stderrPiece == '' and returnCode != None:
        return returnCode, stdout, stderr

    if stdoutPiece != '': print(stdoutPiece)
    if stderrPiece != '': print(stderrPiece)

然而,这里有几个问题。因为read()读取直到EOF,所以while循环的第一行不会返回直到子进程关闭管道。
我可以将read()替换为read(int),但打印输出被扭曲,被截断在读取字符的末尾。我可以替代使用readline(),但当同时有许多输出和错误时,打印输出会扭曲,输出和错误信息交替出现。
也许有一种read-until-end-of-buffer()变量是我不知道的?或者它可以被实现?
也许最好像在另一篇文章的答案中建议的那样实现一个sys.stdout包装器?但是我只想在这个函数中使用包装器。
社区还有其他想法吗?
感谢您的帮助! :)
编辑:解决方案确实应该是跨平台的,但如果您有不跨平台的想法,请分享它们以使头脑风暴继续进行。

如果你对我的另一个Python子进程问题感到困惑,请查看我在计算子进程开销的时间上的另一个问题。


你可能想看看类似pexpect的东西。 - Thomas K
为什么不只是创建一个StringIO并将相同的实例传递给子进程的stdout和stderr呢? - Nathan Ernst
@NathanErnst:因为那样行不通。stdout和stderr必须是真正的操作系统级文件描述符。 - Sven Marnach
1
@Sven Marnach,我刚刚查看了文档,stderr可以设置为STDOUT,这样就可以重定向,然后您可以将stdout设置为PIPE,并从stdout中读取。 - Nathan Ernst
3个回答

11

使用fcntl.fcntl使管道变为非阻塞,并使用select.select等待两个管道中的数据变得可用。例如:

# Helper function to add the O_NONBLOCK flag to a file descriptor
def make_async(fd):
    fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK)

# Helper function to read some data from a file descriptor, ignoring EAGAIN errors
def read_async(fd):
    try:
        return fd.read()
    except IOError, e:
        if e.errno != errno.EAGAIN:
            raise e
        else:
            return ''

process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
make_async(process.stdout)
make_async(process.stderr)

stdout = str()
stderr = str()
returnCode = None

while True:
    # Wait for data to become available 
    select.select([process.stdout, process.stderr], [], [])

    # Try reading some data from each
    stdoutPiece = read_async(process.stdout)
    stderrPiece = read_async(process.stderr)

    if stdoutPiece:
        print stdoutPiece,
    if stderrPiece:
        print stderrPiece,

    stdout += stdoutPiece
    stderr += stderrPiece
    returnCode = process.poll()

    if returnCode != None:
        return (returnCode, stdout, stderr)
请注意,fcntl 只在类Unix平台上可用,包括Cygwin。 如果您需要在没有Cygwin的Windows上使用它,这是可以做到的,但要困难得多。您需要:

我明白了。嗯,你的类Unix解决方案运行得很好,我想我会把Windows端的事情留到一个下雨天的项目中。感谢你的帮助! - perden
2
Windows 系统的支持并不如 Linux 系统那样完善,但我猜这就是野兽的本质...一个庞大、复杂、闭源、非标准管道的野兽。 - perden
它是否因为早期在 poll() is not None 上的返回而丢失了一些输出?更好的 EAGAIN 返回值可能是 None,以允许在空字符串上检测 eof。顺便说一下,如果有一些支持管道超时的 select 平台,但不支持 fcntl(NONBLOCK),那么可以使用 os.read(size) 来读取可用的输出(它可能小于 size)。虽然我不知道有这样的平台。 - jfs
大家好!请不要以这种方式使用此代码。 如果您的命令没有产生太多输出,select.select([process.stdout, process.stderr], [], [])可能会永远挂起。 最好这样使用它: select.select([process.stdout, process.stderr], [], [], 1) 或者 select.select([process.stdout, process.stderr], [], [], 0) 或者 select.select([process.stdout, process.stderr], [], [], 0.1) 无论哪种方式都可以。 Adam Rosenfield,请纠正您的代码。 - kinORnirvana
一个在生产中(Chromium的CI构建)使用的替代函数在这里: https://github.com/catapult-project/catapult/blob/master/devil/devil/utils/cmd_helper.py#L214请查看_IterProcessStdout()方法。 - kinORnirvana
显示剩余2条评论

0

当我测试它时,readline() 似乎是阻塞的。但是,我能够使用线程分别访问 stdout 和 stderr。代码示例如下:

import os
import sys
import subprocess
import threading

class printstd(threading.Thread):
    def __init__(self, std, printstring):
        threading.Thread.__init__(self)
        self.std = std
        self.printstring = printstring
    def run(self):
        while True:
          line = self.std.readline()
          if line != '':
            print self.printstring, line.rstrip()
          else:
            break

pythonfile = os.path.join(os.getcwd(), 'mypythonfile.py')

process = subprocess.Popen([sys.executable,'-u',pythonfile], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

print 'Process ID:', process.pid

thread1 = printstd(process.stdout, 'stdout:')
thread2 = printstd(process.stderr, 'stderr:')

thread1.start()
thread2.start()

threads = []

threads.append(thread1)
threads.append(thread2)

for t in threads:
    t.join()

然而,我不确定这是否是线程安全的。


0

结合这个答案这个, 下面的代码对我有效:

import subprocess, sys
p = subprocess.Popen(args, stderr=sys.stdout.fileno(), stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, ""):
 print line,

如果您要合并stderr和stdout,更常见的方法是使用stderr=subprocess.STDOUT - dbn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接