Mencetak output Subprocess secara konstan saat proses sedang berjalan


202

Untuk meluncurkan program dari skrip Python saya, saya menggunakan metode berikut:

def execute(command):
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    output = process.communicate()[0]
    exitCode = process.returncode

    if (exitCode == 0):
        return output
    else:
        raise ProcessException(command, exitCode, output)

Jadi ketika saya meluncurkan proses seperti Process.execute("mvn clean install"), program saya menunggu sampai proses selesai, dan hanya kemudian saya mendapatkan hasil lengkap dari program saya. Ini menjengkelkan jika saya menjalankan proses yang membutuhkan beberapa saat untuk menyelesaikannya.

Bisakah saya membiarkan program saya menulis garis keluaran proses per baris, dengan mem-polling output proses sebelum selesai dalam satu lingkaran atau sesuatu?

** [EDIT] Maaf saya tidak mencari dengan baik sebelum memposting pertanyaan ini. Threading sebenarnya kuncinya. Temukan contoh di sini yang menunjukkan cara melakukannya: ** Python Subprocess.Popen dari utas


Utas alih-alih subproses, saya pikir
Ant

9
Tidak, Anda tidak perlu utas. Seluruh ide perpipaan bekerja karena Anda bisa mendapatkan baca / tulis dari proses saat mereka sedang berjalan.
tokland

Jawaban:


264

Anda dapat menggunakan iter untuk memproses garis secepat output perintah mereka: lines = iter(fd.readline, ""). Berikut adalah contoh lengkap yang menunjukkan kasus penggunaan umum (terima kasih kepada @jfs untuk membantu):

from __future__ import print_function # Only Python 2.x
import subprocess

def execute(cmd):
    popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
    for stdout_line in iter(popen.stdout.readline, ""):
        yield stdout_line 
    popen.stdout.close()
    return_code = popen.wait()
    if return_code:
        raise subprocess.CalledProcessError(return_code, cmd)

# Example
for path in execute(["locate", "a"]):
    print(path, end="")

24
Saya sudah mencoba kode ini (dengan program yang membutuhkan waktu yang signifikan untuk menjalankannya) dan dapat mengonfirmasi kode tersebut mengeluarkan garis-garis saat diterima, alih-alih menunggu eksekusi selesai. Ini adalah jawaban unggul imo.
Andrew Martin

11
Catatan: Di Python 3, Anda bisa menggunakan for line in popen.stdout: print(line.decode(), end=''). Untuk mendukung kedua Python 2 dan 3, gunakan byte literal: b''jika lines_iteratortidak pernah berakhir pada Python 3.
jfs

3
Masalah dengan pendekatan ini adalah bahwa jika proses berhenti sebentar tanpa menulis apa pun untuk stdout tidak ada lagi input untuk dibaca. Anda akan memerlukan loop untuk memeriksa apakah prosesnya sudah selesai atau belum. Saya mencoba ini menggunakan subprocess32 pada python 2.7
Har

7
itu harus bekerja. Untuk memolesnya, Anda bisa menambahkan bufsize=1(itu dapat meningkatkan kinerja pada Python 2), menutup popen.stdoutpipa secara eksplisit (tanpa menunggu pengumpulan sampah untuk mengurusnya), dan meningkatkan subprocess.CalledProcessError(seperti check_call(), check_output()lakukan). The printpernyataan berbeda di Python 2 dan 3: Anda bisa menggunakan softspace hack print line,(catatan: koma) untuk menghindari penggandaan semua baris seperti kode Anda lakukan dan melewati universal_newlines=Truedi Python 3, untuk mendapatkan teks bukan bytes- jawaban terkait .
jfs

6
@binzhang Itu bukan kesalahan, stdout disangga secara default pada skrip Python (juga untuk banyak alat Unix). Coba execute(["python", "-u", "child_thread.py"]). Info lebih lanjut: stackoverflow.com/questions/14258500/…
tokland

84

Oke, saya berhasil menyelesaikannya tanpa utas (saran mengapa menggunakan utas lebih baik dihargai) dengan menggunakan cuplikan dari pertanyaan ini Mencegat stdout dari suatu subproses saat sedang berjalan

def execute(command):
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    # Poll process for new output until finished
    while True:
        nextline = process.stdout.readline()
        if nextline == '' and process.poll() is not None:
            break
        sys.stdout.write(nextline)
        sys.stdout.flush()

    output = process.communicate()[0]
    exitCode = process.returncode

    if (exitCode == 0):
        return output
    else:
        raise ProcessException(command, exitCode, output)

3
Menggabungkan kode ifcher dan tokland bekerja dengan sangat baik (saya harus mengubahnya print line,menjadi sys.stdout.write(nextline); sys.stdout.flush(). Jika tidak, itu akan mencetak setiap dua baris. Kemudian lagi, ini menggunakan antarmuka Notebook IPython, jadi mungkin sesuatu yang lain terjadi - terlepas, secara eksplisit memanggil flush()karya.
eacousineau

3
Mister kau penyelamat hidupku !! benar-benar aneh bahwa hal-hal semacam ini tidak ada di perpustakaan itu sendiri .. karena jika saya menulis cliapp, saya ingin menunjukkan semua yang memproses dalam lingkaran secara instan .. s'rsly ..
holms

3
Dapat solusi ini dimodifikasi untuk terus mencetak baik output dan kesalahan? Jika saya mengubah stderr=subprocess.STDOUTke stderr=subprocess.PIPEdan kemudian memanggil process.stderr.readline()dari dalam loop, saya tampaknya bertabrakan dengan kebuntuan yang diperingatkan dalam dokumentasi untuk subprocessmodul.
davidrmcharles

7
@ DavidVarles Saya pikir apa yang Anda cari adalah stdout=subprocess.PIPE,stderr=subprocess.STDOUTini menangkap stderr, dan saya percaya (tapi saya belum menguji) bahwa ia juga menangkap stdin.
Andrew Martin

terima kasih telah menunggu kode keluar. Tidak tahu bagaimana cara mengatasinya
Vitaly Isaev

68

Untuk mencetak output baris demi baris segera setelah buffer stdout-nya memerah dengan Python 3:

from subprocess import Popen, PIPE, CalledProcessError

with Popen(cmd, stdout=PIPE, bufsize=1, universal_newlines=True) as p:
    for line in p.stdout:
        print(line, end='') # process line here

if p.returncode != 0:
    raise CalledProcessError(p.returncode, p.args)

Perhatikan: Anda tidak perlu p.poll()- loop berakhir saat eof tercapai. Dan Anda tidak perlu iter(p.stdout.readline, '')- bug read-ahead diperbaiki dalam Python 3.

Lihat juga, Python: baca input streaming dari subprocess.communicate () .


3
Solusi ini bekerja untuk saya. Solusi yang diterima yang diberikan di atas hanya mencetak baris kosong untuk saya.
Codename

3
Saya memang harus menambahkan sys.stdout.flush () untuk mendapatkan cetakan segera.
Codename

3
@Codename: Anda seharusnya tidak perlu sys.stdout.flush()di induk - stdout adalah buffer-line jika tidak diarahkan ke file / pipa dan karenanya mencetak lineflushes buffer secara otomatis. Anda tidak perlu sys.stdout.flush()pada anak juga - lewat -uopsi baris perintah saja.
jfs

1
@Codename: jika Anda ingin menggunakan >kemudian jalankan python -u your-script.py > some-file. Perhatikan: -uopsi yang telah saya sebutkan di atas (tidak perlu digunakan sys.stdout.flush()).
jfs

1
@mvidelgauz tidak perlu menelepon p.wait()— itu dipanggil saat keluar dari withblok. Gunakan p.returncode.
jfs

8

Sebenarnya ada cara yang sangat sederhana untuk melakukan ini ketika Anda hanya ingin mencetak hasilnya:

import subprocess
import sys

def execute(command):
    subprocess.check_call(command, stdout=sys.stdout, stderr=subprocess.STDOUT)

Di sini kita hanya mengarahkan subproses ke stdout kita sendiri, dan menggunakan api yang berhasil atau pengecualian.


1
Solusi ini lebih sederhana dan lebih bersih daripada solusi @ tokland, untuk Python 3.6. Saya perhatikan bahwa shell = Argumen benar tidak perlu.
Good Will

Tangkapan yang bagus, Good Will. Dihapusshell=True
Andrew Ring

Sangat menakjubkan, dan berfungsi sempurna dengan sedikit kode. Mungkin Anda harus mengarahkan subprocess stderr ke sys.stderr juga?
Manu

Manu kamu pasti bisa. Saya tidak, di sini, karena upaya dalam pertanyaan itu mengarahkan stderr ke stdout.
Andrew Ring

Bisakah Anda menjelaskan apa perbedaan antara sys.stdout dan subprocess.STDOUT?
Ron Serruya

7

@tokland

mencoba kode Anda dan memperbaikinya untuk 3.4 dan windows dir.cmd adalah perintah dir sederhana, disimpan sebagai file cmd

import subprocess
c = "dir.cmd"

def execute(command):
    popen = subprocess.Popen(command, stdout=subprocess.PIPE,bufsize=1)
    lines_iterator = iter(popen.stdout.readline, b"")
    while popen.poll() is None:
        for line in lines_iterator:
            nline = line.rstrip()
            print(nline.decode("latin"), end = "\r\n",flush =True) # yield line

execute(c)

3
Anda dapat menyederhanakan kode Anda . iter()dan end='\r\n'tidak perlu. Python menggunakan mode baris baru universal secara default yaitu, apa pun '\n'diterjemahkan ke '\r\n'selama pencetakan. 'latin'mungkin merupakan pengkodean yang salah, Anda dapat menggunakan universal_newlines=Trueuntuk mendapatkan output teks dalam Python 3 (diterjemahkan menggunakan pengodean pilihan lokal). Jangan berhenti .poll(), mungkin ada buffered data yang belum dibaca. Jika skrip Python berjalan di konsol maka outputnya adalah buffer-line; Anda dapat memaksa -uopsi buffering garis menggunakan - Anda tidak perlu di flush=Truesini.
jfs

4

Jika seseorang ingin membaca dari keduanya stdoutdan stderrpada saat yang sama menggunakan utas, inilah yang saya buat:

import threading
import subprocess
import Queue

class AsyncLineReader(threading.Thread):
    def __init__(self, fd, outputQueue):
        threading.Thread.__init__(self)

        assert isinstance(outputQueue, Queue.Queue)
        assert callable(fd.readline)

        self.fd = fd
        self.outputQueue = outputQueue

    def run(self):
        map(self.outputQueue.put, iter(self.fd.readline, ''))

    def eof(self):
        return not self.is_alive() and self.outputQueue.empty()

    @classmethod
    def getForFd(cls, fd, start=True):
        queue = Queue.Queue()
        reader = cls(fd, queue)

        if start:
            reader.start()

        return reader, queue


process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdoutReader, stdoutQueue) = AsyncLineReader.getForFd(process.stdout)
(stderrReader, stderrQueue) = AsyncLineReader.getForFd(process.stderr)

# Keep checking queues until there is no more output.
while not stdoutReader.eof() or not stderrReader.eof():
   # Process all available lines from the stdout Queue.
   while not stdoutQueue.empty():
       line = stdoutQueue.get()
       print 'Received stdout: ' + repr(line)

       # Do stuff with stdout line.

   # Process all available lines from the stderr Queue.
   while not stderrQueue.empty():
       line = stderrQueue.get()
       print 'Received stderr: ' + repr(line)

       # Do stuff with stderr line.

   # Sleep for a short time to avoid excessive CPU use while waiting for data.
   sleep(0.05)

print "Waiting for async readers to finish..."
stdoutReader.join()
stderrReader.join()

# Close subprocess' file descriptors.
process.stdout.close()
process.stderr.close()

print "Waiting for process to exit..."
returnCode = process.wait()

if returnCode != 0:
   raise subprocess.CalledProcessError(returnCode, command)

Saya hanya ingin membagikan ini, karena saya akhirnya pada pertanyaan ini mencoba melakukan sesuatu yang serupa, tetapi tidak ada jawaban yang memecahkan masalah saya. Semoga ini membantu seseorang!

Perhatikan bahwa dalam kasus penggunaan saya, proses eksternal membunuh proses yang kita Popen().


1
Saya harus menggunakan sesuatu yang hampir persis seperti ini untuk python2. Sementara sesuatu seperti ini seharusnya disediakan dalam python2, tidak demikian halnya seperti ini benar-benar baik-baik saja.
Stuart Axon

3

Bagi siapa pun yang mencoba jawaban atas pertanyaan ini untuk mendapatkan stdout dari skrip Python perhatikan bahwa Python mendukung stdout-nya, dan oleh karena itu mungkin perlu beberapa saat untuk melihat stdout.

Ini dapat diperbaiki dengan menambahkan berikut ini setelah setiap stdout menulis dalam skrip target:

sys.stdout.flush()

1
Tetapi menjalankan Python sebagai subproses dari Python memang gila sejak awal. Naskah Anda seharusnya hanya importnaskah lain; melihat ke dalam multiprocessingatau threadingjika Anda membutuhkan eksekusi paralel.
tripleee

3
@triplee Ada beberapa skenario di mana menjalankan Python sebagai subproses dari Python adalah tepat. Saya memiliki sejumlah skrip batch python yang ingin saya jalankan secara berurutan, setiap hari. Ini dapat diatur oleh skrip Python master yang memulai eksekusi, dan mengirimi saya email jika skrip anak gagal. Setiap skrip di-sandbox dari yang lain - tidak ada konflik penamaan. Saya tidak memparalelkan sehingga multiprocessing dan threading tidak relevan.
user1379351

Anda juga dapat memulai program python lainnya menggunakan python yang dapat dieksekusi yang berbeda dari program python utama yang sedang berjalan, misalnyasubprocess.run("/path/to/python/executable", "pythonProgramToRun.py")
Kyle Bridenstine

3

Dalam Python> = 3.5 menggunakan subprocess.runkarya untuk saya:

import subprocess

cmd = 'echo foo; sleep 1; echo foo; sleep 2; echo foo'
subprocess.run(cmd, shell=True)

(mendapatkan output selama eksekusi juga berfungsi tanpa shell=True) https://docs.python.org/3/library/subprocess.html#subprocess.run


2
Ini bukan "selama eksekusi". The subprocess.run()panggilan hanya mengembalikan ketika subprocess memiliki selesai berjalan.
tripleee

1
Bisakah Anda menjelaskan bagaimana ini bukan "selama eksekusi"? Sesuatu seperti >>> import subprocess; subprocess.run('top')juga tampaknya mencetak "selama eksekusi" (dan top tidak pernah selesai). Mungkin saya tidak memahami perbedaan yang halus?
user7017793

Jika Anda mengarahkan kembali output ke Python misalnya dengan stdout=subprocess.PIPEAnda hanya dapat membacanya setelah topselesai. Program Python Anda diblokir selama pelaksanaan subproses.
tripleee

1
Benar, itu masuk akal. The runMetode masih bekerja jika Anda hanya tertarik melihat output seperti itu dihasilkan. Jika Anda ingin melakukan sesuatu dengan output di python secara asinkron Anda benar bahwa itu tidak berfungsi.
user7017793

3

Untuk menjawab pertanyaan awal, cara terbaik IMO adalah mengarahkan ulang subproses stdoutlangsung ke program Anda stdout(secara opsional, hal yang sama dapat dilakukan untuk stderr, seperti dalam contoh di bawah)

p = Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
p.communicate()

3
Tidak menentukan apa pun untuk stdoutdan stderrmelakukan hal yang sama dengan kode yang lebih sedikit. Padahal saya kira eksplisit lebih baik daripada implisit.
tripleee

1

PoC ini secara konstan membaca output dari suatu proses dan dapat diakses ketika dibutuhkan. Hanya hasil terakhir yang disimpan, semua output lainnya dibuang, karenanya mencegah PIPE dari kehabisan memori:

import subprocess
import time
import threading
import Queue


class FlushPipe(object):
    def __init__(self):
        self.command = ['python', './print_date.py']
        self.process = None
        self.process_output = Queue.LifoQueue(0)
        self.capture_output = threading.Thread(target=self.output_reader)

    def output_reader(self):
        for line in iter(self.process.stdout.readline, b''):
            self.process_output.put_nowait(line)

    def start_process(self):
        self.process = subprocess.Popen(self.command,
                                        stdout=subprocess.PIPE)
        self.capture_output.start()

    def get_output_for_processing(self):
        line = self.process_output.get()
        print ">>>" + line


if __name__ == "__main__":
    flush_pipe = FlushPipe()
    flush_pipe.start_process()

    now = time.time()
    while time.time() - now < 10:
        flush_pipe.get_output_for_processing()
        time.sleep(2.5)

    flush_pipe.capture_output.join(timeout=0.001)
    flush_pipe.process.kill()

print_date.py

#!/usr/bin/env python
import time

if __name__ == "__main__":
    while True:
        print str(time.time())
        time.sleep(0.01)

output: Anda dapat dengan jelas melihat bahwa hanya ada output dari interval ~ 2.5s tidak ada di antaranya.

>>>1520535158.51
>>>1520535161.01
>>>1520535163.51
>>>1520535166.01

0

Ini berfungsi setidaknya di Python3.4

import subprocess

process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE)
for line in process.stdout:
    print(line.decode().strip())

1
Ini memiliki masalah yang diblokir dalam loop sampai proses selesai berjalan.
tripleee

0

Tidak ada jawaban di sini yang menjawab semua kebutuhan saya.

  1. Tidak ada utas untuk stdout (tidak ada Antrian, dll, juga)
  2. Non-blocking karena saya perlu memeriksa hal-hal lain yang terjadi
  3. Gunakan PIPE seperti yang saya perlukan untuk melakukan banyak hal, misalnya stream output, menulis ke file log dan mengembalikan salinan string dari output.

Sedikit latar belakang: Saya menggunakan ThreadPoolExecutor untuk mengelola kumpulan utas, masing-masing meluncurkan subproses dan menjalankannya bersamaan. (Dalam Python2.7, tetapi ini harus bekerja di 3.x yang lebih baru juga). Saya tidak ingin menggunakan utas hanya untuk pengumpulan keluaran karena saya ingin sebanyak mungkin tersedia untuk hal-hal lain (kumpulan 20 proses akan menggunakan 40 utas hanya untuk menjalankan; 1 untuk utas proses dan 1 untuk stdout ... dan lebih banyak jika Anda ingin stderr kurasa)

Saya menelanjangi banyak pengecualian dan semacamnya di sini jadi ini didasarkan pada kode yang bekerja di produksi. Semoga saya tidak merusaknya di copy dan paste. Juga, umpan balik sangat disambut!

import time
import fcntl
import subprocess
import time

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

# Make stdout non-blocking when using read/readline
proc_stdout = proc.stdout
fl = fcntl.fcntl(proc_stdout, fcntl.F_GETFL)
fcntl.fcntl(proc_stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK)

def handle_stdout(proc_stream, my_buffer, echo_streams=True, log_file=None):
    """A little inline function to handle the stdout business. """
    # fcntl makes readline non-blocking so it raises an IOError when empty
    try:
        for s in iter(proc_stream.readline, ''):   # replace '' with b'' for Python 3
            my_buffer.append(s)

            if echo_streams:
                sys.stdout.write(s)

            if log_file:
                log_file.write(s)
    except IOError:
        pass

# The main loop while subprocess is running
stdout_parts = []
while proc.poll() is None:
    handle_stdout(proc_stdout, stdout_parts)

    # ...Check for other things here...
    # For example, check a multiprocessor.Value('b') to proc.kill()

    time.sleep(0.01)

# Not sure if this is needed, but run it again just to be sure we got it all?
handle_stdout(proc_stdout, stdout_parts)

stdout_str = "".join(stdout_parts)  # Just to demo

Saya yakin ada overhead yang ditambahkan di sini tapi itu bukan masalah dalam kasus saya. Secara fungsional itu melakukan apa yang saya butuhkan. Satu-satunya hal yang belum saya pecahkan adalah mengapa ini bekerja dengan sempurna untuk pesan log tetapi saya melihat beberapa printpesan muncul kemudian dan sekaligus.


-2

Dalam Python 3.6 saya menggunakan ini:

import subprocess

cmd = "command"
output = subprocess.call(cmd, shell=True)
print(process)

1
Ini bukan jawaban untuk pertanyaan khusus ini. Menunggu proses selesai sebelum mendapatkan hasilnya secara spesifik dan tepat apa yang OP berusaha hindari. Fungsi lawas lama subprocess.call()memiliki beberapa kutil yang diperbaiki oleh fungsi yang lebih baru; dalam Python 3.6 Anda biasanya akan menggunakan subprocess.run()ini; untuk kenyamanan, fungsi pembungkus yang lebih lama subprocess.check_output()juga masih tersedia - ini mengembalikan output aktual dari proses (kode ini hanya akan mengembalikan kode keluar, tetapi bahkan kemudian mencetak sesuatu yang tidak ditentukan sebagai gantinya).
tripleee
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.