przechwytywanie stdout w czasie rzeczywistym z podprocesu

89

Chcę subprocess.Popen()rsync.exe w systemie Windows i wydrukować standardowe wyjście w Pythonie.

Mój kod działa, ale nie rejestruje postępu, dopóki nie zakończy się przesyłanie pliku! Chcę wydrukować postęp dla każdego pliku w czasie rzeczywistym.

Używając Pythona 3.1 teraz, ponieważ słyszałem, powinno być lepsze w obsłudze 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.
źródło
1
(Pochodzi z Google?) Wszystkie PIPE zablokują się, gdy jeden z buforów PIPE zostanie zapełniony i nie zostanie odczytany. np. zakleszczenie stdout po wypełnieniu stderr. Nigdy nie przekazuj PIPE, którego nie masz zamiaru przeczytać.
Nasser Al-Wohaibi
Czy ktoś mógłby wyjaśnić, dlaczego nie można po prostu ustawić stdout na sys.stdout zamiast na subprocess.PIPE?
Mike

Odpowiedzi:

101

Kilka praktycznych zasad subprocess.

  • Nigdy nie używaj shell=True. Niepotrzebnie wywołuje dodatkowy proces powłoki, aby wywołać program.
  • Podczas wywoływania procesów argumenty są przekazywane jako listy. sys.argvw Pythonie jest lista, a więc jest argvw C. Więc przekazać listę do Popen, aby zadzwonić do podprocesów, a nie ciąg.
  • Nie przekierowuj stderrdo a, PIPEgdy go nie czytasz.
  • Nie przekierowuj, stdinkiedy do niego nie piszesz.

Przykład:

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

To powiedziawszy, jest prawdopodobne, że rsync buforuje swoje dane wyjściowe, gdy wykryje, że jest podłączony do potoku zamiast terminala. Jest to zachowanie domyślne - po podłączeniu do potoku programy muszą jawnie opróżniać standardowe wyjście, aby uzyskać wyniki w czasie rzeczywistym, w przeciwnym razie standardowa biblioteka C będzie buforować.

Aby to sprawdzić, spróbuj zamiast tego uruchomić:

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

i utwórz test_out.pyplik z zawartością:

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

Wykonanie tego podprocesu powinno dać ci „Hello” i odczekać 10 sekund przed podaniem „World”. Jeśli tak się stanie z powyższym kodem Pythona, a nie z rsync, oznacza to, że rsyncsam buforuje dane wyjściowe, więc nie masz szczęścia.

Rozwiązaniem byłoby podłączenie się bezpośrednio do pty, używając czegoś takiego jak pexpect.

nosklo
źródło
12
shell=Falsejest słuszna, gdy konstruujesz wiersz poleceń, zwłaszcza z danych wprowadzonych przez użytkownika. Niemniej jednak shell=Truejest również przydatny, gdy otrzymujesz całą linię poleceń z zaufanego źródła (np. Zakodowaną na stałe w skrypcie).
Denis Otkidach
10
@Denis Otkidach: Nie sądzę, żeby to uzasadniało użycie shell=True. Pomyśl o tym - wywołujesz inny proces w swoim systemie operacyjnym, obejmujący alokację pamięci, użycie dysku, planowanie procesora, tylko po to, aby podzielić ciąg ! I jeden, do którego dołączyłeś !! Możesz podzielić w Pythonie, ale i tak łatwiej jest pisać każdy parametr osobno. Ponadto, na podstawie listy oznacza, że nie ma ucieczki znaki specjalne powłoki: przestrzenie, ;, >, <, &.. Twoje parametry mogą zawierać te znaki i nie trzeba się martwić! Naprawdę nie widzę powodu, aby go używać shell=True, chyba że używasz polecenia tylko dla powłoki.
nosklo
nosklo, to powinno być: p = podproces.Popen (cmd, stdout = podproces.PIPE, stderr = subprocess.STDOUT)
Senthil Kumaran
1
@mathtick: Nie jestem pewien, dlaczego miałbyś wykonywać te operacje jako oddzielne procesy ... możesz łatwo wyciąć zawartość pliku i wyodrębnić pierwsze pole w Pythonie za pomocą csvmodułu. Ale na przykład twój potok w Pythonie wyglądałby tak: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultZauważ, że możesz pracować z długimi nazwami plików i znakami specjalnymi powłoki bez konieczności ucieczki, teraz, gdy powłoka nie jest zaangażowana. Jest też dużo szybszy, ponieważ jest o jeden proces mniej.
nosklo
11
użyj for line in iter(p.stdout.readline, b'')zamiast for line in p.stdoutw Pythonie 2, w przeciwnym razie wiersze nie są odczytywane w czasie rzeczywistym, nawet jeśli proces źródłowy nie buforuje swoich danych wyjściowych.
jfs
43

Wiem, że to stary temat, ale teraz jest rozwiązanie. Wywołaj rsync z opcją --outbuf = L. Przykład:

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
źródło
3
To działa i należy za nim głosować, aby przyszli czytelnicy nie musieli przewijać całego powyższego okna dialogowego.
VectorVictor
1
@VectorVictor To nie wyjaśnia, co się dzieje i dlaczego. Może się zdarzyć, że twój program będzie działał, dopóki: 1. nie dodasz, preexec_fn=os.setpgrpaby program przetrwał jego skrypt nadrzędny 2. pominiesz czytanie z potoku procesu 3. proces wyprowadza dużo danych, wypełniając potok 4. utkniesz na wiele godzin , próbując dowiedzieć się, dlaczego uruchomiony program zamyka się po jakimś losowym czasie . Odpowiedź z @nosklo bardzo mi pomogła.
danuker
16

W Linuksie miałem ten sam problem z pozbyciem się buforowania. W końcu użyłem "stdbuf -o0" (lub unbuffer z oczekiwania), aby pozbyć się buforowania PIPE.

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

Mógłbym wtedy użyć select.select na stdout.

Zobacz też /unix/25372/

Molwa
źródło
2
Dla każdego, kto próbuje pobrać standardowe wyjście kodu C z Pythona, mogę potwierdzić, że to rozwiązanie było jedynym, które działało dla mnie. Żeby było jasne, mówię o dodaniu „stdbuf”, „-o0” do mojej istniejącej listy poleceń w Popen.
Reckless
Dziękuję Ci! stdbuf -o0okazał się bardzo przydatny z kilkoma testami pytest / pytest-bdd Napisałem, że spawnujemy aplikację w C ++ i sprawdzam, czy emituje określone instrukcje dziennika. Bez stdbuf -o0tych testów te testy potrzebowały 7 sekund, aby uzyskać (buforowane) dane wyjściowe z programu C ++. Teraz działają niemal natychmiast!
evadeflow
Ta odpowiedź uratowała mnie dzisiaj! Uruchamiając aplikację jako podprocesy w ramach pytest, nie mogłem uzyskać jej wyniku. stdbufczy to.
Janos
14

W zależności od przypadku użycia możesz również chcieć wyłączyć buforowanie w samym podprocesie.

Jeśli podproces będzie procesem w Pythonie, możesz to zrobić przed wywołaniem:

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

Lub alternatywnie przekaż to w envargumencie do Popen.

W przeciwnym razie, jeśli korzystasz z systemu Linux / Unix, możesz użyć stdbufnarzędzia. Np. Jak:

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

Zobacz również tutaj o stdbuflub inne opcje.

Albert
źródło
1
Ratujesz mój dzień, dzięki za PYTHONUNBUFFERED = 1
diewland
9
for line in p.stdout:
  ...

zawsze blokuje do następnego wysunięcia wiersza.

Aby zachować zachowanie w czasie rzeczywistym, musisz zrobić coś takiego:

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

Pętla while jest pozostawiana, gdy proces potomny zamyka swoje standardowe wyjście lub kończy pracę. read()/read(-1)będzie blokować, dopóki proces potomny nie zamknie swojego wyjścia standardowego lub nie zakończy działania.

IBue
źródło
1
incharnigdy nie jest Noneużywany if not inchar:( read()zwraca pusty ciąg w EOF). btw, Gorzej for line in p.stdout, nie drukuje nawet pełnych wierszy w czasie rzeczywistym w Pythonie 2 ( for line in zamiast tego można użyć iter (p.stdout.readline, '') ``).
jfs
1
Przetestowałem to w Pythonie 3.4 na OSX i nie działa.
okazania
1
@qed: for line in p.stdout:działa na Pythonie 3. Upewnij się, że rozumiesz różnicę między ''(ciąg znaków Unicode) a b''(bajty). Zobacz Python: read streaming input from subprocess.communicate ()
jfs
8

Twój problem to:

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

sam iterator ma dodatkowe buforowanie.

Spróbuj zrobić tak:

while True:
  line = p.stdout.readline()
  if not line:
     break
  print line
zviadm
źródło
5

Nie możesz zmusić stdout do drukowania niebuforowanego do potoku (chyba że możesz przepisać program, który drukuje na stdout), więc oto moje rozwiązanie:

Przekieruj standardowe wyjście do sterr, które nie jest buforowane. '<cmd> 1>&2'powinien to zrobić. Otwórz proces w następujący sposób: myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
Nie możesz odróżnić od stdout lub stderr, ale wszystkie dane wyjściowe otrzymasz natychmiast.

Mam nadzieję, że pomoże to każdemu rozwiązującemu ten problem.

Erik
źródło
4
Próbowałeś tego? Ponieważ to nie działa ... Jeśli stdout jest buforowane w tym procesie, nie zostanie przekierowane na stderr w ten sam sposób, w jaki nie jest przekierowane do PIPE lub pliku ..
Filipe Pina
5
To jest po prostu błędne. buforowanie stdout występuje w samym programie. Składnia powłoki 1>&2zmienia tylko pliki, na które wskazują deskryptory plików, przed uruchomieniem programu. Sam program nie jest w stanie rozróżnić przekierowania stdout do stderr ( 1>&2) lub odwrotnie ( 2>&1), więc nie będzie to miało wpływu na zachowanie programu podczas buforowania. Tak czy inaczej, 1>&2składnia jest interpretowana przez powłokę. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)nie powiedzie się, ponieważ nie określono shell=True.
Will Manley,
Na wypadek, gdyby ludzie czytali to: próbowałem użyć stderr zamiast stdout, pokazuje dokładnie to samo zachowanie.
martinthenext
3

Zmień standardowe wyjście z procesu rsync na niebuforowane.

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)
Wola
źródło
3
Buforowanie odbywa się po stronie rsync, zmiana atrybutu bufsize po stronie Pythona nie pomoże.
nosklo
14
Dla każdego, kto szuka, odpowiedź nosklo jest całkowicie błędna: wyświetlanie postępu rsync nie jest buforowane; prawdziwym problemem jest to, że podproces zwraca obiekt pliku, a interfejs iteratora pliku ma słabo udokumentowany bufor wewnętrzny, nawet z bufsize = 0, co wymaga wielokrotnego wywoływania readline (), jeśli potrzebujesz wyników przed zapełnieniem bufora.
Chris Adams
3

Aby uniknąć buforowania wyjścia, możesz spróbować pexpect,

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

PS : Wiem, że to pytanie jest dość stare, wciąż dostarcza rozwiązania, które działało dla mnie.

PPS : otrzymałem tę odpowiedź z innego pytania

Nithin
źródło
3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

Piszę GUI dla rsync w Pythonie i mam te same problemy. Ten problem niepokoi mnie od kilku dni, dopóki nie znajdę go w pyDoc.

Jeśli universal_newlines ma wartość True, obiekty pliku stdout i stderr są otwierane jako pliki tekstowe w trybie uniwersalnych znaków nowej linii. Linie mogą być zakończone dowolną z „\ n”, uniksowej konwencji końca linii, „\ r”, starej konwencji Macintosha lub „\ r \ n”, konwencji Windows. Wszystkie te zewnętrzne reprezentacje są postrzegane jako „\ n” przez program w języku Python.

Wygląda na to, że rsync wyświetli '\ r', gdy trwa translacja.

xmc
źródło
1

Zauważyłem, że nie ma wzmianki o używaniu pliku tymczasowego jako pośredniego. Poniższe informacje omijają problemy z buforowaniem, przesyłając dane do pliku tymczasowego i umożliwiają analizowanie danych pochodzących z rsync bez łączenia się z pty. Przetestowałem następujące elementy na Linuksie, a dane wyjściowe rsync różnią się na różnych platformach, więc wyrażenia regularne do analizowania danych wyjściowych mogą się różnić:

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
źródło
to nie jest w czasie rzeczywistym. Plik nie rozwiązuje problemu z buforowaniem po stronie rsync.
jfs
tempfile.TemporaryFile może usunąć się w celu łatwiejszego czyszczenia w przypadku wyjątków
jfs
3
while not p.poll()prowadzi do nieskończonej pętli, jeśli podproces zakończy się pomyślnie z 0, użyj p.poll() is Nonezamiast tego
jfs
Windows może zabronić otwierania już otwartego pliku, więc open(file_name)może się nie powieść
jfs
1
Właśnie znalazłem tę odpowiedź, niestety tylko dla linuxa, ale działa jak urok linku Więc po prostu rozszerzam moje polecenie w następujący sposób: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argvi dzwonię: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) i teraz mogę czytać bez buforowania
Arvid Terzibaschian
0

jeśli uruchomisz coś takiego w wątku i zapiszesz właściwość ffmpeg_time we właściwości metody, aby uzyskać do niej dostęp, działałoby bardzo dobrze, otrzymuję takie wyniki: wyjście będzie takie, jak gdybyś używał wątków w 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
źródło
-1

W Pythonie 3 jest rozwiązanie, które usuwa polecenie z wiersza poleceń i dostarcza ładnie dekodowane ciągi w czasie rzeczywistym, gdy są odbierane.

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

Przykładowy prosty program, który może generować dane wyjściowe w czasie rzeczywistym ( dummy_out.py):

import time
import sys

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

Wynik:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
watsonic
źródło