читать подпроцесс stdout построчно

235

Мой скрипт на python использует подпроцесс для вызова очень шумной утилиты linux. Я хочу сохранить весь вывод в файл журнала и показать некоторые из них пользователю. Я думал, что следующее будет работать, но вывод не будет отображаться в моем приложении, пока утилита не выдаст значительный объем вывода.

#fake_utility.py, just generates lots of output over time
import time
i = 0
while True:
   print hex(i)*512
   i += 1
   time.sleep(0.5)

#filters output
import subprocess
proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)
for line in proc.stdout:
   #the real code does filtering here
   print "test:", line.rstrip()

Я действительно хочу, чтобы скрипт фильтра печатал каждую строку, полученную от подпроцесса. Сорт, как то, что teeделает, но с кодом Python.

Чего мне не хватает? Это вообще возможно?


Обновить:

Если a sys.stdout.flush()добавлено в fake_utility.py, код имеет желаемое поведение в Python 3.1. Я использую Python 2.6. Вы можете подумать, что использование proc.stdout.xreadlines()будет работать так же, как py3k, но это не так.


Обновление 2:

Вот минимальный рабочий код.

#fake_utility.py, just generates lots of output over time
import sys, time
for i in range(10):
   print i
   sys.stdout.flush()
   time.sleep(0.5)

#display out put line by line
import subprocess
proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)
#works in python 3.0+
#for line in proc.stdout:
for line in iter(proc.stdout.readline,''):
   print line.rstrip()
deft_code
источник
4
Вы можете использовать print line,вместо print line.rstrip()(примечание: запятая в конце).
Jfs
2
В обновлении 2 говорится, что он работает с python 3.0+, но использует старый оператор print, поэтому он не работает с python 3.0+.
Новичок
Ни один из перечисленных здесь ответов не сработал для меня, но stackoverflow.com/questions/5411780/… сработал !
штучной упаковке

Ответы:

179

Прошло много времени с тех пор, как я в последний раз работал с Python, но я думаю, что проблема в утверждении for line in proc.stdout, которое читает весь ввод перед его повторением. Решение состоит в том, чтобы использовать readline()вместо:

#filters output
import subprocess
proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)
while True:
  line = proc.stdout.readline()
  if not line:
    break
  #the real code does filtering here
  print "test:", line.rstrip()

Конечно, вам все равно придется иметь дело с буферизацией подпроцесса.

Примечание: согласно документации решение с итератором должно быть эквивалентно использованию readline(), за исключением буфера упреждающего чтения, но (или именно из-за этого) предлагаемое изменение действительно дало для меня другие результаты (Python 2.5 в Windows XP).

Ромуло Цеккон
источник
11
для file.readline()vs. for line in fileсм bugs.python.org/issue3907 (короче: он работает на Python3, использование io.open()на Python 2.6+)
JFS
5
Более питонным тестом для EOF, согласно «Рекомендациям по программированию» в PEP 8 ( python.org/dev/peps/pep-0008 ), было бы «если не строка:».
Джейсон Мок
14
@naxa: для труб: for line in iter(proc.stdout.readline, ''):.
JFS
3
@ Ян-ФилиппГерк: да. 1. вы можете использовать for line in proc.stdoutна Python 3 (нет ошибки опережающего чтения) 2. '' != b''на Python 3 - не копируйте и не вставляйте код вслепую - подумайте, что он делает и как он работает.
Jfs
2
@JFSebastian: конечно, iter(f.readline, b'')решение довольно очевидно (а также работает на Python 2, если кому-то интересно). Смысл моего комментария состоял не в том, чтобы обвинить ваше решение (извините, если оно появилось таким образом, я тоже сейчас это прочитал!), А в том, чтобы описать степень симптомов, которые в этом случае весьма серьезны (большинство из Py2 / 3 проблемы приводят к исключениям, в то время как здесь хорошо ведущий себя цикл изменился на бесконечный, и сборка мусора борется с потоком вновь созданных объектов, приводя к колебаниям использования памяти с большим периодом и большой амплитудой).
Доктор Ян-Филипп Герке
45

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

import io
import subprocess

proc = subprocess.Popen(["prog", "arg"], stdout=subprocess.PIPE)
for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):  # or another encoding
    # do something with line

(Для этого требуется Python 3.)

JBG
источник
25
Я хотел бы использовать этот ответ, но я получаю: AttributeError: 'file' object has no attribute 'readable' py2.7
Дэн Гартвейт
3
Работает с питоном 3
matanster
Очевидно, что этот код недействителен по нескольким причинам совместимости с py3 / py3 и реальным риском получения ValueError: операция ввода-вывода для закрытого файла
sorin
3
@sorin ни одна из этих вещей не делает его «недействительным». Если вы пишете библиотеку, которая все еще должна поддерживать Python 2, не используйте этот код. Но многие люди могут позволить себе использовать программное обеспечение, выпущенное совсем недавно, чем десять лет назад. Если вы попытаетесь прочитать закрытый файл, вы получите это исключение независимо от того, используете вы его TextIOWrapperили нет. Вы можете просто обработать исключение.
JBG
1
Вы, возможно, опоздали на вечеринку, но ваш ответ актуален в текущей версии Python, ты
Dusan Gligoric
20

Действительно, если вы разобрались с итератором, буферизация теперь может стать вашей проблемой. Вы можете указать питону в подпроцессе не буферизировать его вывод.

proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)

становится

proc = subprocess.Popen(['python','-u', 'fake_utility.py'],stdout=subprocess.PIPE)

Мне это нужно при вызове Python изнутри Python.

Стив Картер
источник
14

Вы хотите передать эти дополнительные параметры subprocess.Popen:

bufsize=1, universal_newlines=True

Затем вы можете повторить, как в вашем примере. (Протестировано с Python 3.5)

user1747134
источник
2
@nicoulaj Должно работать при использовании пакета subprocess32.
Quantum7
4

Функция, позволяющая выполнять итерации одновременно stdoutи stderrв режиме реального времени построчно

Если вам нужно получить выходной поток для обоих stdoutи stderrодновременно, вы можете использовать следующую функцию.

Функция использует очереди для объединения обоих каналов Popen в один итератор.

Здесь мы создаем функцию read_popen_pipes():

from queue import Queue, Empty
from concurrent.futures import ThreadPoolExecutor


def enqueue_output(file, queue):
    for line in iter(file.readline, ''):
        queue.put(line)
    file.close()


def read_popen_pipes(p):

    with ThreadPoolExecutor(2) as pool:
        q_stdout, q_stderr = Queue(), Queue()

        pool.submit(enqueue_output, p.stdout, q_stdout)
        pool.submit(enqueue_output, p.stderr, q_stderr)

        while True:

            if p.poll() is not None and q_stdout.empty() and q_stderr.empty():
                break

            out_line = err_line = ''

            try:
                out_line = q_stdout.get_nowait()
            except Empty:
                pass
            try:
                err_line = q_stderr.get_nowait()
            except Empty:
                pass

            yield (out_line, err_line)

read_popen_pipes() в использовании:

import subprocess as sp


with sp.Popen(my_cmd, stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    for out_line, err_line in read_popen_pipes(p):

        # Do stuff with each line, e.g.:
        print(out_line, end='')
        print(err_line, end='')

    return p.poll() # return status-code
Rotareti
источник
2

Вы также можете читать строки без цикла. Работает в python3.6.

import os
import subprocess

process = subprocess.Popen(command, stdout=subprocess.PIPE)
list_of_byte_strings = process.stdout.readlines()
Aiven
источник
1
Или преобразовать в строки:list_of_strings = [x.decode('utf-8').rstrip('\n') for x in iter(process.stdout.readlines())]
ndtreviv
1

Я попробовал это с python3, и это сработало, источник

def output_reader(proc):
    for line in iter(proc.stdout.readline, b''):
        print('got line: {0}'.format(line.decode('utf-8')), end='')


def main():
    proc = subprocess.Popen(['python', 'fake_utility.py'],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)

    t = threading.Thread(target=output_reader, args=(proc,))
    t.start()

    try:
        time.sleep(0.2)
        import time
        i = 0

        while True:
        print (hex(i)*512)
        i += 1
        time.sleep(0.5)
    finally:
        proc.terminate()
        try:
            proc.wait(timeout=0.2)
            print('== subprocess exited with rc =', proc.returncode)
        except subprocess.TimeoutExpired:
            print('subprocess did not terminate in time')
    t.join()
shakram02
источник
1

Следующая модификация ответа Ромуло работает для меня на Python 2 и 3 (2.7.12 и 3.6.1):

import os
import subprocess

process = subprocess.Popen(command, stdout=subprocess.PIPE)
while True:
  line = process.stdout.readline()
  if line != '':
    os.write(1, line)
  else:
    break
MDH
источник
0

Не знаю, когда это было добавлено в модуль подпроцесса, но с Python 3 у вас должно получиться использовать proc.stdout.splitlines():

for line in proc.stdout.splitlines():
   print "stdout:", line
StefanQ
источник