Объединение stdout и stderr подпроцесса скрипта Python с сохранением их различимости - PullRequest
31 голосов
/ 25 июля 2011

Я бы хотел направить stdout и stdin подпроцесса скрипта Python в один и тот же файл.Что я не знаю, так это как сделать линии из двух источников различимыми?(Например, префикс строки из stderr с восклицательным знаком.)

В моем конкретном случае нет необходимости в оперативном мониторинге подпроцесса, исполняемый скрипт Python может ожидать конца своего выполнения.

Ответы [ 6 ]

43 голосов
/ 03 октября 2011
tsk = subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)

subprocess.STDOUT - это специальный флаг, который указывает подпроцессу направлять весь вывод stderr в stdout, объединяя, таким образом, два ваших потока.

Кстати, select не имеетопрос () в окнах.подпроцесс использует только номер дескриптора файла и не вызывает метод записи объекта вывода файла.

для захвата вывода выполните что-то вроде:

logfile = open(logfilename, 'w')

while tsk.poll() is None:
    line = tsk.stdout.readline()
    logfile.write(line)
12 голосов
/ 28 марта 2012

Мне недавно пришлось столкнуться с этой проблемой, и потребовалось некоторое время, чтобы получить то, что, по моему мнению, работало правильно в большинстве случаев, так что вот оно! (У этого также есть хороший побочный эффект обработки вывода через регистратор 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 командой, которая, как я знал, будет печатать как в stdout, так и в stderr. Ключевой бит читает stderr и stdout каждый в отдельном потоке. В противном случае вы можете заблокировать чтение одного, когда есть данные, готовые для чтения на другом.

9 голосов
/ 25 июля 2011

Если вы хотите чередовать, чтобы получить примерно такой же порядок, как если бы вы запустили процесс в интерактивном режиме, вам нужно сделать то, что делает оболочка, и опросить 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()
2 голосов
/ 29 июля 2012

В настоящее время все остальные ответы не обрабатывают буферизацию на стороне дочернего подпроцесса, если подпроцесс не является скриптом Python, который принимает флаг -u. Смотрите "Q: Почему бы просто не использовать канал (popen ())?" в документации pexpect .

Чтобы смоделировать флаг -u для некоторых программ на основе C stdio (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 :

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 буферизован в интерактивном режиме. Только полные строки читаются. Программа может блокироваться, если в выводе есть незавершенные строки.

1 голос
/ 25 июля 2011

Я предлагаю вам написать свои собственные обработчики, что-то вроде (не проверено, я надеюсь, что вы поймете идею):

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)
0 голосов
/ 25 июля 2011

Вы можете записать stdout / err в файл после выполнения команды.В приведенном ниже примере я использую параметр pickling, поэтому я уверен, что смогу читать без какого-либо конкретного анализа, чтобы различать stdout / err, и в какой-то момент я мог бы указать код выхода и саму команду.

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()
...