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

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

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

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

3 ответа

Решение

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

#!/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

Сохранить как 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's stdout, stderr while displaying them as they arrive."""
    # start process
    process = yield from asyncio.create_subprocess_exec(*cmd,
            stdout=PIPE, stderr=PIPE)

    # read child's 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)

Скрипт работает '/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()

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

Документ 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

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

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

Это выводит на терминал как stdout, так и stderr, а также сохраняет как stdout, так и stderr в переменную:

from subprocess import Popen, PIPE, STDOUT

with Popen(args, stdout=PIPE, stderr=STDOUT, text=True, bufsize=1) as p:
    output = "".join([print(buf, end="") or buf for buf in p.stdout])

Однако, в зависимости от того, что именно вы делаете, может быть важно отметить следующее: Используя stderr=STDOUT, мы больше не можем различать stdout и stderr и с вызовом print, ваш вывод всегда будет выводиться на стандартный вывод, независимо от того, поступил он из стандартного вывода или стандартного вывода.

Для Python < 3.7 вам нужно будет использовать universal_newlines вместо того text.

Новое в версии 3.7: текст был добавлен как более читаемый псевдоним для universal_newlines.

Источник: https://docs.python.org/3/library/subprocess.html

Другие вопросы по тегам