Mendapatkan output realtime menggunakan subproses

135

Saya mencoba menulis skrip wrapper untuk program baris perintah (svnadmin memverifikasi) yang akan menampilkan indikator kemajuan yang bagus untuk operasi. Ini mengharuskan saya untuk dapat melihat setiap baris output dari program yang dibungkus segera setelah itu adalah output.

Saya pikir saya hanya akan menjalankan program menggunakan subprocess.Popen, menggunakan stdout=PIPE, kemudian membaca setiap baris ketika masuk dan bertindak sesuai. Namun, ketika saya menjalankan kode berikut, output tampaknya buffer di suatu tempat, menyebabkannya muncul dalam dua potongan, baris 1 hingga 332, kemudian 333 hingga 439 (baris terakhir output)

from subprocess import Popen, PIPE, STDOUT

p = Popen('svnadmin verify /var/svn/repos/config', stdout = PIPE, 
        stderr = STDOUT, shell = True)
for line in p.stdout:
    print line.replace('\n', '')

Setelah melihat dokumentasi pada subproses sedikit, saya menemukan bufsizeparameter untuk Popen, jadi saya mencoba menetapkan bufsize ke 1 (buffer setiap baris) dan 0 (tidak ada buffer), tetapi tidak ada nilai yang tampaknya mengubah cara jalur dikirim.

Pada titik ini saya mulai memahami sedotan, jadi saya menulis loop keluaran berikut:

while True:
    try:
        print p.stdout.next().replace('\n', '')
    except StopIteration:
        break

tetapi mendapat hasil yang sama.

Apakah mungkin untuk mendapatkan output program 'realtime' dari suatu program yang dieksekusi menggunakan subproses? Apakah ada opsi lain di Python yang kompatibel dengan maju (tidak exec*)?

Chris Lieb
sumber
1
Sudahkah Anda mencoba menghilangkan sydout=PIPEsehingga proses menulis langsung ke konsol Anda, melewati proses induk?
S.Lott
5
Masalahnya adalah saya ingin membaca output. Jika itu output langsung ke konsol, bagaimana saya bisa melakukan itu? Juga, saya tidak ingin pengguna melihat output dari program yang dibungkus, hanya output saya.
Chris Lieb
Lalu mengapa tampilan "waktu nyata"? Saya tidak mendapatkan use case.
S.Lott
8
Jangan gunakan shell = True. Tak perlu meminta shell Anda. Gunakan p = Popen (['svnadmin', 'verifikasi', '/ var / svn / repos / config'], sebagai gantinya stdout = PIPE, stderr = STDOUT)
nosklo
2
@ S.Lott Pada dasarnya, svnadmin memverifikasi mencetak garis output untuk setiap revisi yang diverifikasi. Saya ingin membuat indikator kemajuan yang bagus yang tidak akan menyebabkan jumlah output yang berlebihan. Agak seperti wget, misalnya
Chris Lieb

Jawaban:

82

Saya mencoba ini, dan untuk beberapa alasan sementara kode

for line in p.stdout:
  ...

buffer secara agresif, varian

while True:
  line = p.stdout.readline()
  if not line: break
  ...

tidak. Rupanya ini adalah bug yang dikenal: http://bugs.python.org/issue3907 (Masalahnya sekarang "Ditutup" pada 29 Agustus 2018)

Dave
sumber
Ini bukan satu-satunya kekacauan dalam implementasi Python IO lama. Inilah sebabnya mengapa Py2.6 dan Py3k berakhir dengan perpustakaan IO yang sama sekali baru.
Tim Lin
3
Kode ini akan rusak jika subproses mengembalikan baris kosong. Solusi yang lebih baik adalah dengan menggunakan while p.poll() is Nonealih-alih while True, dan menghapusif not line
exhuma
6
@ Exuma: berfungsi dengan baik. readline mengembalikan "\ n" pada baris kosong, yang tidak dievaluasi sebagai benar. itu hanya mengembalikan string kosong ketika pipa ditutup, yang akan terjadi ketika subproses berakhir.
Alice Purcell
1
@Dave For ref ref: cetak utf-8 baris di py2 + with print(line.decode('utf-8').rstrip()).
Jonathan Komar
3
Juga untuk memiliki realtime membaca output proses Anda perlu memberitahu python bahwa Anda TIDAK ingin buffering apa pun. Dear Python, beri saya output langsung. Dan ini caranya: Anda perlu mengatur variabel lingkungan PYTHONUNBUFFERED=1. Ini sangat berguna untuk keluaran yang tidak terbatas
George Pligoropoulos
38
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1)
for line in iter(p.stdout.readline, b''):
    print line,
p.stdout.close()
p.wait()
Corey Goldberg
sumber
1
@nbro mungkin karena p.stdout.close()tidak jelas.
anatoly techtonik
1
@nbro mungkin karena kode diberikan tanpa penjelasan ...: /
Aaron Hall
3
Tentang apa ini?
ManuelSchneid3r
29

Anda dapat mengarahkan output subproses ke stream secara langsung. Contoh sederhana:

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)
Aidan Feldman
sumber
Apakah ini memungkinkan Anda untuk juga mendapatkan konten setelah fakta masuk .communicate()? Atau konten hilang ke induk stderr / stdout stream?
theferrit32
Tidak, tidak ada communicate()metode untuk kembali CompletedProcess. Juga, capture_outputsaling eksklusif dengan stdoutdan stderr.
Aidan Feldman
20

Anda dapat mencoba ini:

import subprocess
import sys

process = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

while True:
    out = process.stdout.read(1)
    if out == '' and process.poll() != None:
        break
    if out != '':
        sys.stdout.write(out)
        sys.stdout.flush()

Jika Anda menggunakan readline sebagai ganti read, akan ada beberapa kasus di mana pesan input tidak dicetak. Cobalah dengan perintah yang memerlukan input inline dan lihat sendiri.

Nadia Alramli
sumber
Ya, menggunakan readline () akan berhenti mencetak (bahkan dengan memanggil sys.stdout.flush ())
Mark Ma
3
Apakah ini seharusnya menggantung tanpa batas? Saya ingin solusi yang diberikan juga menyertakan kode boilerplate untuk mengedit loop ketika proses awal dilakukan. Maaf saya tidak peduli berapa kali saya melihatnya, subproses dan sebagainya adalah sesuatu yang saya tidak bisa mulai bekerja.
ThorSummoner
1
Mengapa menguji '' ketika di Python kita bisa menggunakan jika tidak keluar?
Greg Bell
2
ini adalah solusi terbaik untuk pekerjaan jangka panjang. tetapi yang harus digunakan bukanlah Tidak ada dan tidak! = Tidak ada. Anda tidak boleh menggunakan! = Dengan Tidak Ada.
Cari
Apakah stderr juga ditampilkan oleh ini?
Pieter Vogelaar
7

The Streaming subproses stdin dan stdout dengan asyncio di Python posting blog oleh Kevin McCarthy menunjukkan bagaimana melakukannya dengan asyncio:

import asyncio
from asyncio.subprocess import PIPE
from asyncio import create_subprocess_exec


async def _read_stream(stream, callback):
    while True:
        line = await stream.readline()
        if line:
            callback(line)
        else:
            break


async def run(command):
    process = await create_subprocess_exec(
        *command, stdout=PIPE, stderr=PIPE
    )

    await asyncio.wait(
        [
            _read_stream(
                process.stdout,
                lambda x: print(
                    "STDOUT: {}".format(x.decode("UTF8"))
                ),
            ),
            _read_stream(
                process.stderr,
                lambda x: print(
                    "STDERR: {}".format(x.decode("UTF8"))
                ),
            ),
        ]
    )

    await process.wait()


async def main():
    await run("docker build -t my-docker-image:latest .")


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
Pablo
sumber
ini bekerja dengan sedikit modifikasi pada kode yang diposting
ampun
HaiJeef, bisakah Anda menunjukkan perbaikannya sehingga saya dapat memperbarui jawabannya?
Pablo
Hai, itu berhasil bagi saya, tetapi saya harus menambahkan yang berikut untuk menyingkirkan beberapa pesan kesalahan: import nest_asyncio; nest_asyncio.apply()dan menggunakan perintah shell, yaitu process = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)alih-alih process = await create_subprocess_exec(...). Bersulang!
user319436
4

Masalah Output Real Time diselesaikan: Saya memang mengalami masalah serupa di Python, sambil menangkap output real time dari program c. Saya menambahkan " fflush (stdout) ;" dalam kode C saya. Ini berhasil untuk saya. Ini adalah snip kodenya

<< Program C >>

#include <stdio.h>
void main()
{
    int count = 1;
    while (1)
    {
        printf(" Count  %d\n", count++);
        fflush(stdout);
        sleep(1);
    }
}

<< Program Python >>

#!/usr/bin/python

import os, sys
import subprocess


procExe = subprocess.Popen(".//count", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

while procExe.poll() is None:
    line = procExe.stdout.readline()
    print("Print:" + line)

<< OUTPUT >> Cetak: Hitungan 1 Cetak: Hitungan 2 Cetak: Hitungan 3

Semoga ini bisa membantu.

~ sairam

sairam
sumber
1
Ini adalah satu-satunya hal yang benar-benar membantu. Saya menggunakan kode yang sama ( flush(stdout)) di C ++. Terima kasih!
Gerhard Hagerer
Saya mengalami masalah yang sama dengan skrip python memanggil skrip python lain sebagai subproses. Pada cetakan subproses, "flush" diperlukan (print ("hello", flush = True) di python 3). Juga, banyak contoh di sana masih (2020) python 2, ini python 3, jadi +1
smajtkst
3

Saya mengalami masalah yang sama beberapa waktu lalu. Solusi saya adalah membuang iterasi untuk readmetode ini, yang akan segera kembali walaupun subproses Anda belum selesai dieksekusi, dll.

Eli Courtwright
sumber
3

Tergantung pada kasus penggunaan, Anda mungkin juga ingin menonaktifkan buffering dalam subproses itu sendiri.

Jika subproses akan menjadi proses Python, Anda bisa melakukan ini sebelum panggilan:

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

Atau sebagai alternatif, berikan envargumen ini pada Popen.

Jika tidak, jika Anda menggunakan Linux / Unix, Anda dapat menggunakan stdbufalat ini. Misalnya suka:

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

Lihat juga di sini tentang stdbufatau opsi lain.

(Lihat juga di sini untuk jawaban yang sama.)

Albert
sumber
2

Saya menggunakan solusi ini untuk mendapatkan output realtime pada suatu subproses. Loop ini akan berhenti segera setelah proses selesai tanpa meninggalkan kebutuhan untuk pernyataan break atau loop tak terbatas yang mungkin.

sub_process = subprocess.Popen(my_command, close_fds=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while sub_process.poll() is None:
    out = sub_process.stdout.read(1)
    sys.stdout.write(out)
    sys.stdout.flush()
Jason Hedlund
sumber
5
apakah mungkin ini akan keluar dari loop tanpa buffer stdout kosong?
jayjay
Saya telah mencari banyak jawaban yang cocok yang tidak menunggu setelah selesai! Saya menemukan ini sebagai solusi dengan menambahkan if out=='': breaksetelahout = sub_process...
Sos
2

Menemukan fungsi "plug-and-play" ini di sini . Bekerja seperti pesona!

import subprocess

def myrun(cmd):
    """from http://blog.kagesenshi.org/2008/02/teeing-python-subprocesspopen-output.html
    """
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    stdout = []
    while True:
        line = p.stdout.readline()
        stdout.append(line)
        print line,
        if line == '' and p.poll() != None:
            break
    return ''.join(stdout)
Deena
sumber
1
Penambahan stderr=subprocess.STDOUTsebenarnya sangat membantu dalam menangkap data streaming. Saya membatalkannya.
Khan
1
Daging sapi utama di sini tampaknya berasal dari jawaban yang diterima
tripleee
2

Anda dapat menggunakan iterator pada setiap byte dalam output dari subproses. Ini memungkinkan pembaruan inline (baris yang diakhiri dengan '\ r' menimpa jalur output sebelumnya) dari subproses:

from subprocess import PIPE, Popen

command = ["my_command", "-my_arg"]

# Open pipe to subprocess
subprocess = Popen(command, stdout=PIPE, stderr=PIPE)


# read each byte of subprocess
while subprocess.poll() is None:
    for c in iter(lambda: subprocess.stdout.read(1) if subprocess.poll() is None else {}, b''):
        c = c.decode('ascii')
        sys.stdout.write(c)
sys.stdout.flush()

if subprocess.returncode != 0:
    raise Exception("The subprocess did not terminate correctly.")
rhyno183
sumber
2

Dalam Python 3.x proses mungkin hang karena outputnya adalah array byte, bukan string. Pastikan Anda mendekodekannya menjadi string.

Mulai dari Python 3.6 Anda bisa melakukannya menggunakan parameter encodingdi Popen Constructor . Contoh lengkap:

process = subprocess.Popen(
    'my_command',
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    shell=True,
    encoding='utf-8',
    errors='replace'
)

while True:
    realtime_output = process.stdout.readline()

    if realtime_output == '' and process.poll() is not None:
        break

    if realtime_output:
        print(realtime_output.strip(), flush=True)

Perhatikan bahwa kode ini mengalihkan stderr ke stdoutdan menangani kesalahan output .

pavelnazimok
sumber
1

Menggunakan pexpect [ http://www.noah.org/wiki/Pexpect ] dengan readlines non-blocking akan menyelesaikan masalah ini. Ini berasal dari fakta bahwa pipa di-buffer, dan output aplikasi Anda semakin di-buffer oleh pipa, oleh karena itu Anda tidak bisa mencapai output itu sampai buffer mengisi atau proses mati.

Gabe
sumber
0

Solusi lengkap:

import contextlib
import subprocess

# Unix, Windows and old Macintosh end-of-line
newlines = ['\n', '\r\n', '\r']
def unbuffered(proc, stream='stdout'):
    stream = getattr(proc, stream)
    with contextlib.closing(stream):
        while True:
            out = []
            last = stream.read(1)
            # Don't loop forever
            if last == '' and proc.poll() is not None:
                break
            while last not in newlines:
                # Don't loop forever
                if last == '' and proc.poll() is not None:
                    break
                out.append(last)
                last = stream.read(1)
            out = ''.join(out)
            yield out

def example():
    cmd = ['ls', '-l', '/']
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        # Make all end-of-lines '\n'
        universal_newlines=True,
    )
    for line in unbuffered(proc):
        print line

example()
Andres Restrepo
sumber
1
Karena Anda menggunakan universal_newlines=Truepada Popen()panggilan, Anda mungkin tidak perlu untuk menempatkan penanganan Anda sendiri dari mereka dalam, juga - itulah inti dari pilihan.
martineau
1
sepertinya tidak perlu rumit. Itu tidak memecahkan masalah buffering. Lihat tautan dalam jawaban saya .
jfs
Ini adalah satu-satunya cara saya bisa mendapatkan output kemajuan rsync secara realtime (- outbuf = L)! terima kasih
Mohammadhzp
0

Ini adalah kerangka dasar yang selalu saya gunakan untuk ini. Ini membuatnya mudah untuk menerapkan batas waktu dan mampu menangani proses gantung yang tidak terhindarkan.

import subprocess
import threading
import Queue

def t_read_stdout(process, queue):
    """Read from stdout"""

    for output in iter(process.stdout.readline, b''):
        queue.put(output)

    return

process = subprocess.Popen(['dir'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT,
                           bufsize=1,
                           cwd='C:\\',
                           shell=True)

queue = Queue.Queue()
t_stdout = threading.Thread(target=t_read_stdout, args=(process, queue))
t_stdout.daemon = True
t_stdout.start()

while process.poll() is None or not queue.empty():
    try:
        output = queue.get(timeout=.5)

    except Queue.Empty:
        continue

    if not output:
        continue

    print(output),

t_stdout.join()
Badslacks
sumber
0

(Solusi ini telah diuji dengan Python 2.7.15)
Anda hanya perlu sys.stdout.flush () setelah setiap baris membaca / menulis:

while proc.poll() is None:
    line = proc.stdout.readline()
    sys.stdout.write(line)
    # or print(line.strip()), you still need to force the flush.
    sys.stdout.flush()
dan
sumber
0

Beberapa jawaban menyarankan python 3.x atau pthon 2.x, kode di bawah ini akan berfungsi untuk keduanya.

 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,)
    stdout = []
    while True:
        line = p.stdout.readline()
        if not isinstance(line, (str)):
            line = line.decode('utf-8')
        stdout.append(line)
        print (line)
        if (line == '' and p.poll() != None):
            break
Djai
sumber