menangkap stdout secara realtime dari subprocess

89

Saya ingin subprocess.Popen()rsync.exe di Windows, dan mencetak stdout dengan Python.

Kode saya berfungsi, tetapi tidak menangkap kemajuan sampai transfer file selesai! Saya ingin mencetak kemajuan untuk setiap file secara real time.

Menggunakan Python 3.1 sekarang karena saya mendengarnya seharusnya lebih baik dalam menangani IO.

import subprocess, time, os, sys

cmd = "rsync.exe -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()
John A
sumber
1
(Berasal dari google?) Semua PIPE akan menemui jalan buntu ketika salah satu buffer PIPE terisi dan tidak terbaca. misalnya kebuntuan stdout saat stderr terisi. Jangan pernah melewatkan PIPE yang tidak ingin Anda baca.
Nasser Al-Wohaibi
Bisakah seseorang menjelaskan mengapa Anda tidak bisa mengatur stdout ke sys.stdout daripada subprocess.PIPE?
Mike

Jawaban:

101

Beberapa aturan praktis untuk subprocess.

  • Jangan pernah gunakan shell=True. Itu tidak perlu memanggil proses shell ekstra untuk memanggil program Anda.
  • Saat memanggil proses, argumen diteruskan sebagai daftar. sys.argvdi python adalah daftar, dan begitu juga argvdi C. Jadi Anda lulus daftar untuk Popenmemanggil subproses, bukan string.
  • Jangan mengalihkan stderrke PIPEsaat Anda tidak membacanya.
  • Jangan mengalihkan stdinketika Anda tidak sedang menulis untuk itu.

Contoh:

import subprocess, time, os, sys
cmd = ["rsync.exe", "-vaz", "-P", "source/" ,"dest/"]

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

for line in iter(p.stdout.readline, b''):
    print(">>> " + line.rstrip())

Meskipun demikian, kemungkinan besar rsync menyangga keluarannya ketika ia mendeteksi bahwa ia terhubung ke pipa dan bukan ke terminal. Ini adalah perilaku default - ketika terhubung ke pipa, program harus secara eksplisit membersihkan stdout untuk hasil realtime, jika tidak, library C standar akan buffer.

Untuk mengujinya, coba jalankan ini sebagai gantinya:

cmd = [sys.executable, 'test_out.py']

dan buat test_out.pyfile dengan isinya:

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

Menjalankan subproses itu akan memberi Anda "Halo" dan menunggu 10 detik sebelum memberikan "Dunia". Jika itu terjadi dengan kode python di atas dan bukan dengan rsync, itu berarti rsyncoutput buffering, jadi Anda kurang beruntung.

Solusinya adalah menghubungkan langsung ke pty, menggunakan sesuatu seperti pexpect.

nosklo.dll
sumber
12
shell=Falseadalah hal yang benar ketika Anda membuat baris perintah terutama dari data yang dimasukkan pengguna. Tapi bagaimanapun shell=Truejuga berguna ketika Anda mendapatkan seluruh baris perintah dari sumber terpercaya (misalnya hardcode di skrip).
Denis Otkidach
10
@Denis Otkidach: Saya rasa itu tidak perlu menggunakan shell=True. Pikirkan tentang itu - Anda menjalankan proses lain di OS Anda, yang melibatkan alokasi memori, penggunaan disk, penjadwalan prosesor, hanya untuk membagi string ! Dan yang Anda gabungkan sendiri !! Anda dapat membaginya dengan python, tetapi lebih mudah menulis setiap parameter secara terpisah. Juga, menggunakan daftar sarana Anda tidak harus melarikan diri karakter shell khusus: ruang, ;, >, <, &.. parameter Anda dapat berisi orang-orang chars dan Anda tidak perlu khawatir! Saya tidak bisa melihat alasan untuk menggunakan shell=True, sungguh, kecuali Anda menjalankan perintah khusus shell.
nosklo
nosklo, yang seharusnya: p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
Senthil Kumaran
1
@ mathtick: Saya tidak yakin mengapa Anda melakukan operasi tersebut sebagai proses terpisah ... Anda dapat memotong konten file dan mengekstrak bidang pertama dengan mudah dalam python dengan menggunakan csvmodul. Tetapi sebagai contoh, pipeline Anda dengan python adalah: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultPerhatikan bahwa Anda dapat bekerja dengan nama file yang panjang dan karakter khusus shell tanpa harus keluar, karena sekarang shell tidak terlibat. Juga jauh lebih cepat karena ada satu proses yang lebih sedikit.
nosklo
11
gunakan for line in iter(p.stdout.readline, b'')alih-alih for line in p.stdoutdi Python 2 jika tidak, garis tidak dibaca secara real time meskipun proses sumber tidak menyangga keluarannya.
jfs
43

Saya tahu ini adalah topik lama, tetapi sekarang ada solusinya. Panggil rsync dengan opsi --outbuf = L. Contoh:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())
Elvin
sumber
3
Ini berfungsi dan harus diberi suara positif untuk menyelamatkan pembaca di masa mendatang dari menggulir semua dialog di atas.
VectorVictor
1
@VectorVictor Itu tidak menjelaskan apa yang sedang terjadi, dan mengapa itu terjadi. Mungkin saja program Anda bekerja, sampai: 1. Anda menambahkan preexec_fn=os.setpgrpagar program bertahan dari skrip induknya 2. Anda melewatkan membaca dari pipa proses 3. proses mengeluarkan banyak data, mengisi pipa 4. Anda terjebak selama berjam-jam , mencoba mencari tahu mengapa program yang Anda jalankan berhenti setelah beberapa waktu . Jawaban dari @nosklo sangat membantu saya.
danuker
16

Di Linux, saya memiliki masalah yang sama dalam menghilangkan buffering. Saya akhirnya menggunakan "stdbuf -o0" (atau, unbuffer from expect) untuk menghilangkan buffering PIPE.

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

Saya kemudian dapat menggunakan select.select di stdout.

Lihat juga /unix/25372/

Ling
sumber
2
Bagi siapa pun yang mencoba mengambil stdout kode C dari Python, saya dapat mengonfirmasi bahwa solusi ini adalah satu-satunya yang berhasil untuk saya. Untuk lebih jelasnya, saya berbicara tentang menambahkan 'stdbuf', '-o0' ke daftar perintah saya yang ada di Popen.
Ceroboh
Terima kasih! stdbuf -o0terbukti sangat berguna dengan sekumpulan tes pytest / pytest-bdd Saya menulis yang menelurkan aplikasi C ++ dan memverifikasi bahwa itu memancarkan pernyataan log tertentu. Tanpanya stdbuf -o0, pengujian ini membutuhkan 7 detik untuk mendapatkan keluaran (buffer) dari program C ++. Sekarang mereka berjalan hampir seketika!
evadeflow
Jawaban ini menyelamatkan saya hari ini! Menjalankan aplikasi sebagai subproses sebagai bagian dari pytest, tidak mungkin bagi saya untuk mendapatkan keluarannya. stdbufmelakukannya.
Janos
14

Bergantung pada kasus penggunaan, Anda mungkin juga ingin menonaktifkan buffering di subproses itu sendiri.

Jika subproses adalah proses Python, Anda dapat melakukan ini sebelum panggilan:

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

Atau sebagai alternatif, berikan ini dalam envargumen ke Popen.

Jika tidak, jika Anda menggunakan Linux / Unix, Anda dapat menggunakan stdbufalat tersebut. Misalnya seperti:

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

Lihat juga di sini tentang stdbufatau opsi lain.

Albert
sumber
1
Anda menyelamatkan hari saya, Terima kasih untuk PYTHONUNBUFFERED = 1
diewland
9
for line in p.stdout:
  ...

selalu memblokir sampai feed baris berikutnya.

Untuk perilaku "waktu nyata", Anda harus melakukan sesuatu seperti ini:

while True:
  inchar = p.stdout.read(1)
  if inchar: #neither empty string nor None
    print(str(inchar), end='') #or end=None to flush immediately
  else:
    print('') #flush for implicit line-buffering
    break

While-loop ditinggalkan saat proses anak menutup stdout atau keluarnya. read()/read(-1)akan memblokir sampai proses anak menutup stdout atau keluarnya.

IBue
sumber
1
inchartidak pernah Nonedigunakan if not inchar:sebagai gantinya ( read()mengembalikan string kosong pada EOF). btw, Lebih buruk lagi for line in p.stdouttidak mencetak bahkan baris penuh secara realtime dengan Python 2 ( for line in iter (p.stdout.readline, '') `bisa digunakan sebagai gantinya).
jfs
1
Saya telah menguji ini dengan python 3.4 di osx, dan itu tidak berhasil.
qed
1
@qed: for line in p.stdout:bekerja pada Python 3. Pastikan untuk memahami perbedaan antara ''(Unicode string) dan b''(bytes). Lihat Python: membaca input streaming dari subprocess.communicate ()
jfs
8

Masalah Anda adalah:

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()

iterator itu sendiri memiliki buffering ekstra.

Coba lakukan seperti ini:

while True:
  line = p.stdout.readline()
  if not line:
     break
  print line
zviadm
sumber
5

Anda tidak bisa mendapatkan stdout untuk mencetak unbuffered ke pipa (kecuali jika Anda dapat menulis ulang program yang mencetak ke stdout), jadi inilah solusi saya:

Alihkan stdout ke sterr, yang tidak di-buffer. '<cmd> 1>&2'harus melakukannya. Buka prosesnya sebagai berikut: myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
Anda tidak dapat membedakan dari stdout atau stderr, tetapi Anda langsung mendapatkan semua output.

Semoga ini bisa membantu siapa pun yang menangani masalah ini.

Erik
sumber
4
Apakah kamu sudah mencobanya Karena tidak berhasil .. Jika stdout di-buffer dalam proses itu, itu tidak akan dialihkan ke stderr dengan cara yang sama tidak dialihkan ke PIPE atau file ..
Filipe Pina
5
Ini jelas salah. stdout buffering terjadi di dalam program itu sendiri. Sintaks shell 1>&2hanya mengubah file mana yang ditunjuk oleh deskriptor file sebelum meluncurkan program. Program itu sendiri tidak dapat membedakan antara mengalihkan stdout ke stderr ( 1>&2) atau sebaliknya ( 2>&1) jadi ini tidak akan berpengaruh pada perilaku buffering program. Dan cara apa pun 1>&2sintaksnya diinterpretasikan oleh shell. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)akan gagal karena Anda belum menentukan shell=True.
Will Manley
Seandainya orang akan membaca ini: Saya mencoba menggunakan stderr daripada stdout, ini menunjukkan perilaku yang sama persis.
martintselanjutnya
3

Ubah stdout dari proses rsync menjadi unbuffered.

p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=0,  # 0=unbuffered, 1=line-buffered, else buffer-size
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)
Akan
sumber
3
Buffering terjadi di sisi rsync, mengubah atribut bufsize di sisi python tidak akan membantu.
nosklo
14
Bagi siapa pun yang mencari, jawaban nosklo sepenuhnya salah: tampilan kemajuan rsync tidak di-buffer; masalah sebenarnya adalah bahwa subproses mengembalikan objek file dan antarmuka iterator file memiliki buffer internal yang didokumentasikan dengan buruk bahkan dengan bufsize = 0, mengharuskan Anda untuk memanggil readline () berulang kali jika Anda memerlukan hasil sebelum buffer terisi.
Chris Adams
3

Untuk menghindari caching output Anda mungkin ingin mencoba pexpect,

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

PS : Saya tahu pertanyaan ini cukup lama, masih memberikan solusi yang berhasil untuk saya.

PPS : dapatkan jawaban ini dari pertanyaan lain

Nithin
sumber
3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

Saya menulis GUI untuk rsync dengan python, dan memiliki masalah yang sama. Masalah ini mengganggu saya selama beberapa hari hingga saya menemukan ini di pyDoc.

Jika universal_newlines True, file objek stdout dan stderr dibuka sebagai file teks dalam mode baris baru universal. Baris dapat diakhiri oleh salah satu '\ n', konvensi akhir baris Unix, '\ r', konvensi Macintosh lama atau '\ r \ n', konvensi Windows. Semua representasi eksternal ini dilihat sebagai '\ n' oleh program Python.

Tampaknya rsync akan mengeluarkan '\ r' saat terjemahan sedang berlangsung.

xmc
sumber
1

Saya perhatikan bahwa tidak disebutkan menggunakan file sementara sebagai perantara. Berikut ini adalah mengatasi masalah buffering dengan mengeluarkan ke file sementara dan memungkinkan Anda untuk mengurai data yang berasal dari rsync tanpa menghubungkan ke pty. Saya menguji yang berikut ini di kotak linux, dan keluaran rsync cenderung berbeda di seluruh platform, sehingga ekspresi reguler untuk mengurai keluaran dapat bervariasi:

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...
MikeGM
sumber
itu tidak dalam waktu nyata. File tidak menyelesaikan masalah buffering di sisi rsync.
jfs
tempfile.T temporaryFile dapat menghapus dirinya sendiri untuk memudahkan pembersihan jika ada pengecualian
jfs
3
while not p.poll()mengarah ke loop tak terbatas jika subproses berhasil keluar dengan 0, gunakan p.poll() is Nonesebagai gantinya
jfs
Windows mungkin melarang untuk membuka file yang sudah dibuka, jadi open(file_name)mungkin gagal
jfs
1
Saya baru saja menemukan jawaban ini, sayangnya hanya untuk linux, tetapi berfungsi seperti tautan pesona Jadi saya hanya memperpanjang perintah saya sebagai berikut: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argvdan panggil: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) dan sekarang saya dapat membaca tanpa buffering
Arvid Terzibaschian
0

jika Anda menjalankan sesuatu seperti ini di utas dan menyimpan properti ffmpeg_time dalam properti metode sehingga Anda dapat mengaksesnya, itu akan bekerja dengan sangat baik Saya mendapatkan output seperti ini: output seperti jika Anda menggunakan threading di tkinter

input = 'path/input_file.mp4'
output = 'path/input_file.mp4'
command = "ffmpeg -y -v quiet -stats -i \"" + str(input) + "\" -metadata title=\"@alaa_sanatisharif\" -preset ultrafast -vcodec copy -r 50 -vsync 1 -async 1 \"" + output + "\""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
for line in self.process.stdout:
    reg = re.search('\d\d:\d\d:\d\d', line)
    ffmpeg_time = reg.group(0) if reg else ''
    print(ffmpeg_time)
erfan
sumber
-1

Di Python 3, inilah solusinya, yang mengambil perintah dari baris perintah dan memberikan string yang didekodekan secara real-time dengan baik saat diterima.

Penerima ( receiver.py):

import subprocess
import sys

cmd = sys.argv[1:]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in p.stdout:
    print("received: {}".format(line.rstrip().decode("utf-8")))

Contoh program sederhana yang dapat menghasilkan keluaran waktu nyata ( dummy_out.py):

import time
import sys

for i in range(5):
    print("hello {}".format(i))
    sys.stdout.flush()  
    time.sleep(1)

Keluaran:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
watsonic.dll
sumber