baca subproses stdout baris demi baris

235

Skrip python saya menggunakan subproses untuk memanggil utilitas linux yang sangat bising. Saya ingin menyimpan semua output ke file log dan menunjukkan sebagian kepada pengguna. Saya pikir yang berikut ini akan berfungsi, tetapi output tidak muncul di aplikasi saya sampai utilitas telah menghasilkan sejumlah besar output.

#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()

Perilaku yang saya benar-benar inginkan adalah skrip filter untuk mencetak setiap baris saat diterima dari subproses. Agak seperti apa teetetapi dengan kode python.

Apa yang saya lewatkan? Apakah ini mungkin?


Memperbarui:

Jika a sys.stdout.flush()ditambahkan ke fake_utility.py, kode tersebut memiliki perilaku yang diinginkan dalam python 3.1. Saya menggunakan python 2.6. Anda akan berpikir bahwa menggunakan proc.stdout.xreadlines()akan bekerja sama dengan py3k, tetapi tidak.


Pembaruan 2:

Ini adalah kode kerja minimal.

#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
sumber
4
Anda bisa menggunakan print line,sebagai gantinya print line.rstrip()(catatan: koma di akhir).
jfs
2
Pembaruan 2 menyatakan bahwa ia bekerja dengan python 3.0+ tetapi menggunakan pernyataan cetak yang lama, jadi itu tidak bekerja dengan python 3.0+.
Rooky
Tidak ada jawaban yang tercantum di sini yang berfungsi untuk saya, tetapi stackoverflow.com/questions/5411780/… berhasil!
kemas

Jawaban:

179

Sudah lama sejak saya terakhir bekerja dengan Python, tapi saya pikir masalahnya ada pada pernyataan for line in proc.stdout, yang membaca seluruh input sebelum mengulanginya. Solusinya adalah menggunakan readline()sebagai gantinya:

#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()

Tentu saja Anda masih harus berurusan dengan buffering subproses.

Catatan: menurut dokumentasi solusi dengan iterator harus setara dengan menggunakan readline(), kecuali untuk buffer read-ahead, tetapi (atau justru karena ini) perubahan yang diajukan memang menghasilkan hasil yang berbeda untuk saya (Python 2.5 pada Windows XP).

Rômulo Ceccon
sumber
11
untuk file.readline()vs. for line in filelihat bugs.python.org/issue3907 (singkatnya: ini bekerja pada Python3; gunakan io.open()pada Python 2.6+)
jfs
5
Tes lebih pythonic untuk EOF, per "Rekomendasi Pemrograman" di PEP 8 ( python.org/dev/peps/pep-0008 ), akan menjadi 'jika tidak baris:'.
Jason Mock
14
@naxa: untuk pipa: for line in iter(proc.stdout.readline, ''):.
jfs
3
@ Jan-PhilipGehrcke: ya. 1. Anda dapat menggunakan for line in proc.stdoutpada Python 3 (tidak ada bug read-ahead) 2. '' != b''pada Python 3 - jangan salin-tempel kode secara membabi buta - pikirkan apa yang dilakukannya dan bagaimana cara kerjanya.
jfs
2
@ JFSebastian: pasti, iter(f.readline, b'')solusinya agak jelas (dan juga berfungsi pada Python 2, jika ada yang tertarik). Inti dari komentar saya bukan untuk menyalahkan solusi Anda (maaf jika muncul seperti itu, saya baca sekarang juga!), Tetapi untuk menggambarkan sejauh mana gejala, yang cukup parah dalam kasus ini (sebagian besar Py2 / 3 masalah menghasilkan pengecualian, sedangkan di sini loop berperilaku baik berubah menjadi tanpa akhir, dan pengumpulan sampah berjuang melawan banjir objek yang baru dibuat, menghasilkan osilasi penggunaan memori dengan periode panjang dan amplitudo besar).
Dr. Jan-Philip Gehrcke
45

Agak terlambat ke pesta, tetapi terkejut tidak melihat apa yang saya pikir merupakan solusi paling sederhana di sini:

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

(Ini membutuhkan Python 3.)

jbg
sumber
25
Saya ingin menggunakan jawaban ini tetapi saya mendapatkan: AttributeError: 'file' object has no attribute 'readable' py2.7
Dan Garthwaite
3
Bekerja dengan python 3
matanster
Jelas kode ini tidak valid karena berbagai alasan kompatibilitas py3 / py3 dan risiko nyata mendapatkan ValueError: Operasi I / O pada file tertutup
sorin
3
@ Atau tidak satu pun dari hal-hal itu membuatnya "tidak valid". Jika Anda sedang menulis perpustakaan yang masih perlu mendukung Python 2, maka jangan gunakan kode ini. Tetapi banyak orang memiliki kemewahan untuk dapat menggunakan perangkat lunak yang dirilis lebih baru dari satu dekade yang lalu. Jika Anda mencoba membaca pada file tertutup Anda akan mendapatkan pengecualian itu terlepas dari apakah Anda menggunakan TextIOWrapperatau tidak. Anda bisa menangani pengecualian.
jbg
1
Anda mungkin terlambat ke pesta tetapi jawaban Anda mutakhir dengan versi Python saat ini, ty
Dusan Gligoric
20

Memang, jika Anda mengurutkan iterator maka buffering sekarang bisa menjadi masalah Anda. Anda bisa memberi tahu python dalam sub-proses untuk tidak menyangga outputnya.

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

menjadi

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

Saya membutuhkan ini ketika memanggil python dari dalam python.

Steve Carter
sumber
14

Anda ingin meneruskan parameter tambahan ini ke subprocess.Popen:

bufsize=1, universal_newlines=True

Kemudian Anda dapat mengulangi seperti pada contoh Anda. (Diuji dengan Python 3.5)

pengguna1747134
sumber
2
@nicoulaj Ini seharusnya berfungsi jika menggunakan paket subprocess32.
Quantum7
4

Fungsi yang memungkinkan pengulangan baik secara bersamaan stdoutmaupun stderrbersamaan, secara real time, baris demi baris

Jika Anda perlu mendapatkan aliran output untuk keduanya stdoutdan stderrpada saat yang sama, Anda dapat menggunakan fungsi berikut.

Fungsi ini menggunakan Antrian untuk menggabungkan kedua pipa Popen menjadi satu iterator.

Di sini kita membuat fungsinya 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() digunakan:

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
sumber
2

Anda juga dapat membaca baris tanpa loop. Bekerja di python3.6.

import os
import subprocess

process = subprocess.Popen(command, stdout=subprocess.PIPE)
list_of_byte_strings = process.stdout.readlines()
aiven
sumber
1
Atau untuk mengubahnya menjadi string:list_of_strings = [x.decode('utf-8').rstrip('\n') for x in iter(process.stdout.readlines())]
ndtreviv
1

Saya mencoba ini dengan python3 dan berhasil, sumber

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
sumber
1

Modifikasi jawaban Rômulo berikut berfungsi untuk saya di Python 2 dan 3 (2.7.12 dan 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
sumber
0

Entah ketika ini telah ditambahkan ke modul subproses, tetapi dengan Python 3 Anda harus baik-baik saja dengan menggunakan proc.stdout.splitlines():

for line in proc.stdout.splitlines():
   print "stdout:", line
StefanQ
sumber