stdout'u alt işlemden gerçek zamanlı olarak yakalama


89

subprocess.Popen()Windows'ta rsync.exe'yi ve Python'da stdout'u yazdırmak istiyorum .

Kodum çalışıyor, ancak bir dosya aktarımı tamamlanana kadar ilerlemeyi yakalayamıyor! Her dosya için ilerlemeyi gerçek zamanlı olarak yazdırmak istiyorum.

Python 3.1'i şimdi kullanmanın, IO'yu işlemede daha iyi olacağını duydum.

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


1
(Google'dan mı geliyor?) Tüm PIPE'ler, PIPE'lerin tamponlarından biri dolduğunda ve okunmadığında kilitlenecektir. örneğin stderr doldurulduğunda stdout kilitlenme. Okumak istemediğiniz bir BORU'yı asla geçmeyin.
Nasser Al-Wohaibi

Birisi neden stdout'u subprocess.PIPE yerine sys.stdout olarak ayarlayamadığınızı açıklayabilir mi?
Mike

Yanıtlar:


101

Bazı pratik kurallar subprocess.

  • Asla kullanma shell=True. Programınızı çağırmak için gereksiz yere fazladan bir kabuk işlemi başlatır.
  • Süreçleri çağırırken, argümanlar liste olarak iletilir. sys.argvPython'da bir listedir ve argvC'de de böyledir . Yani alt süreçleri çağırmak için bir liste iletirsiniz Popen, dizge değil.
  • Yönlendirmeyin stderra PIPEbunu okurken değilken.
  • Yazmadığınız stdinzamanlarda yeniden yönlendirmeyin .

Misal:

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

Bununla birlikte, rsync'in bir terminal yerine bir boruya bağlı olduğunu algıladığında çıktısını tamponlaması muhtemeldir. Bu varsayılan davranıştır - bir boruya bağlandığında, programlar gerçek zamanlı sonuçlar için stdout'u açıkça yıkamalıdır, aksi takdirde standart C kitaplığı arabelleğe alınır.

Bunu test etmek için bunun yerine şunu çalıştırmayı deneyin:

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

ve test_out.pyiçeriğe sahip bir dosya oluşturun :

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

Bu alt işlemi yürütmek size "Merhaba" vermeli ve "Dünya" vermeden önce 10 saniye beklemelidir. Bu, yukarıdaki python koduyla gerçekleşirse ve bununla birlikte olmazsa rsync, bu rsync, çıktının arabelleğe alındığı anlamına gelir , yani şansınız kalmaz .

Bir çözüm pty, gibi bir şey kullanarak doğrudan a'ya bağlanmak olabilir pexpect.


12
shell=Falseözellikle kullanıcı tarafından girilen verilerden komut satırı oluşturduğunuzda doğru olanıdır. Ancak yine de shell=True, komut satırının tamamını güvenilir kaynaktan aldığınızda (örneğin komut dosyasında kodlanmış) yararlıdır.
Denis Otkidach

10
@Denis Otkidach: Bunun kullanımını garanti ettiğini sanmıyorum shell=True. Bir düşünün - işletim sisteminizde bellek ayırma, disk kullanımı, işlemci planlamasını içeren, sırf bir dizeyi bölmek için başka bir işlemi başlatıyorsunuz ! Ve sen kendin katıldın !! Python'a bölünebilirsin, ama yine de her parametreyi ayrı ayrı yazmak daha kolay. Ayrıca, araçlar listesini kullanarak özel kabuk karakter kaçmak zorunda değilsiniz: alanlar, ;, >, <, &.. Parametreleriniz bu karakter içerebilir ve endişe gerekmez! shell=TrueYalnızca kabuk komutunu çalıştırmadığınız sürece kullanmak için bir neden göremiyorum .
nosklo

nosklo, bu şöyle olmalıdır: p = alt süreç.Popen (cmd, stdout = alt işlem.PIPE, stderr = alt işlem.STDOUT)
Senthil Kumaran

1
@mathtick: Bu işlemleri neden ayrı işlemler olarak yaparsınız bilmiyorum ... csvmodülü kullanarak dosya içeriklerini kesebilir ve python'da ilk alanı kolayca çıkarabilirsiniz . Ancak bir örnek olarak, python'daki ardışık düzeniniz şöyle olacaktır: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultKabuk dahil olmadığına göre, uzun dosya adlarıyla ve kabuk özel karakterlerle kaçmak zorunda kalmadan çalışabileceğinizi unutmayın. Ayrıca bir işlem daha az olduğu için çok daha hızlıdır.
nosklo

11
kullanmak for line in iter(p.stdout.readline, b'')yerine for line in p.stdoutPython 2'de aksi çizgiler kaynak sürecinin çıktısı tampon olmasa bile gerçek zamanlı olarak okunmaz.
jfs

43

Bunun eski bir konu olduğunu biliyorum ama şimdi bir çözüm var. --Outbuf = L seçeneğiyle rsync çağırın. Misal:

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

3
Bu çalışır ve gelecekteki okuyucuların yukarıdaki iletişim kutusunun tamamını kaydırmasını önlemek için desteklenmelidir.
VectorVictor

1
@VectorVictor Neler olduğunu ve neden olduğunu açıklamıyor. Aşağıdakilere kadar programınız çalışıyor olabilir: 1. preexec_fn=os.setpgrpprogramı ana betiğinden kurtulmak için eklersiniz 2. işlemin borusundan okumayı atlarsınız 3. işlem boruyu doldurarak çok sayıda veri üretir 4. saatlerce takılıp kalırsınız , çalıştırdığınız programın rastgele bir süre sonra neden kapandığını anlamaya çalışmak . @Nosklo'nun cevabı bana çok yardımcı oldu.
danuker

16

Linux'ta, ara belleğe alma işleminden kurtulma konusunda aynı problemi yaşadım. Sonunda, PIPE tamponlamasından kurtulmak için "stdbuf -o0" (veya beklediğimden tamponu kaldır) kullandım.

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

Daha sonra stdout'ta select.select'i kullanabilirim.

Ayrıca bkz. Https://unix.stackexchange.com/questions/25372/


2
Python'dan C kodunu almaya çalışan herkes için, bu çözümün benim için çalışan tek çözüm olduğunu doğrulayabilirim. Açık olmak gerekirse, Popen'deki mevcut komut listeme 'stdbuf', '-o0' eklemekten bahsediyorum.
Reckless

Teşekkür ederim! Bir dizi pytest / pytest-bdd testiyle gerçekten yararlı stdbuf -o0olduğu kanıtlandı. Bir C ++ uygulaması oluşturan ve belirli günlük ifadelerini yayınladığını doğrulayan yazdım. Olmadan , bu testlerin C ++ programından (arabelleğe alınmış) çıktı almak için 7 saniyeye ihtiyacı vardı. Şimdi neredeyse anında çalışıyorlar! stdbuf -o0
evadeflow

Bu cevap beni bugün kurtardı! Bir uygulamayı bir parçası olarak alt işlemler olarak çalıştırmak pytest, çıktısını almam imkansızdı. stdbufyapar.
Janos

14

Kullanım durumuna bağlı olarak, alt işlemin kendisinde de arabelleğe almayı devre dışı bırakmak isteyebilirsiniz.

Alt süreç bir Python süreci olacaksa, bunu aramadan önce yapabilirsiniz:

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

Veya alternatif olarak bunu envargümanda iletin Popen.

Aksi takdirde, Linux / Unix üzerindeyseniz, stdbufaracı kullanabilirsiniz . Örneğin:

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

Ayrıca bkz burada yaklaşık stdbufveya diğer seçenekler.


1
Günümü kurtardın, Teşekkürler PYTHONUNBUFFERED = 1
diewland

9
for line in p.stdout:
  ...

her zaman bir sonraki satır beslemeye kadar engeller.

"Gerçek zamanlı" davranış için şuna benzer bir şey yapmanız gerekir:

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

Alt süreç stdout'unu kapattığında veya çıktığında while döngüsü bırakılır. read()/read(-1)alt süreç stdout'unu kapatana veya çıkana kadar engellenir.


1
incharbunun yerine asla Nonekullanılmaz if not inchar:( read()EOF'de boş dizge döndürür). btw, for line in p.stdoutPython 2'de tam satırları bile gerçek zamanlı olarak yazdırmaz ( for line in bunun yerine iter (p.stdout.readline, '') `kullanılabilir).
jfs

1
Bunu osx üzerinde python 3.4 ile test ettim ve çalışmıyor.
2014

1
@qed: for line in p.stdout:Python 3 üzerinde çalışır. ''(Unicode dizesi) ve b''(bayt) arasındaki farkı anladığınızdan emin olun . Python'a
jfs

8

Senin sorunun:

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

yineleyicinin kendisi fazladan arabelleğe alma özelliğine sahiptir.

Bunu yapmayı dene:

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

5

Bir boruya arabelleksiz yazdırmak için standart çıktı alamazsınız (standart çıktıya yazdıran programı yeniden yazamazsanız), işte benim çözümüm:

Stdout'u arabelleğe alınmayan sterr'e yönlendirin. '<cmd> 1>&2'yapmalı. İşlemi şu şekilde açın:myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
stdout veya stderr'yi ayırt edemezsiniz, ancak tüm çıktıları anında alırsınız.

Umarım bu, bu sorunu çözen herkese yardımcı olur.


4
Bunu denediniz mi? Çünkü işe yaramıyor .. Eğer stdout bu süreçte arabelleğe alınırsa, bir PIPE veya dosyaya yönlendirilmediği gibi stderr'e yeniden yönlendirilmez.
Filipe Pina

5
Bu çok yanlış. stdout arabelleğe alma, programın içinde gerçekleşir. Kabuk sözdizimi, 1>&2yalnızca dosya tanımlayıcıların programı başlatmadan önce işaret ettiği dosyaları değiştirir. Programın kendisi, stdout'u stderr ( 1>&2) 'e veya tersi ( 2>&1)' ye yeniden yönlendirmeyi ayırt edemez, bu nedenle bu, programın arabelleğe alma davranışı üzerinde hiçbir etkiye sahip olmayacaktır ve 1>&2sözdizimi her iki şekilde de kabuk tarafından yorumlanır. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)başarısız olur çünkü belirtmediniz shell=True.
Will Manley

İnsanlar bunu okursa: stdout yerine stderr kullanmayı denedim, aynı davranışı gösteriyor.
martinthenext

3

Stdout'u rsync işleminden arabelleğe alınmayacak şekilde değiştirin.

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)

3
Arabellekleme rsync tarafında gerçekleşir, python tarafında bufsize özniteliğini değiştirmek yardımcı olmaz.
nosklo

14
Arama yapan başka biri için nosklo'nun cevabı tamamen yanlış: rsync'in ilerleme göstergesi ara belleğe alınmamış; gerçek sorun, alt işlemin bir dosya nesnesi döndürmesidir ve dosya yineleyici arabirimi, bufsize = 0 olsa bile, yetersiz şekilde belgelenmiş bir dahili arabelleğe sahiptir, bu, arabellek dolmadan önce sonuçlara ihtiyacınız varsa, readline () 'ı tekrar tekrar çağırmanızı gerektirir.
Chris Adams

3

Çıktının önbelleğe alınmasını önlemek için pexpect'i denemek isteyebilirsiniz,

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

Not : Bu sorunun oldukça eski olduğunu biliyorum, hala benim için işe yarayan çözümü sağlıyor.

PPS : bu yanıtı başka bir sorudan aldım


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

Python'da rsync için bir GUI yazıyorum ve aynı sorunlara sahibim. Bu problem, bunu pyDoc'ta bulana kadar beni birkaç gün rahatsız etti.

Universal_newlines True ise, stdout ve stderr dosya nesneleri evrensel yeni satırlar modunda metin dosyaları olarak açılır. Satırlar, '\ n', Unix satır sonu kuralı, '\ r', eski Macintosh kuralı veya '\ r \ n' Windows kurallarından herhangi biri tarafından sonlandırılabilir. Tüm bu dış temsiller Python programı tarafından "\ n" olarak görülür.

Görünüşe göre translate devam ederken rsync '\ r' sonucunu verecek.


1

Ara dosya olarak geçici bir dosyanın kullanılmasından söz edilmediğini fark ettim. Aşağıdakiler, geçici bir dosyaya çıktı göndererek arabelleğe alma sorunlarını aşar ve bir pty'ye bağlanmadan rsync'den gelen verileri ayrıştırmanıza izin verir. Aşağıdakileri bir linux kutusunda test ettim ve rsync'in çıktısı platformlar arasında farklılık gösteriyor, bu nedenle çıktıyı ayrıştırmak için normal ifadeler değişebilir:

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

gerçek zamanlı değil. Bir dosya rsync tarafındaki arabelleğe alma sorununu çözmez.
jfs

tempfile.TporaryFile istisnalar durumunda daha kolay temizlik için kendini silebilir
jfs

3
while not p.poll()alt işlem 0 ile başarılı bir şekilde çıkarsa sonsuz döngüye yol açar, p.poll() is Nonebunun yerine
jfs

Windows önceden açılmış dosyayı açmayı yasaklayabilir, bu nedenle open(file_name) başarısız olabilir
jfs

1
Maalesef bu cevabı buldum, maalesef sadece linux için, ancak bir cazibe bağlantısı gibi çalışıyor Bu yüzden komutumu aşağıdaki gibi genişletiyorum: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argvve çağır: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) ve artık arabelleğe alma olmadan okuyabiliyorum
Arvid Terzibaschian

0

Bir iş parçacığında buna benzer bir şey çalıştırırsanız ve ffmpeg_time özelliğini bir yöntemin özelliğine kaydederseniz, ona erişebilirsiniz, çok iyi çalışır, bunun gibi çıktılar alırım: tkinter'de threading kullanırsanız çıktı gibi olur

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)

-1

Python 3'te, komut satırından bir komut alan ve alınırken gerçek zamanlı güzel bir şekilde çözülmüş dizeler sağlayan bir çözüm var.

Alıcı (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")))

Gerçek zamanlı çıktı üretebilen örnek basit program (dummy_out.py ):

import time
import sys

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

Çıktı:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.