如何无扭曲地打印和显示子进程的stdout和stderr输出?
也许在这个网络空间里,有人能帮我解决这个问题。(我在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 个回答
当我测试的时候,发现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()
不过,我不太确定这样做是否安全,可能会有一些问题。
通过使用 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 上使用这个功能,虽然可以做到,但会难得多。你需要:
- 使用 pywin32 库来调用原生的 Win32 API
- 使用
SetNamedPipeHandleState
和PIPE_NOWAIT
来让标准输出和错误输出的管道变成非阻塞模式 - 用
WaitForMultipleObjects
来等待数据可用,而不是使用select
- 使用
ReadFile
来读取数据