如何无扭曲地打印和显示子进程的stdout和stderr输出?

8 投票
3 回答
4341 浏览
提问于 2025-04-17 04:06

也许在这个网络空间里,有人能帮我解决这个问题。(我在StackOverflow上看到过很多类似的问题,但没有一个同时涉及标准输出和标准错误,或者处理的情况和我一样,所以我才发了这个新问题。)

我有一个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(int)来替代read(),但打印出来的内容会变得很奇怪,最后的字符会被截断。我也可以用readline()来替代,但当同时有很多输出和错误时,打印的内容会交替出现,变得很混乱。

也许有一种read-until-end-of-buffer()的变体我还不知道?或者说可以实现这种功能?

也许按照这个其他帖子中的建议,实现一个sys.stdout的包装器会更好?不过我只想在这个函数里使用这个包装器。

社区里还有其他的想法吗?

谢谢大家的帮助! :)

编辑:这个解决方案最好是跨平台的,但如果你有不跨平台的想法,也请分享出来,继续激发灵感。


如果你对我的另一个Python子进程的问题感兴趣,可以看看我在计时时考虑子进程开销的另一个问题。

3 个回答

0

当我测试的时候,发现readline()这个函数会阻塞,也就是说它会让程序停下来等着输入。不过,我可以通过使用线程来分别访问标准输出和标准错误。下面是代码示例:

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,
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 上使用这个功能,虽然可以做到,但会难得多。你需要:

撰写回答