Отримання результатів у реальному часі за допомогою підпроцесу


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параметр 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 IO. Ось чому 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. Це особливо корисно для нескінченних результатів
Джордж Плігоропулос


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 замість прочитаного, трапляються випадки, коли вхідне повідомлення не друкується. Спробуйте це за допомогою команди вимагає вбудованого вводу та переконайтесь у цьому самі.


Так, використання readline () зупинить друк (навіть із викликом sys.stdout.flush ())
Марк Ма

3
Це повинно вішатись нескінченно? Я хотів би, щоб дане рішення включало також кодовий код для редагування циклу, коли виконується початковий підпроцес. Вибачте, незалежно від того, скільки разів я заглядаю, підпроцес і так далі я просто не можу працювати.
ThorSummoner

1
Навіщо тестувати на '', коли в Python ми можемо просто використовувати, якщо ні?
Грег Белл

2
це найкраще рішення для тривалої роботи. але він повинен використовувати не None і не! = None. Не слід використовувати! = З None.
Карі

Чи відображається також stderr?
Пітер Вогелаар

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())

це працює з невеликою зміною коду, розміщеного
Jeef

Привіт @Jeef, ти можеш вказати на виправлення, щоб я міг оновити відповідь?
Пабло

Привіт, це працювало для мене, але мені довелося додати наступне, щоб позбутися деяких повідомлень про помилки: import nest_asyncio; nest_asyncio.apply()і використовувати команду shell, тобто process = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)замість process = await create_subprocess_exec(...). Ура!
користувач319436

4

Проблема з вихідним виходом у реальному часі вирішена: у Python я зіткнувся з подібною проблемою, захоплюючи вихід у реальному часі з програми c. Я додав " fflush (stdout) ;" в моєму коді С. Це працювало для мене. Ось фрагмент коду

<< Програма 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

Сподіваюся, це допомагає.

~ sairam


1
Це було єдине, що насправді допомогло. Я використовував той самий код ( flush(stdout)) у C ++. Дякую!
Герхард Хагерер

У мене була така ж проблема із сценарієм python, який викликав інший сценарій python, як підпроцес. На підпроцесових відбитках "flush" був необхідний (print ("привіт", 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

Дивіться також тут про stdbufчи інші варіанти.

(Дивіться також тут і ту ж відповідь.)


2

Я використовував це рішення, щоб отримати вихід у реальному часі на підпроцес. Цей цикл припиняється, як тільки процес завершиться, виключаючи необхідність у заяві про перерву або можливому нескінченному циклі.

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)

1
Додавання stderr=subprocess.STDOUTнасправді дуже допомагає у захопленні потокових даних. Я це закликаю.
хан

1
Основна яловичина тут, мабуть, походить із прийнятої відповіді
трійчатка

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.")

2

У Python 3.x процес може зависати, оскільки вихід є байтовим масивом замість рядка. Переконайтеся, що ви розшифрували його в рядок.

Починаючи з Python 3.6, ви можете зробити це за допомогою параметра encodingв Popen Constructor . Повний приклад:

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 та обробляє їх .


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()

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.