захват stdout в реальном времени из подпроцесса


87

Я хочу subprocess.Popen()запустить rsync.exe в Windows и распечатать стандартный вывод на Python.

Мой код работает, но не отслеживает прогресс, пока не будет выполнена передача файла! Я хочу распечатать прогресс для каждого файла в реальном времени.

Теперь я использую Python 3.1, поскольку я слышал, что он должен лучше обрабатывать ввод-вывод.

import subprocess, time, os, sys

cmd = "rsync.exe -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()


1
(Исходя из Google?) Все PIPEs зайдут в тупик, когда один из буферов PIPE будет заполнен и не прочитан. например, тупик stdout при заполнении stderr. Никогда не передавайте ТРУБУ, которую не собираетесь читать.
— Насер Аль-Вохайби

Может ли кто-нибудь объяснить, почему вы не можете просто установить stdout на sys.stdout вместо subprocess.PIPE?
— Майк,

Ответы:


98

Некоторые практические правила для subprocess.

  • Никогда не используйте shell=True. Он без нужды вызывает дополнительный процесс оболочки для вызова вашей программы.
  • При вызове процессов аргументы передаются в виде списков. sys.argvв Python список, и поэтому argvв С. Таким образом , вы передаете список , чтобы Popenпозвонить подпроцессы, а не строка.
  • Не перенаправляйте stderrна сайт, PIPEкогда вы его не читаете.
  • Не перенаправляйте, stdinкогда вы не пишете на него.

Пример:

import subprocess, time, os, sys
cmd = ["rsync.exe", "-vaz", "-P", "source/" ,"dest/"]

p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT)

for line in iter(p.stdout.readline, b''):
    print(">>> " + line.rstrip())

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

Чтобы проверить это, попробуйте вместо этого запустить следующее:

cmd = [sys.executable, 'test_out.py']

и создадим test_out.pyфайл с содержимым:

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

Выполнение этого подпроцесса должно дать вам «Hello» и подождать 10 секунд, прежде чем дать «World». Если это происходит с кодом Python выше, а не с rsync, это означает, что он rsyncсам буферизует вывод, поэтому вам не повезло.

Решением было бы подключиться напрямую к a pty, используя что-то вроде pexpect.


12
shell=Falseэто правильно, когда вы строите командную строку, особенно из введенных пользователем данных. Но, тем не менее, shell=Trueэто также полезно, когда вы получаете всю командную строку из надежного источника (например, жестко запрограммированного в сценарии).
— Денис Откидач

10
@ Денис Откидач: Я не думаю, что это оправдывает использование shell=True. Подумайте об этом - вы вызываете другой процесс в своей ОС, включающий выделение памяти, использование диска, планирование процессора, просто чтобы разбить строку ! И к одному вы присоединились !! Вы можете разделить на Python, но в любом случае проще писать каждый параметр отдельно. Кроме того , используя список означает , что вы не должны экранировать специальные символы оболочки: пространства, ;, >, <, &.. Ваши параметры могут содержать эти символы , и вы не должны беспокоиться! На shell=Trueсамом деле, я не вижу причин для использования , если только вы не запускаете команду только для оболочки.
— nosklo

nosklo, это должно быть: p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
— Senthil Kumaran

1
@mathtick: я не уверен, почему вы должны выполнять эти операции как отдельные процессы ... вы можете легко вырезать содержимое файла и легко извлекать первое поле в python с помощью csvмодуля. Но в качестве примера ваш конвейер в python будет выглядеть следующим образом: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultОбратите внимание, что теперь, когда оболочка не задействована, вы можете работать с длинными именами файлов и специальными символами оболочки без необходимости экранирования. Кроме того, это намного быстрее, так как на один процесс меньше.
— носкло 04

11
используйте for line in iter(p.stdout.readline, b'')вместо for line in p.stdoutPython 2, иначе строки не читаются в реальном времени, даже если исходный процесс не буферизует свой вывод.
— jfs

41

Я знаю, что это старая тема, но теперь есть решение. Вызовите rsync с опцией --outbuf = L. Пример:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())

3
Это работает, и за него следует проголосовать, чтобы будущие читатели не прокручивали весь диалог выше.
— VectorVictor

1
@VectorVictor Это не объясняет, что происходит и почему это происходит. Может случиться так, что ваша программа работает до тех пор, пока: 1. вы не добавите, preexec_fn=os.setpgrpчтобы программа выжила в родительском скрипте 2. вы пропустили чтение из конвейера процесса 3. процесс выдает много данных, заполняя канал 4. вы застряли на несколько часов , пытаясь выяснить, почему программа, которую вы запускаете, завершает работу через произвольный промежуток времени . Ответ от @nosklo мне очень помог.
— danuker

16

В Linux у меня была такая же проблема с избавлением от буферизации. Наконец, я использовал "stdbuf -o0" (или unbuffer from expect), чтобы избавиться от буферизации PIPE.

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

Затем я мог бы использовать select.select на stdout.

См. Также /unix/25372/


2
Для всех, кто пытается получить стандартный вывод кода C из Python, я могу подтвердить, что это решение было единственным, которое сработало для меня. Для ясности, я говорю о добавлении 'stdbuf', '-o0' в мой существующий список команд в Popen.
— Reckless

Спасибо! stdbuf -o0оказался действительно полезным с кучей тестов pytest / pytest-bdd, которые я написал, которые порождают приложение C ++ и проверяют, что оно генерирует определенные операторы журнала. Без stdbuf -o0них этим тестам потребовалось 7 секунд для получения (буферизованного) вывода из программы C ++. Теперь они запускаются практически мгновенно!
— evadeflow 08

Этот ответ спас меня сегодня! Запуская приложение как подпроцессы pytest, я не мог получить его результат. stdbufИмеет ли это.
— Янош

11

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

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

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

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

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

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

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


1
Вы спасаете мой день, спасибо за PYTHONUNBUFFERED = 1
— diewland

9
for line in p.stdout:
  ...

всегда блокируется до следующего перевода строки.

Для поведения «в реальном времени» вы должны сделать что-то вроде этого:

while True:
  inchar = p.stdout.read(1)
  if inchar: #neither empty string nor None
    print(str(inchar), end='') #or end=None to flush immediately
  else:
    print('') #flush for implicit line-buffering
    break

Цикл while остается, когда дочерний процесс закрывает свой стандартный вывод или завершается. read()/read(-1)будет блокироваться до тех пор, пока дочерний процесс не закроет свой стандартный вывод или не завершится.


1
incharникогда не Noneиспользуется if not inchar:вместо этого ( read()возвращает пустую строку в EOF). кстати, это еще хуже for line in p.stdout, не печатает даже полные строки в реальном времени в Python 2 ( for line in вместо этого можно использовать iter (p.stdout.readline, ') `).
— jfs

1
Я тестировал это с помощью python 3.4 на osx, и он не работает.
— qed

1
@qed: for line in p.stdout:работает на Python 3. Убедитесь, что вы понимаете разницу между ''(строка Unicode) и b''(байтами). См. Python: чтение потокового ввода из subprocess.communicate ()
— jfs

8

Ваша проблема:

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()

сам итератор имеет дополнительную буферизацию.

Попробуйте сделать так:

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

5

Вы не можете заставить stdout печатать без буферизации в канал (если вы не можете переписать программу, которая печатает на stdout), поэтому вот мое решение:

Перенаправьте stdout на sterr, который не буферизуется. '<cmd> 1>&2'должен это сделать. Откройте процесс следующим образом:myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
вы не можете отличить от stdout или stderr, но вы получите весь вывод сразу.

Надеюсь, это поможет любому, кто решит эту проблему.


4
Ты это пробовал? Потому что это не работает ... Если stdout буферизуется в этом процессе, он не будет перенаправлен на stderr так же, как он не перенаправлен на PIPE или файл ..
— Филипе Пина

5
Это совершенно неправильно. Буферизация stdout происходит внутри самой программы. Синтаксис оболочки 1>&2просто изменяет файлы, на которые указывают дескрипторы файлов, перед запуском программы. Сама программа не может различить перенаправление stdout на stderr ( 1>&2) или наоборот ( 2>&1), поэтому это не повлияет на поведение программы при буферизации. В любом случае 1>&2синтаксис интерпретируется оболочкой. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)потерпит неудачу, потому что вы не указали shell=True.
— Will Manley

На случай, если люди будут это читать: я пробовал использовать stderr вместо stdout, он показывает точно такое же поведение.
— martinthenext

3

Измените стандартный вывод процесса rsync на небуферизованный.

p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=0,  # 0=unbuffered, 1=line-buffered, else buffer-size
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

3
Буферизация происходит на стороне rsync, изменение атрибута bufsize на стороне python не поможет.
— nosklo

14
Для всех, кто ищет, ответ nosklo совершенно неверен: отображение прогресса rsync не буферизуется; настоящая проблема заключается в том, что подпроцесс возвращает файловый объект, а интерфейс файлового итератора имеет плохо документированный внутренний буфер даже с bufsize = 0, что требует многократного вызова readline (), если вам нужны результаты до заполнения буфера.
— Крис Адамс

3

Чтобы избежать кеширования вывода, вы можете попробовать pexpect,

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

PS : Я знаю, что этот вопрос довольно старый, но он все еще предлагает решение, которое сработало для меня.

PPS : получил этот ответ из другого вопроса


3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

Я пишу графический интерфейс для rsync на Python и имею те же проблемы. Эта проблема беспокоила меня несколько дней, пока я не нашел ее в pyDoc.

Если для universal_newlines установлено значение True, файловые объекты stdout и stderr открываются как текстовые файлы в универсальном режиме новой строки. Строки могут заканчиваться любым из символов "\ n", соглашения о конце строки в Unix, "\ r", старого соглашения Macintosh, или "\ r \ n", соглашения Windows. Все эти внешние представления отображаются программой Python как '\ n'.

Кажется, что rsync будет выводить '\ r', когда выполняется перевод.


1

Я заметил, что нет упоминания об использовании временного файла в качестве промежуточного. Следующее позволяет обойти проблемы с буферизацией путем вывода во временный файл и позволяет анализировать данные, поступающие из rsync, без подключения к pty. Я тестировал следующее на компьютере с Linux, и вывод rsync имеет тенденцию различаться на разных платформах, поэтому регулярные выражения для анализа вывода могут отличаться:

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...

это не в реальном времени. Файл не решает проблему с буферизацией на стороне rsync.
— jfs

tempfile.TemporaryFile может удалять себя для облегчения очистки в случае исключений
— jfs

3
while not p.poll()приводит к бесконечному циклу, если подпроцесс успешно завершается с 0, используйте p.poll() is Noneвместо этого
— jfs

Windows может запретить открывать уже открытый файл, поэтому open(file_name)может выйти из строя
— jfs

1
Я только что нашел этот ответ, к сожалению, только для Linux, но работает как очаровательная ссылка. Поэтому я просто расширяю свою команду следующим образом: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argvи вызываю: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) и теперь я могу читать без какой-либо буферизации
— Арвид Терзибашян

0

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

input = 'path/input_file.mp4'
output = 'path/input_file.mp4'
command = "ffmpeg -y -v quiet -stats -i \"" + str(input) + "\" -metadata title=\"@alaa_sanatisharif\" -preset ultrafast -vcodec copy -r 50 -vsync 1 -async 1 \"" + output + "\""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
for line in self.process.stdout:
    reg = re.search('\d\d:\d\d:\d\d', line)
    ffmpeg_time = reg.group(0) if reg else ''
    print(ffmpeg_time)

-1

В Python 3 есть решение, которое выводит команду из командной строки и доставляет красиво декодированные строки в реальном времени по мере их получения.

Получатель ( receiver.py):

import subprocess
import sys

cmd = sys.argv[1:]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in p.stdout:
    print("received: {}".format(line.rstrip().decode("utf-8")))

Пример простой программы, которая может генерировать вывод в реальном времени ( dummy_out.py):

import time
import sys

for i in range(5):
    print("hello {}".format(i))
    sys.stdout.flush()  
    time.sleep(1)

Выход:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.