odczyt podprocesu stdout linia po linii

Mój skrypt w Pythonie używa podprocesu, aby wywołać narzędzie linuksowe, które jest bardzo hałaśliwe. Chcę zapisać wszystkie dane wyjściowe do pliku dziennika i pokazać część z nich użytkownikowi. Myślałem, że to zadziała, ale wyjście nie pojawia się w mojej aplikacji, dopóki narzędzie nie wytworzy znaczącej 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()

Zachowanie, którego naprawdę chcę, polega na tym, aby skrypt filtra drukował każdą linię, gdy jest ona odbierana z podprocesu. Jakby co tee robi ale z Pythonem kod.

Co mi umyka? Czy to w ogóle możliwe?

Update:

Jeśli dodaje się sys.stdout.flush() 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życie proc.stdout.xreadlines() działa tak samo jak py3k, ale tak nie jest.


Aktualizacja 2:

Oto minimalny kod roboczy.

#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()
Author: deft_code, 2010-05-10

6 answers

Minęło sporo czasu, odkąd ostatnio pracowałem z Pythonem, ale myślę, że problem jest z instrukcją for line in proc.stdout, która odczytuje całe dane wejściowe przed iteracją. Rozwiązaniem jest użycie readline() zamiast:

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

Oczywiście nadal musisz poradzić sobie z buforowaniem podprocesu.

Uwaga: zgodnie z dokumentacją Rozwiązanie z iteratorem powinno być równoważne z użyciem readline(), z wyjątkiem bufora do odczytu, ale (Lub właśnie z tego powodu) proponowane zmiana dała mi Inne wyniki (Python 2.5 NA Windows XP).

 142
Author: Rômulo Ceccon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-05-11 18:56:24

Trochę za późno na imprezę, ale zdziwiło mnie, że nie widzę tu 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
 26
Author: jbg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-22 03:56:27

Rzeczywiście, jeśli rozwiązałeś iterator, buforowanie może być teraz Twoim problemem. Możesz powiedzieć pythonowi w podprocesie, aby nie buforował swoich 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.

 16
Author: Steve Carter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-29 16:36:03

Chcesz przekazać te dodatkowe parametry subprocess.Popen:

bufsize=1, universal_newlines=True

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

 10
Author: user1747134,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-11 18:11:23

Następująca modyfikacja odpowiedzi Rômulo działa dla mnie na 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 != b'':
    os.write(1, line)
  else:
    break
 1
Author: mdh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-02 17:14:13

Próbowałem tego z python3 i zadziałało, source

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()
 0
Author: shakram02,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-21 12:00:42