czytaj standardowe wyjście podprocesu linia po linii

235

Mój skrypt Pythona używa podprocesu do wywołania bardzo głośnego narzędzia linux. Chcę zapisać wszystkie dane wyjściowe w pliku dziennika i pokazać niektóre z nich użytkownikowi. Myślałem, że następujące działania będą działać, ale dane wyjściowe nie są wyświetlane w mojej aplikacji, dopóki narzędzie nie wygeneruje znacznej ilości danych wyjściowych.

#fake_utility.py, just generates lots of output over time
import time
i = 0
while True:
   print hex(i)*512
   i += 1
   time.sleep(0.5)

#filters output
import subprocess
proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)
for line in proc.stdout:
   #the real code does filtering here
   print "test:", line.rstrip()

Naprawdę chcę, aby skrypt filtrujący wypisał każdą linię otrzymaną z podprocesu. Trochę jak to, co teerobi, ale z kodem python.

czego mi brakuje? Czy to w ogóle możliwe?


Aktualizacja:

Jeśli a sys.stdout.flush()zostanie dodane do fake_utility.py, kod ma pożądane zachowanie w Pythonie 3.1. Używam Pythona 2.6. Można by pomyśleć, że używanie proc.stdout.xreadlines()działałoby tak samo jak py3k, ale tak nie jest.


Aktualizacja 2:

Oto minimalny działający kod.

#fake_utility.py, just generates lots of output over time
import sys, time
for i in range(10):
   print i
   sys.stdout.flush()
   time.sleep(0.5)

#display out put line by line
import subprocess
proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)
#works in python 3.0+
#for line in proc.stdout:
for line in iter(proc.stdout.readline,''):
   print line.rstrip()
deft_code
źródło
4
możesz użyć print line,zamiast print line.rstrip()(uwaga: przecinek na końcu).
jfs
2
Aktualizacja 2 stwierdza, że ​​działa z Pythonem 3.0+, ale korzysta ze starej instrukcji print, więc nie działa z Pythonem 3.0+.
Rooky
Żadna z wymienionych tutaj odpowiedzi nie działała dla mnie, ale stackoverflow.com/questions/5411780/ ... zadziałało !
zapakowane

Odpowiedzi:

179

Minęło dużo czasu, odkąd ostatnio pracowałem z Pythonem, ale myślę, że problem dotyczy instrukcji for line in proc.stdout, która odczytuje cały wkład przed iteracją. Rozwiązaniem jest użycie readline()zamiast tego:

#filters output
import subprocess
proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)
while True:
  line = proc.stdout.readline()
  if not line:
    break
  #the real code does filtering here
  print "test:", line.rstrip()

Oczywiście nadal masz do czynienia z buforowaniem podprocesu.

Uwaga: zgodnie z dokumentacją rozwiązanie z iteratorem powinno być równoważne z używaniem readline(), z wyjątkiem bufora do odczytu z wyprzedzeniem, ale (lub właśnie z tego powodu) proponowana zmiana dała mi różne wyniki (Python 2.5 w systemie Windows XP).

Rômulo Ceccon
źródło
11
dla file.readline()vs. for line in filepatrz bugs.python.org/issue3907 (w skrócie: działa na Python3; użyj io.open()na Python 2.6+)
jfs
5
Bardziej pythonowym testem dla EOF, zgodnie z „Zaleceniami programistycznymi” w PEP 8 ( python.org/dev/peps/pep-0008 ), byłoby „jeśli nie linia:”.
Jason Mock
14
@naxa: rur: for line in iter(proc.stdout.readline, ''):.
jfs
3
@ Jan-PhilipGehrcke: tak. 1. możesz użyć for line in proc.stdoutw Pythonie 3 (nie ma błędu odczytu z wyprzedzeniem) 2. '' != b''w Pythonie 3 - nie kopiuj i nie wklejaj kodu na ślepo - pomyśl, co robi i jak działa.
jfs
2
@JFSebastian: jasne, iter(f.readline, b'')rozwiązanie jest dość oczywiste (i działa również na Pythonie 2, jeśli ktoś jest zainteresowany). Celem mojego komentarza nie było obwinianie twojego rozwiązania (przepraszam, jeśli tak wyglądało, ja też to teraz czytam!), Ale opisanie zakresu objawów, które w tym przypadku są dość poważne (większość Py2 / 3 problemy powodują wyjątki, podczas gdy dobrze zachowująca się pętla zmieniła się na nieskończoną, a odśmiecanie walczy z powodzią nowo tworzonych obiektów, powodując oscylacje wykorzystania pamięci o długim okresie i dużej amplitudzie).
Dr Jan-Philip Gehrcke
45

Trochę późno na imprezę, ale zdziwiło mnie, że nie widzę tutaj najprostszego rozwiązania:

import io
import subprocess

proc = subprocess.Popen(["prog", "arg"], stdout=subprocess.PIPE)
for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):  # or another encoding
    # do something with line

(Wymaga to Python 3.)

jbg
źródło
25
Chciałbym skorzystać z tej odpowiedzi, ale otrzymuję: AttributeError: 'file' object has no attribute 'readable' py2.7
Dan Garthwaite
3
Działa z python 3
matanster
Oczywiście ten kod nie jest ważny z wielu powodów kompatybilności py3 / py3 i realnego ryzyka uzyskania ValueError: operacja I / O na zamkniętym pliku
sorin
3
@sorin żadna z tych rzeczy nie powoduje, że jest „nieważny”. Jeśli piszesz bibliotekę, która nadal musi obsługiwać Python 2, nie używaj tego kodu. Ale wiele osób ma luksus korzystania z oprogramowania wydanego niedawno ponad dziesięć lat temu. Jeśli spróbujesz czytać na zamkniętym pliku, otrzymasz ten wyjątek, niezależnie od tego, czy używasz, TextIOWrapperczy nie. Możesz po prostu obsłużyć wyjątek.
jbg
1
być może spóźniasz się na imprezę, ale odpowiadasz na aktualną wersję Pythona, ty
Dusan Gligoric
20

Rzeczywiście, jeśli uporządkowałeś iterator, buforowanie może być teraz twoim problemem. Można powiedzieć pythonowi w podprocesie, aby nie buforował danych wyjściowych.

proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)

staje się

proc = subprocess.Popen(['python','-u', 'fake_utility.py'],stdout=subprocess.PIPE)

Potrzebowałem tego podczas wywoływania Pythona z poziomu Pythona.

Steve Carter
źródło
14

Chcesz przekazać te dodatkowe parametry do subprocess.Popen:

bufsize=1, universal_newlines=True

Następnie możesz iterować jak w twoim przykładzie. (Testowane z Python 3.5)

użytkownik1747134
źródło
2
@nicoulaj Powinno działać, jeśli używasz pakietu subprocess32.
Quantum7
4

Funkcja, która pozwala na iterację po obu stdoutistderr jednocześnie, w czasie rzeczywistym, linia po linii

Jeśli potrzebujesz uzyskać strumień wyjściowy dla obu stdouti stderrjednocześnie, możesz użyć następującej funkcji.

Funkcja używa kolejek do łączenia obu potoków Popen w jeden iterator.

Tutaj tworzymy funkcję read_popen_pipes():

from queue import Queue, Empty
from concurrent.futures import ThreadPoolExecutor


def enqueue_output(file, queue):
    for line in iter(file.readline, ''):
        queue.put(line)
    file.close()


def read_popen_pipes(p):

    with ThreadPoolExecutor(2) as pool:
        q_stdout, q_stderr = Queue(), Queue()

        pool.submit(enqueue_output, p.stdout, q_stdout)
        pool.submit(enqueue_output, p.stderr, q_stderr)

        while True:

            if p.poll() is not None and q_stdout.empty() and q_stderr.empty():
                break

            out_line = err_line = ''

            try:
                out_line = q_stdout.get_nowait()
            except Empty:
                pass
            try:
                err_line = q_stderr.get_nowait()
            except Empty:
                pass

            yield (out_line, err_line)

read_popen_pipes() w użyciu:

import subprocess as sp


with sp.Popen(my_cmd, stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    for out_line, err_line in read_popen_pipes(p):

        # Do stuff with each line, e.g.:
        print(out_line, end='')
        print(err_line, end='')

    return p.poll() # return status-code
Rotareti
źródło
2

Możesz także odczytać linie bez pętli. Działa w python3.6.

import os
import subprocess

process = subprocess.Popen(command, stdout=subprocess.PIPE)
list_of_byte_strings = process.stdout.readlines()
biorąc pod uwagę
źródło
1
Lub przekonwertować na ciągi:list_of_strings = [x.decode('utf-8').rstrip('\n') for x in iter(process.stdout.readlines())]
ndtreviv
1

Próbowałem tego z python3 i działało, źródło

def output_reader(proc):
    for line in iter(proc.stdout.readline, b''):
        print('got line: {0}'.format(line.decode('utf-8')), end='')


def main():
    proc = subprocess.Popen(['python', 'fake_utility.py'],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)

    t = threading.Thread(target=output_reader, args=(proc,))
    t.start()

    try:
        time.sleep(0.2)
        import time
        i = 0

        while True:
        print (hex(i)*512)
        i += 1
        time.sleep(0.5)
    finally:
        proc.terminate()
        try:
            proc.wait(timeout=0.2)
            print('== subprocess exited with rc =', proc.returncode)
        except subprocess.TimeoutExpired:
            print('subprocess did not terminate in time')
    t.join()
shakram02
źródło
1

Następująca modyfikacja odpowiedzi Rômulo działa dla mnie w Pythonie 2 i 3 (2.7.12 i 3.6.1):

import os
import subprocess

process = subprocess.Popen(command, stdout=subprocess.PIPE)
while True:
  line = process.stdout.readline()
  if line != '':
    os.write(1, line)
  else:
    break
mdh
źródło
0

Nie wiem, kiedy zostało to dodane do modułu podprocesu, ale w Pythonie 3 powinieneś używać proc.stdout.splitlines():

for line in proc.stdout.splitlines():
   print "stdout:", line
StefanQ
źródło