Отображение вывода подпроцесса в stdout и перенаправление его

Я запускаю script через модуль подпроцесса Python. В настоящее время я использую:

p = subprocess.Popen('/path/to/script', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = p.communicate()

Затем я печатаю результат на stdout. Это нормально, но поскольку script занимает много времени, я хотел получить вывод в режиме реального времени из script в stdout. Причина, по которой я обрабатываю вывод, заключается в том, что я хочу его проанализировать.

Ответ 1

Чтобы сохранить подпроцесс 'stdout для переменной для дальнейшей обработки и отобразить его, пока дочерний процесс запущен по мере его появления:

#!/usr/bin/env python3
from io import StringIO
from subprocess import Popen, PIPE

with Popen('/path/to/script', stdout=PIPE, bufsize=1,
           universal_newlines=True) as p, StringIO() as buf:
    for line in p.stdout:
        print(line, end='')
        buf.write(line)
    output = buf.getvalue()
rc = p.returncode

Чтобы сохранить как subprocess stdout, так и stderr более сложно, потому что вы должны одновременно использовать оба потока, чтобы избежать тупика:

stdout_buf, stderr_buf = StringIO(), StringIO()
rc =  teed_call('/path/to/script', stdout=stdout_buf, stderr=stderr_buf,
                universal_newlines=True)
output = stdout_buf.getvalue()
...

где teed_call() определяется здесь.


Обновить здесь более простая версия asyncio.


Старая версия:

Здесь однопоточное решение на основе child_process.py пример из tulip:

import asyncio
import sys
from asyncio.subprocess import PIPE

@asyncio.coroutine
def read_and_display(*cmd):
    """Read cmd stdout, stderr while displaying them as they arrive."""
    # start process
    process = yield from asyncio.create_subprocess_exec(*cmd,
            stdout=PIPE, stderr=PIPE)

    # read child stdout/stderr concurrently
    stdout, stderr = [], [] # stderr, stdout buffers
    tasks = {
        asyncio.Task(process.stdout.readline()): (
            stdout, process.stdout, sys.stdout.buffer),
        asyncio.Task(process.stderr.readline()): (
            stderr, process.stderr, sys.stderr.buffer)}
    while tasks:
        done, pending = yield from asyncio.wait(tasks,
                return_when=asyncio.FIRST_COMPLETED)
        assert done
        for future in done:
            buf, stream, display = tasks.pop(future)
            line = future.result()
            if line: # not EOF
                buf.append(line)    # save for later
                display.write(line) # display in terminal
                # schedule to read the next line
                tasks[asyncio.Task(stream.readline())] = buf, stream, display

    # wait for the process to exit
    rc = yield from process.wait()
    return rc, b''.join(stdout), b''.join(stderr)

script запускает команду '/path/to/script и одновременно строит строки как stdout, так и stderr. Строки печатаются на родительский stdout/stderr соответственно и сохраняются как байты для будущей обработки. Чтобы запустить сопрограмму read_and_display(), нам нужен цикл событий:

import os

if os.name == 'nt':
    loop = asyncio.ProactorEventLoop() # for subprocess' pipes on Windows
    asyncio.set_event_loop(loop)
else:
    loop = asyncio.get_event_loop()
try:
    rc, *output = loop.run_until_complete(read_and_display("/path/to/script"))
    if rc:
        sys.exit("child failed with '{}' exit code".format(rc))
finally:
    loop.close()

Ответ 2

p.communicate() ожидает завершения подпроцесса и затем сразу возвращает весь его результат.

Вы пробовали что-то подобное вместо этого, где вы читаете вывод подпроцесса по очереди?

p = subprocess.Popen('/path/to/script', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for line in p.stdout:
  # do something with this individual line
  print line

Ответ 3

В документе Popen.communicate четко указано:

Note: The data read is buffered in memory, so do not use this method if the data size is large or unlimited.

https://docs.python.org/2/library/subprocess.html#subprocess.Popen.communicate

Итак, если вам нужен вывод в реальном времени, вам нужно использовать что-то вроде этого:

stream_p = subprocess.Popen('/path/to/script', stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while stream_line in stream_p:
    #Parse it the way you want
    print stream_line