合并Python脚本的子进程'stdout和stderr,同时保持它们可区分

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了合并Python脚本的子进程'stdout和stderr,同时保持它们可区分相关的知识,希望对你有一定的参考价值。

我想将python脚本的子进程'stdout和stdin指向同一个文件。我不知道的是如何使两个来源的线条区分开来? (例如,带有感叹号的stderr行前缀。)

在我的特定情况下,不需要对子进程进行实时监视,执行的Python脚本可以等待其执行结束。

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

subprocess.STDOUT是一个特殊的标志,告诉subprocess将所有stderr输出路由到stdout,从而组合你的两个流。

顺便说一句,select在Windows中没有poll()。 subprocess仅使用文件句柄号,而不调用文件输出对象的write方法。

捕获输出,执行以下操作:

logfile = open(logfilename, 'w')

while tsk.poll() is None:
    line = tsk.stdout.readline()
    logfile.write(line)
另一答案

我发现自己最近必须解决这个问题,并且在大多数情况下需要一段时间才能得到我认为正常工作的东西,所以这就是它! (它还具有通过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

您可以替换子进程调用以执行您想要的任何操作,我只选择使用我知道将打印到stdout和stderr的命令运行python。关键位是在一个单独的线程中读取stderr和stdout。否则,当有数据准备好在另一个上读取时,您可能会阻止读取一个。

另一答案

如果要交错以获得与以交互方式运行进程时大致相同的顺序,则需要执行shell所执行的操作并轮询stdin / stdout并按其轮询的顺序进行写入。

这里有一些代码可以按照您的需要执行某些操作 - 在这种情况下,将stdout / stderr发送到记录器信息/错误流。

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()
另一答案

目前,如果子进程不是接受-u标志的Python脚本,则所有其他答案都不会处理子子进程侧的缓冲。见"Q: Why not just use a pipe (popen())?" in the pexpect documentation

要模拟一些基于C stdio(-u)程序的FILE*标志,你可以试试stdbuf

如果忽略这一点,那么输出将无法正确交错,可能如下所示:

stderr
stderr
...large block of stdout including parts that are printed before stderr...

您可以使用以下客户端程序尝试它,注意与/没有-u标志的差异(['stdbuf', '-o', 'L', 'child_program']也修复了输出):

#!/usr/bin/env python
from __future__ import print_function
import random
import sys
import time
from datetime import datetime

def tprint(msg, file=sys.stdout):
    time.sleep(.1*random.random())
    print("%s %s" % (datetime.utcnow().strftime('%S.%f'), msg), file=file)

tprint("stdout1 before stderr")
tprint("stdout2 before stderr")
for x in range(5):
    tprint('stderr%d' % x, file=sys.stderr)
tprint("stdout3 after stderr")

在Linux上你可以使用pty来获得与子进程以交互方式运行时相同的行为,例如,这是一个修改过的@T.Rojan's answer

import logging, os, select, subprocess, sys, pty

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

master_fd, slave_fd = pty.openpty()
p = subprocess.Popen(args,stdout=slave_fd, stderr=subprocess.PIPE, close_fds=True)
with os.fdopen(master_fd) as stdout:
    poll = select.poll()
    poll.register(stdout, select.POLLIN)
    poll.register(p.stderr,select.POLLIN | select.POLLHUP)

    def cleanup(_done=[]):
        if _done: return
        _done.append(1)
        poll.unregister(p.stderr)
        p.stderr.close()
        poll.unregister(stdout)
        assert p.poll() is not None

    read_write = {stdout.fileno(): (stdout.readline, logger.info),
                  p.stderr.fileno(): (p.stderr.readline, logger.error)}
    while True:
        events = poll.poll(40) # poll with a small timeout to avoid both
                               # blocking forever and a busy loop
        if not events and p.poll() is not None:
            # no IO events and the subprocess exited
            cleanup()
            break

        for fd, event in events:
            if event & select.POLLIN: # there is something to read
                read, write = read_write[fd]
                line = read()
                if line:
                    write(line.rstrip())
            elif event & select.POLLHUP: # free resources if stderr hung up
                cleanup()
            else: # something unexpected happened
                assert 0
sys.exit(p.wait()) # return child's exit code

它假定stderr始终是无缓冲/行缓冲的,并且stdout在交互模式下是行缓冲的。只读全行。如果输出中存在非终止行,程序可能会阻止。

另一答案

我建议你编写自己的处理程序,类似于(未经测试,我希望你能抓住这个想法):

class my_buffer(object):
    def __init__(self, fileobject, prefix):
        self._fileobject = fileobject
        self.prefix = prefix
    def write(self, text):
        return self._fileobject.write('%s %s' % (self.prefix, text))
    # delegate other methods to fileobject if necessary

log_file = open('log.log', 'w')
my_out = my_buffer(log_file, 'OK:')
my_err = my_buffer(log_file, '!!!ERROR:')
p = subprocess.Popen(command, stdout=my_out, stderr=my_err, shell=True)
另一答案

您可以在命令执行后将stdout / err写入文件。在下面的例子中,我使用酸洗,所以我确信我能够在没有任何特定解析的情况下阅读以区分stdout / err,并且在某些时候我可以使用exitcode和命令本身。

import subprocess
import cPickle

command = 'ls -altrh'
outfile = 'log.errout'
pipe = subprocess.Popen(command, stdout = subprocess.PIPE,
                        stderr = subprocess.PIPE, shell = True)
stdout, stderr = pipe.communicate()

f = open(outfile, 'w')
cPickle.dump({'out': stdout, 'err': stderr},f)
f.close()

以上是关于合并Python脚本的子进程'stdout和stderr,同时保持它们可区分的主要内容,如果未能解决你的问题,请参考以下文章

管道时在python的子进程模块中使用stdout.close()

Go 子进程通信

如何异步获取子进程'stdout数据?

如何在 python 中的子进程调用期间阻止 stdout 显示我的密码? [复制]

在没有flush()和新行的子进程输出上进行非阻塞读取

没有STDOUT的两个python脚本之间的进程间通信