Получение результата в реальном времени с использованием подпроцесса

Я пытаюсь написать оболочку script для программы командной строки (svnadmin verify), которая отобразит хороший индикатор прогресса для операции. Это требует, чтобы я мог видеть каждую строку вывода из завернутой программы, как только она выводится.

Я решил, что я просто запустил программу, используя subprocess.Popen, используйте stdout=PIPE, затем прочитайте каждую строку, когда она войдет и действует на нее соответствующим образом. Однако, когда я запускал следующий код, выход, похоже, где-то буферизировался, заставляя его появляться в двух кусках, строки с 1 по 332, а затем с 333 по 439 (последняя строка вывода)

from subprocess import Popen, PIPE, STDOUT

p = Popen('svnadmin verify /var/svn/repos/config', stdout = PIPE, 
        stderr = STDOUT, shell = True)
for line in p.stdout:
    print line.replace('\n', '')

Несколько раз посмотрев документацию по подпроцессу, я обнаружил параметр bufsize до Popen, поэтому я попробовал установить bufsize в 1 (буфера каждой строки) и 0 (без буфера), но ни одно значение не изменилось способ доставки линий.

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

while True:
    try:
        print p.stdout.next().replace('\n', '')
    except StopIteration:
        break

но получил тот же результат.

Возможно ли получить выход программы "в реальном времени" программы, выполненной с использованием подпроцесса? Есть ли еще какой-нибудь другой вариант в Python, совместимый с прямой (не exec*)?

Ответы

Ответ 1

Я попробовал это, и по какой-то причине пока код

for line in p.stdout:
  ...

агрессивно буферизирует, вариант

while True:
  line = p.stdout.readline()
  if not line: break
  ...

не. По-видимому, это известная ошибка: http://bugs.python.org/issue3907 (по состоянию на 29 августа 2018 года проблема закрыта)

Ответ 2

p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1)
for line in iter(p.stdout.readline, b''):
    print line,
p.stdout.close()
p.wait()

Ответ 3

Вы можете попробовать следующее:

import subprocess
import sys

process = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

while True:
    out = process.stdout.read(1)
    if out == '' and process.poll() != None:
        break
    if out != '':
        sys.stdout.write(out)
        sys.stdout.flush()

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

Ответ 4

Вы можете направить вывод подпроцесса непосредственно в потоки. Упрощенный пример:

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)

Ответ 5

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

Ответ 6

Ошибка выхода в реальном времени: Я столкнулся с подобной проблемой в Python, захватив вывод в реальном времени из c-программы. Я добавил " fflush (stdout);" в моем C-коде. Это сработало для меня. Вот отрезок кода

< < C Программа →

#include <stdio.h>
void main()
{
    int count = 1;
    while (1)
    {
        printf(" Count  %d\n", count++);
        fflush(stdout);
        sleep(1);
    }
}

< < Программа Python →

#!/usr/bin/python

import os, sys
import subprocess


procExe = subprocess.Popen(".//count", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

while procExe.poll() is None:
    line = procExe.stdout.readline()
    print("Print:" + line)

< < ВЫВОД → Печать: Количество 1 Печать: граф 2 Печать: граф 3

Надеюсь, что это поможет.

~ Сайрам

Ответ 7

Вы можете использовать итератор над каждым байтом в выводе подпроцесса. Это позволяет встроенное обновление (строки, заканчивающиеся на "\ r", переписывать предыдущую выходную строку) из подпроцесса:

from subprocess import PIPE, Popen

command = ["my_command", "-my_arg"]

# Open pipe to subprocess
subprocess = Popen(command, stdout=PIPE, stderr=PIPE)


# read each byte of subprocess
while subprocess.poll() is None:
    for c in iter(lambda: subprocess.stdout.read(1) if subprocess.poll() is None else {}, b''):
        c = c.decode('ascii')
        sys.stdout.write(c)
sys.stdout.flush()

if subprocess.returncode != 0:
    raise Exception("The subprocess did not terminate correctly.")

Ответ 8

Использование pexpect [http://www.noah.org/wiki/Pexpect] с неблокирующими линиями чтения разрешит эту проблему. Это связано с тем, что трубы буферизуются, и поэтому ваш вывод приложения буферизуется трубой, поэтому вы не можете добраться до этого вывода до заполнения буфера или процесса.

Ответ 9

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

sub_process = subprocess.Popen(my_command, close_fds=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while sub_process.poll() is None:
    out = sub_process.stdout.read(1)
    sys.stdout.write(out)
    sys.stdout.flush()

Ответ 10

Нашел эту функцию "plug-and-play" здесь. Работали как шарм!

import subprocess

def myrun(cmd):
    """from http://blog.kagesenshi.org/2008/02/teeing-python-subprocesspopen-output.html
    """
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    stdout = []
    while True:
        line = p.stdout.readline()
        stdout.append(line)
        print line,
        if line == '' and p.poll() != None:
            break
    return ''.join(stdout)

Ответ 11

В зависимости от варианта использования вы также можете отключить буферизацию в самом подпроцессе.

Если подпроцесс будет процессом Python, вы можете сделать это до вызова:

os.environ["PYTHONUNBUFFERED"] = "1"

Или, в качестве альтернативы, передайте это в аргументе env Popen.

В противном случае, если вы работаете в Linux/Unix, вы можете использовать инструмент stdbuf. Например, как:

cmd = ["stdbuf", "-oL"] + cmd

Смотрите также здесь о stdbuf или других опциях.

(Смотрите также здесь для того же ответа.)

Ответ 12

Полное решение:

import contextlib
import subprocess

# Unix, Windows and old Macintosh end-of-line
newlines = ['\n', '\r\n', '\r']
def unbuffered(proc, stream='stdout'):
    stream = getattr(proc, stream)
    with contextlib.closing(stream):
        while True:
            out = []
            last = stream.read(1)
            # Don't loop forever
            if last == '' and proc.poll() is not None:
                break
            while last not in newlines:
                # Don't loop forever
                if last == '' and proc.poll() is not None:
                    break
                out.append(last)
                last = stream.read(1)
            out = ''.join(out)
            yield out

def example():
    cmd = ['ls', '-l', '/']
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        # Make all end-of-lines '\n'
        universal_newlines=True,
    )
    for line in unbuffered(proc):
        print line

example()

Ответ 13

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

import subprocess
import threading
import Queue

def t_read_stdout(process, queue):
    """Read from stdout"""

    for output in iter(process.stdout.readline, b''):
        queue.put(output)

    return

process = subprocess.Popen(['dir'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT,
                           bufsize=1,
                           cwd='C:\\',
                           shell=True)

queue = Queue.Queue()
t_stdout = threading.Thread(target=t_read_stdout, args=(process, queue))
t_stdout.daemon = True
t_stdout.start()

while process.poll() is None or not queue.empty():
    try:
        output = queue.get(timeout=.5)

    except Queue.Empty:
        continue

    if not output:
        continue

    print(output),

t_stdout.join()

Ответ 14

Подпроцесс Streaming stdin и stdout с asyncio в блоге Python Кевина Маккарти показывает, как это сделать с помощью asyncio:

import asyncio
from asyncio.subprocess import PIPE
from asyncio import create_subprocess_exec


async def _read_stream(stream, callback):
    while True:
        line = await stream.readline()
        if line:
            callback(line)
        else:
            break


async def run(command):
    process = await create_subprocess_exec(
        *command, stdout=PIPE, stderr=PIPE
    )

    await asyncio.wait(
        [
            _read_stream(
                process.stdout,
                lambda x: print(
                    "STDOUT: {}".format(x.decode("UTF8"))
                ),
            ),
            _read_stream(
                process.stderr,
                lambda x: print(
                    "STDERR: {}".format(x.decode("UTF8"))
                ),
            ),
        ]
    )

    await process.wait()


async def main():
    await run("docker build -t my-docker-image:latest .")


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Ответ 15

(Это решение было протестировано с Python 2.7.15)
Вам просто нужно sys.stdout.flush() после каждой строки чтения/записи:

while proc.poll() is None:
    line = proc.stdout.readline()
    sys.stdout.write(line)
    # or print(line.strip()), you still need to force the flush.
    sys.stdout.flush()

Ответ 16

Это то, что я сделал:

def read_command_output(command):
    process = subprocess.Popen(command, 
        stdout=subprocess.PIPE, 
        stderr=subprocess.STDOUT)
    return iter(process.stdout.readline, b"")

for output_line in read_command_output(cmd):
    print(line)

Он непрерывно печатает новые строки, поскольку они выводятся подпроцессом.