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


87

Я хочу subprocess.Popen()rsync.exe у Windows та надрукувати stdout у Python.

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

Зараз я використовую Python 3.1, оскільки я чув, що це має бути кращим в обробці IO.

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?) Всі PIPE заблокуються, коли один із буферів PIPE заповниться і не буде прочитаний. наприклад stdout тупик, коли stderr заповнений. Ніколи не передавайте ТРУБУ, яку ви не збираєтеся читати.
Нассер Аль-Вохайбі

Хтось може пояснити, чому ви не можете просто встановити stdout для sys.stdout замість subprocess.PIPE?
Майк

Відповіді:


96

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

Щоб перевірити це, спробуйте замість цього запустити:

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

і створити test_out.pyфайл із вмістом:

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

Виконання цього підпроцесу має дати вам "Привіт" і зачекати 10 секунд, перш ніж дати "Світ". Якщо це трапляється з наведеним вище кодом python, а не з rsync, це означає, що rsyncсаме воно буферизує вихід, тому вам не пощастило.

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


12
shell=Falseце правильно, коли ви створюєте командний рядок, особливо з введених користувачем даних. Але тим shell=Trueне менш це також корисно, коли ви отримуєте весь командний рядок із надійного джерела (наприклад, жорстко закодованого в сценарії).
Денис Откідах

10
@Denis Otkidach: Я не думаю, що це вимагає використання shell=True. Подумайте - ви викликаєте інший процес у вашій ОС, що включає виділення пам'яті, використання диска, планування процесора, просто для того, щоб розділити рядок ! І до одного ви приєдналися !! Ви можете розділити python, але все одно простіше писати кожен параметр окремо. Крім того , використовуючи список означає , що ви не повинні екранувати спеціальні символи оболонки: простору, ;, >, <, &.. Ваші параметри можуть містити ці символи , і ви не повинні турбуватися! Я не бачу причини використовувати shell=Trueнасправді, якщо тільки ви не виконуєте команду лише для оболонки.
nosklo

nosklo, це має бути: p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
Сентіл Кумаран,

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

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

15

У Linux у мене була та ж проблема позбутися буферизації. Нарешті я використав "stdbuf -o0" (або, скасуйте буфер від очікуваного), щоб позбутися буферизації PIPE.

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

Потім я міг би використовувати select.select на stdout.

Див. Також /unix/25372/


2
Для тих, хто намагається захопити код коду C з Python, я можу підтвердити, що це рішення було єдиним, що працювало для мене. Щоб бути зрозумілим, я говорю про додавання 'stdbuf', '-o0' до мого існуючого списку команд у Popen.
Нерозсудливий

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

11

Залежно від випадку використання, можливо, ви також захочете відключити буферизацію в самому підпроцесі.

Якщо підпроцес буде процесом Python, ви можете зробити це перед викликом:

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

Або ж передайте це в envаргументі на Popen.

В іншому випадку, якщо ви працюєте на Linux / Unix, ви можете скористатися цим stdbufінструментом. Наприклад, як:

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

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


1
Ви рятуєте мій день, Дякую за PYTHONUNBUFFERED = 1
Діуленд

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


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

Змініть stdout із процесу 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 (), якщо вам потрібні результати до заповнення буфера.
Chris Adams

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.