合并Python脚本子进程的stdout和stderr并保持可区分性

55 投票
7 回答
27187 浏览
提问于 2025-04-16 22:11

我想把一个Python脚本的子进程的输出和输入都写到同一个文件里。但是我不知道怎么让这两个来源的内容能区分开来?比如说,可以在错误信息的前面加个感叹号。

在我的情况下,其实不需要实时监控这个子进程,执行的Python脚本可以等到它执行完再继续。

7 个回答

9

如果你想要把输出的顺序弄得跟你在命令行里手动操作时差不多,那你需要像命令行一样,去不断检查输入和输出,然后按照检查的顺序来写数据。

下面有段代码,做的事情跟你想要的差不多——在这个例子中,它把标准输出和标准错误输出发送到日志的正常信息和错误信息流中。

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

poll = select.poll()
poll.register(tsk.stdout,select.POLLIN | select.POLLHUP)
poll.register(tsk.stderr,select.POLLIN | select.POLLHUP)
pollc = 2

events = poll.poll()
while pollc > 0 and len(events) > 0:
  for event in events:
    (rfd,event) = event
    if event & select.POLLIN:
      if rfd == tsk.stdout.fileno():
        line = tsk.stdout.readline()
        if len(line) > 0:
          logger.info(line[:-1])
      if rfd == tsk.stderr.fileno():
        line = tsk.stderr.readline()
        if len(line) > 0:
          logger.error(line[:-1])
    if event & select.POLLHUP:
      poll.unregister(rfd)
      pollc = pollc - 1
    if pollc > 0: events = poll.poll()
tsk.wait()
14

我最近遇到了这个问题,花了一些时间才找到一个在大多数情况下都能正常工作的解决方案,所以我把它分享出来!这个方法还有一个额外的好处,就是可以通过一个Python日志记录器来处理输出,我发现这个问题在StackOverflow上也很常见。

下面是代码:

import sys
import logging
import subprocess
from threading import Thread

logging.basicConfig(stream=sys.stdout,level=logging.INFO)
logging.addLevelName(logging.INFO+2,'STDERR')
logging.addLevelName(logging.INFO+1,'STDOUT')
logger = logging.getLogger('root')

pobj = subprocess.Popen(['python','-c','print 42;bargle'], 
    stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def logstream(stream,loggercb):
    while True:
        out = stream.readline()
        if out:
            loggercb(out.rstrip())       
        else:
            break

stdout_thread = Thread(target=logstream,
    args=(pobj.stdout,lambda s: logger.log(logging.INFO+1,s)))

stderr_thread = Thread(target=logstream,
    args=(pobj.stderr,lambda s: logger.log(logging.INFO+2,s)))

stdout_thread.start()
stderr_thread.start()

while stdout_thread.isAlive() and stderr_thread.isAlive():
     pass

这是输出结果:

STDOUT:root:42
STDERR:root:Traceback (most recent call last):
STDERR:root:  File "<string>", line 1, in <module>
STDERR:root:NameError: name 'bargle' is not defined

你可以把这个子进程的调用换成你想做的任何事情,我只是选择了运行Python,并用一个我知道会同时输出到标准输出和错误输出的命令。关键在于要在不同的线程中分别读取错误输出和标准输出。否则,你可能会在读取一个输出时被阻塞,而另一个输出却有数据可以读取。

67
tsk = subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)

subprocess.STDOUT 是一个特殊的标志,它告诉子进程把所有的错误输出(stderr)都发送到标准输出(stdout),这样就把这两种输出合并在一起了。

顺便提一下,Windows系统的select没有poll()这个功能。subprocess只使用文件句柄的编号,而不调用你的文件输出对象的写入方法。

要捕获输出,可以这样做:

logfile = open(logfilename, 'w')

while tsk.poll() is None:
    line = tsk.stdout.readline()
    logfile.write(line)

撰写回答