Python считывает из подпроцесса stdout и stderr отдельно при сохранении порядка

У меня есть подпроцесс python, из которого я пытаюсь читать потоки вывода и ошибок. В настоящее время у меня это работает, но я могу читать только stderr после того, как я закончил читать с stdout. Вот как это выглядит:

process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout_iterator = iter(process.stdout.readline, b"")
stderr_iterator = iter(process.stderr.readline, b"")

for line in stdout_iterator:
    # Do stuff with line
    print line

for line in stderr_iterator:
    # Do stuff with line
    print line

Как вы можете видеть, цикл stderr for не может начинаться до тех пор, пока цикл цикла stdout не завершится. Как я могу изменить это, чтобы иметь возможность читать из обоих в правильном порядке, в которые входят строки?

Чтобы уточнить: Мне все еще нужно узнать, появилась ли строка из stdout или stderr, потому что в моем коде будут обрабатываться по-разному.

Ответы

Ответ 1

Код в вашем вопросе может зайти в тупик, если дочерний процесс производит достаточный вывод на stderr (~ 100 КБ на моей машине Linux).

Существует метод communicate(), который позволяет читать отдельно как stdout, так и stderr:

from subprocess import Popen, PIPE

process = Popen(command, stdout=PIPE, stderr=PIPE)
output, err = process.communicate()

Если вам нужно прочитать потоки, пока дочерний процесс все еще запущен, портативное решение должно использовать потоки (не проверены):

from subprocess import Popen, PIPE
from threading import Thread
from Queue import Queue # Python 2

def reader(pipe, queue):
    try:
        with pipe:
            for line in iter(pipe.readline, b''):
                queue.put((pipe, line))
    finally:
        queue.put(None)

process = Popen(command, stdout=PIPE, stderr=PIPE, bufsize=1)
q = Queue()
Thread(target=reader, args=[process.stdout, q]).start()
Thread(target=reader, args=[process.stderr, q]).start()
for _ in range(2):
    for source, line in iter(q.get, None):
        print "%s: %s" % (source, line),

См:

Ответ 2

Порядок записи данных в разные каналы теряется после записи.

Невозможно определить, было ли stdout написано до stderr.

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

Эта программа должна продемонстрировать это:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import select
import subprocess

testapps={
    'slow': '''
import os
import time
os.write(1, 'aaa')
time.sleep(0.01)
os.write(2, 'bbb')
time.sleep(0.01)
os.write(1, 'ccc')
''',
    'fast': '''
import os
os.write(1, 'aaa')
os.write(2, 'bbb')
os.write(1, 'ccc')
''',
    'fast2': '''
import os
os.write(1, 'aaa')
os.write(2, 'bbbbbbbbbbbbbbb')
os.write(1, 'ccc')
'''
}

def readfds(fds, maxread):
    while True:
        fdsin, _, _ = select.select(fds,[],[])
        for fd in fdsin:
            s = os.read(fd, maxread)
            if len(s) == 0:
                fds.remove(fd)
                continue
            yield fd, s
        if fds == []:
            break

def readfromapp(app, rounds=10, maxread=1024):
    f=open('testapp.py', 'w')
    f.write(testapps[app])
    f.close()

    results={}
    for i in range(0, rounds):
        p = subprocess.Popen(['python', 'testapp.py'], stdout=subprocess.PIPE
                                                     , stderr=subprocess.PIPE)
        data=''
        for (fd, s) in readfds([p.stdout.fileno(), p.stderr.fileno()], maxread):
            data = data + s
        results[data] = results[data] + 1 if data in results else 1

    print 'running %i rounds %s with maxread=%i' % (rounds, app, maxread)
    results = sorted(results.items(), key=lambda (k,v): k, reverse=False)
    for data, count in results:
        print '%03i x %s' % (count, data)


print
print "=> if output is produced slowly this should work as whished"
print "   and should return: aaabbbccc"
readfromapp('slow',  rounds=100, maxread=1024)

print
print "=> now mostly aaacccbbb is returnd, not as it should be"
readfromapp('fast',  rounds=100, maxread=1024)

print
print "=> you could try to read data one by one, and return"
print "   e.g. a whole line only when LF is read"
print "   (b should be finished before c's)"
readfromapp('fast',  rounds=100, maxread=1)

print
print "=> but even this won't work ..."
readfromapp('fast2', rounds=100, maxread=1)

и выводит что-то вроде этого:

=> if output is produced slowly this should work as whished
   and should return: aaabbbccc
running 100 rounds slow with maxread=1024
100 x aaabbbccc

=> now mostly aaacccbbb is returnd, not as it should be
running 100 rounds fast with maxread=1024
006 x aaabbbccc
094 x aaacccbbb

=> you could try to read data one by one, and return
   e.g. a whole line only when LF is read
   (b should be finished before c's)
running 100 rounds fast with maxread=1
003 x aaabbbccc
003 x aababcbcc
094 x abababccc

=> but even this won't work ...
running 100 rounds fast2 with maxread=1
003 x aaabbbbbbbbbbbbbbbccc
001 x aaacbcbcbbbbbbbbbbbbb
008 x aababcbcbcbbbbbbbbbbb
088 x abababcbcbcbbbbbbbbbb

Ответ 3

Вы можете направить stderr на STDOUT:

process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

Ответ 4

Я написал что-то, чтобы сделать это давным-давно. Я еще не портировал его на Python 3, но это не должно быть слишком сложно (исправлены патчи!)

Если вы запустите его автономно, вы увидите множество различных опций. В любом случае, это позволяет отличать stdout от stderr.

Ответ 5

Я знаю, что этот вопрос очень старый, но этот ответ может помочь другим, кто наткнется на эту страницу, в поиске решения для аналогичной ситуации, поэтому я все равно публикую его.

Я построил простой фрагмент кода Python, который объединит любое количество каналов в один. Конечно, как указано выше, порядок не может быть гарантирован, но это настолько близко, насколько я думаю, вы можете получить в Python.

Он порождает поток для каждого из каналов, читает их построчно и помещает их в очередь (FIFO). Основной поток проходит по очереди, приводя к каждой строке.

import threading, queue
def merge_pipes(**named_pipes):
    r'''
    Merges multiple pipes from subprocess.Popen (maybe other sources as well).
    The keyword argument keys will be used in the output to identify the source
    of the line.

    Example:
    p = subprocess.Popen(['some', 'call'],
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    outputs = {'out': log.info, 'err': log.warn}
    for name, line in merge_pipes(out=p.stdout, err=p.stderr):
        outputs[name](line)

    This will output stdout to the info logger, and stderr to the warning logger
    '''

    # Constants. Could also be placed outside of the method. I just put them here
    # so the method is fully self-contained
    PIPE_OPENED=1
    PIPE_OUTPUT=2
    PIPE_CLOSED=3

    # Create a queue where the pipes will be read into
    output = queue.Queue()

    # This method is the run body for the threads that are instatiated below
    # This could be easily rewritten to be outside of the merge_pipes method,
    # but to make it fully self-contained I put it here
    def pipe_reader(name, pipe):
        r"""
        reads a single pipe into the queue
        """
        output.put( ( PIPE_OPENED, name, ) )
        try:
            for line in iter(pipe.readline,''):
                output.put( ( PIPE_OUTPUT, name, line.rstrip(), ) )
        finally:
            output.put( ( PIPE_CLOSED, name, ) )

    # Start a reader for each pipe
    for name, pipe in named_pipes.items():
        t=threading.Thread(target=pipe_reader, args=(name, pipe, ))
        t.daemon = True
        t.start()

    # Use a counter to determine how many pipes are left open.
    # If all are closed, we can return
    pipe_count = 0

    # Read the queue in order, blocking if there no data
    for data in iter(output.get,''):
        code=data[0]
        if code == PIPE_OPENED:
            pipe_count += 1
        elif code == PIPE_CLOSED:
            pipe_count -= 1
        elif code == PIPE_OUTPUT:
            yield data[1:]
        if pipe_count == 0:
            return

Ответ 6

Это работает для меня (на окнах): https://github.com/waszil/subpiper

from subpiper import subpiper

def my_stdout_callback(line: str):
    print(f'STDOUT: {line}')

def my_stderr_callback(line: str):
    print(f'STDERR: {line}')

my_additional_path_list = [r'c:\important_location']

retcode = subpiper(cmd='echo magic',
                   stdout_callback=my_stdout_callback,
                   stderr_callback=my_stderr_callback,
                   add_path_list=my_additional_path_list)

Ответ 7

Здесь решение, основанное на selectors, но такое, которое сохраняет порядок и передает символы переменной длины (даже одиночные символы).

Хитрость заключается в том, чтобы использовать read1() вместо read().

import selectors
import subprocess
import sys

p = subprocess.Popen(
    "python random_out.py", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

sel = selectors.DefaultSelector()
sel.register(p.stdout, selectors.EVENT_READ)
sel.register(p.stderr, selectors.EVENT_READ)

while True:
    for key, _ in sel.select():
        data = key.fileobj.read1().decode()
        if not data:
            exit()
        if key.fileobj is p.stdout:
            print(data, end="")
        else:
            print(data, end="", file=sys.stderr)

Если вы хотите тестовую программу, используйте это.

import sys
from time import sleep


for i in range(10):
    print(f" x{i} ", file=sys.stderr, end="")
    sleep(0.1)
    print(f" y{i} ", end="")
    sleep(0.1)

Ответ 8

Как объяснено в учебнике python

Вы можете использовать stderr=subprocess.STDOUT

В разделе часто задаваемых вопросов в вышеупомянутом уроке вы можете увидеть этот параграф

stdin, stdout и stderr определяют исполняемые стандартные программы вход, стандартный вывод и стандартные файлы файлов ошибок соответственно. Допустимыми значениями являются PIPE, существующий файловый дескриптор (положительный целое), существующий файловый объект и None. PIPE указывает, что новый труба к ребенку должна быть создана. С настройками по умолчанию Нет, перенаправление не произойдет; файлы файлов childs будут унаследованный от родителя. Кроме того, stderr может быть STDOUT, который указывает, что данные stderr из дочернего процесса должны быть захвачен в тот же дескриптор файла, что и для stdout.

Ответ 9

В соответствии с документом python

Popen.stdout Если аргументом stdout был PIPE, этот атрибут является файловым объектом, который обеспечивает вывод из дочернего процесса. В противном случае это None.

Popen.stderr Если аргументом stderr был PIPE, этот атрибут является файловым объектом, который обеспечивает вывод ошибок из дочернего процесса. В противном случае это None.

Ниже образец может делать то, что вы хотите

test.py

print "I'm stdout"

raise Exception("I'm Error")

printer.py

import subprocess

p = subprocess.Popen(['python', 'test.py'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

print "Normal"
std_lines = p.stdout.readlines()
for line in std_lines:
    print line.rstrip()

print "Error"
stderr_lines = p.stderr.readlines()
for line in stderr_lines:
    print line.rstrip()

Вывод:

Normal
I'm stdout

Error
Traceback (most recent call last):
  File "test.py", line 3, in <module>
    raise Exception("I'm Error")
Exception: I'm Error