захват 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)будет блокироваться до тех пор, пока дочерний процесс не закроет свой стандартный вывод или не завершится.

IBue
источник
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
Zviadm
источник
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', когда выполняется перевод.

xmc
источник
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...
MikeGM
источник
это не в реальном времени. Файл не решает проблему с буферизацией на стороне 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
Watsonic
источник