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

135

Я пытаюсь написать сценарий оболочки для программы командной строки (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параметр to Popen, поэтому я попытался установить bufsize на 1 (буферизовать каждую строку) и 0 (без буфера), но ни одно значение, похоже, не изменило способ доставки строк.

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

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

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

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

Крис Либ
источник
1
Вы пробовали опустить, sydout=PIPEчтобы подпроцесс записывал прямо на вашу консоль, минуя родительский процесс?
S.Lott
5
Дело в том, что я хочу прочитать вывод. Если он выводится прямо на консоль, как я могу это сделать? Кроме того, я не хочу, чтобы пользователь видел вывод завернутой программы, только мой вывод.
Крис Либ,
Тогда почему дисплей «в реальном времени»? Я не понимаю варианта использования.
S.Lott
8
Не используйте shell = True. Он излишне вызывает вашу оболочку. Вместо этого используйте p = Popen (['svnadmin', 'verify', '/ var / svn / repos / config'], stdout = PIPE, stderr = STDOUT)
nosklo
2
@ S.Lott По сути, svnadmin verify печатает строку вывода для каждой проверенной ревизии. Я хотел сделать хороший индикатор прогресса, который не вызывал бы чрезмерных объемов вывода. Что-то вроде wget, например
Крис Либ

Ответы:

82

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

for line in p.stdout:
  ...

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

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

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

Дейв
источник
Это не единственный беспорядок в старых реализациях ввода-вывода Python. Вот почему Py2.6 и Py3k получили совершенно новую библиотеку ввода-вывода.
Тим Лин
3
Этот код сломается, если подпроцесс вернет пустую строку. Лучшим решением было бы использовать while p.poll() is Noneвместо while Trueи удалитьif not line
exhuma
6
@exhuma: отлично работает. readline возвращает "\ n" в пустой строке, что не соответствует истине. он возвращает только пустую строку, когда канал закрывается, что будет, когда подпроцесс завершится.
Элис Перселл
1
@Dave Для будущего использования: напечатайте строки utf-8 в py2 + с помощью print(line.decode('utf-8').rstrip()).
Джонатан Комар,
3
Кроме того, для реального чтения результатов процесса в реальном времени вам нужно будет сообщить python, что вам НЕ нужна буферизация. Дорогой Python, просто дайте мне результат напрямую. А вот как: вам нужно установить переменную окружения PYTHONUNBUFFERED=1. Это особенно полезно для бесконечных выходов
Джордж Плигоропулос
38
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1)
for line in iter(p.stdout.readline, b''):
    print line,
p.stdout.close()
p.wait()
Кори Голдберг
источник
1
@nbro наверное потому что p.stdout.close()непонятно.
анатолий техтоник
1
@nbro, вероятно, потому что код был дан без объяснения причин ...: /
Аарон Холл
3
Что это за б ''?
ManuelSchneid3r
29

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

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)
Эйдан Фельдман
источник
Позволяет ли это получить содержимое постфактум .communicate()? Или содержимое потеряно для родительских потоков stderr / stdout?
theferrit32
Нет, communicate()для возвращаемого метода нет метода CompletedProcess. Также capture_outputявляется взаимоисключающим с stdoutи stderr.
Эйдан Фельдман
20

Вы можете попробовать это:

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 вместо read, в некоторых случаях входное сообщение не будет распечатано. Попробуйте выполнить это с помощью команды, требующей встроенного ввода, и убедитесь в этом сами.

Надя Алрамли
источник
Да, использование readline () остановит печать (даже при вызове sys.stdout.flush ())
Марк Ма
3
Это должно висеть бесконечно? Я бы хотел, чтобы данное решение также включало шаблонный код для редактирования цикла после завершения начального подпроцесса. Извини, сколько бы раз я это ни изучал, подпроцесс и т.д. - это то, что я просто не могу заставить работать.
ThorSummoner
1
Зачем проверять "", когда в Python мы можем просто использовать if not out?
Грег Белл
2
это лучшее решение для длительных работ. но следует использовать не None и не! = None. Вы не должны использовать! = С None.
Кари,
Stderr также отображается этим?
Pieter Vogelaar
7

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())
Pablo
источник
это работает с небольшими изменениями в опубликованном коде
Jeef
Привет, @Jeef, можешь указать на исправление, чтобы я мог обновить ответ?
Пабло
Привет, это сработало для меня, но мне пришлось добавить следующее, чтобы избавиться от некоторых сообщений об ошибках: import nest_asyncio; nest_asyncio.apply()и использовать команду оболочки, т.е. process = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)вместо process = await create_subprocess_exec(...). Ура!
user319436
4

Устранена проблема вывода в реальном времени: я столкнулся с аналогичной проблемой в 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

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

~ Сайрам

Сайрам
источник
1
Это было единственное, что действительно помогло. Я использовал тот же код ( flush(stdout)) на C ++. Спасибо!
Герхард Хагерер
У меня была та же проблема с сценарием python, вызывающим другой сценарий python в качестве подпроцесса. Для распечаток подпроцесса был необходим «flush» (print («hello», flush = True) в python 3). Кроме того, существует множество примеров (2020) python 2, это python 3, поэтому +1
smajtkst
3

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

Эли Кортрайт
источник
3

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

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

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

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

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

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

См. Также здесь about stdbufили другие варианты.

(См. Также здесь тот же ответ.)

Альберт
источник
2

Я использовал это решение для получения вывода в реальном времени в подпроцессе. Этот цикл остановится, как только процесс завершится, исключая необходимость в инструкции 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()
Джейсон Хедлунд
источник
5
возможно ли, что это приведет к выходу из цикла без пустого буфера stdout?
jayjay
Я много искал подходящий ответ, который не зависал после завершения! Я нашел это как решение, добавив if out=='': breakпослеout = sub_process...
Sos
2

Найдено эту функцию «подключи и работай» здесь . Работал как шарм!

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)
Deena
источник
1
Добавление stderr=subprocess.STDOUTдействительно очень помогает при захвате потоковых данных. Я поддерживаю это.
хан
1
Основная проблема здесь, кажется, исходит из принятого ответа
tripleee
2

Вы можете использовать итератор для каждого байта на выходе подпроцесса. Это позволяет встроенное обновление (строки, заканчивающиеся на '\ 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.")
rhyno183
источник
2

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

Начиная с Python 3.6 это можно сделать с помощью параметра encodingв конструкторе Popen . Полный пример:

process = subprocess.Popen(
    'my_command',
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    shell=True,
    encoding='utf-8',
    errors='replace'
)

while True:
    realtime_output = process.stdout.readline()

    if realtime_output == '' and process.poll() is not None:
        break

    if realtime_output:
        print(realtime_output.strip(), flush=True)

Обратите внимание , что этот код редиректов stderr на stdoutи ручки вывода ошибок .

pavelnazimok
источник
1

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

Гейб
источник
0

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

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()
Андрес Рестрепо
источник
1
Поскольку вы используете universal_newlines=Trueих для Popen()вызова, вам, вероятно, не нужно также вводить собственную обработку их - в этом весь смысл опции.
Мартино
1
это кажется ненужным сложным. Это не решает проблемы с буферизацией. Смотрите ссылки в моем ответе .
jfs
Это единственный способ получить информацию о ходе выполнения rsync в реальном времени (- outbuf = L)! спасибо
Mohammadhzp
0

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

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()
Badslacks
источник
0

(Это решение было протестировано с 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()
Дан
источник
0

Несколько ответов, предлагающих python 3.x или pthon 2.x, код ниже будет работать для обоих.

 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,)
    stdout = []
    while True:
        line = p.stdout.readline()
        if not isinstance(line, (str)):
            line = line.decode('utf-8')
        stdout.append(line)
        print (line)
        if (line == '' and p.poll() != None):
            break
Đại
источник