Вопрос-Ответ

read subprocess stdout line by line

чтение стандартного вывода подпроцесса построчно

Мой скрипт на 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)

В родительском процессе:

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.

Чего я не понимаю? Возможно ли это вообще?


Переведено автоматически
Ответ 1

Я думаю, проблема в операторе 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).

Ответ 2

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

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

Ответ 3

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

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

становится

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

Мне это понадобилось при вызове python изнутри python.

Ответ 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
python subprocess