leer el subproceso stdout línea por línea

235

Mi script de Python usa un subproceso para llamar a una utilidad de Linux que es muy ruidosa. Quiero almacenar todo el resultado en un archivo de registro y mostrarlo al usuario. Pensé que lo siguiente funcionaría, pero el resultado no aparece en mi aplicación hasta que la utilidad haya producido una cantidad significativa de resultados.

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

El comportamiento que realmente quiero es que el script de filtro imprima cada línea tal como se recibe del subproceso. Sorta como lo que teehace pero con código python.

¿Qué me estoy perdiendo? ¿Es esto posible?


Actualizar:

Si sys.stdout.flush()se agrega a fake_utility.py, el código tiene el comportamiento deseado en python 3.1. Estoy usando python 2.6. Se podría pensar que usar proc.stdout.xreadlines()funcionaría igual que py3k, pero no es así.


Actualización 2:

Aquí está el código de trabajo mínimo.

#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()
código_deft
fuente
44
podría usar en print line,lugar de print line.rstrip()(nota: coma al final).
jfs el
2
La actualización 2 indica que funciona con python 3.0+ pero usa la antigua declaración de impresión, por lo que no funciona con python 3.0+.
Novato
Ninguna de las respuestas enumeradas aquí funcionó para mí, pero stackoverflow.com/questions/5411780/… lo hizo.
caja el

Respuestas:

179

Ha pasado mucho tiempo desde la última vez que trabajé con Python, pero creo que el problema es con la declaración for line in proc.stdout, que lee toda la entrada antes de repetirla. La solución es usar readline()en su lugar:

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

Por supuesto, todavía tiene que lidiar con el almacenamiento en búfer del subproceso.

Nota: de acuerdo con la documentación, la solución con un iterador debería ser equivalente a usar readline(), excepto el búfer de lectura anticipada, pero (o exactamente por esto) el cambio propuesto produjo resultados diferentes para mí (Python 2.5 en Windows XP).

Rômulo Ceccon
fuente
11
para file.readline()vs. for line in filever bugs.python.org/issue3907 (en resumen: se trabaja en python3, el uso io.open()de Python 2.6+)
JFS
55
La prueba más pitónica para un EOF, según las "Recomendaciones de programación" en PEP 8 ( python.org/dev/peps/pep-0008 ), sería 'si no línea:'.
Jason Mock el
14
@naxa: para tuberías: for line in iter(proc.stdout.readline, ''):.
jfs
3
@ Jan-PhilipGehrcke: sí. 1. podría usar for line in proc.stdouten Python 3 (no existe el error de lectura anticipada) 2. '' != b''en Python 3 - no copie y pegue el código a ciegas - piense qué hace y cómo funciona.
jfs
2
@JFSebastian: claro, la iter(f.readline, b'')solución es bastante obvia (y también funciona en Python 2, si alguien está interesado). El punto de mi comentario no fue culpar a su solución (perdón si parecía así, ¡también lo leí ahora!), Sino describir la extensión de los síntomas, que son bastante graves en este caso (la mayoría de las Py2 / 3 problemas resultan en excepciones, mientras que aquí un bucle de buen comportamiento cambió para ser interminable, y la recolección de basura lucha contra la inundación de objetos recién creados, produciendo oscilaciones de uso de memoria con un período largo y gran amplitud).
Dr. Jan-Philip Gehrcke
45

Un poco tarde a la fiesta, pero me sorprendió no ver cuál es la solución más simple aquí:

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

(Esto requiere Python 3.)

jbg
fuente
25
Me gustaría usar esta respuesta pero estoy obteniendo: AttributeError: 'file' object has no attribute 'readable' py2.7
Dan Garthwaite
3
Funciona con python 3
matanster
Es evidente que este código no es válido para múltiples razones AP3 / AP3 compatibilidad y riesgo real de conseguir ValueError: Me operación de E / S de archivo cerrado
Sorin
3
@sorin ninguna de esas cosas lo hace "no válido". Si está escribiendo una biblioteca que todavía necesita admitir Python 2, entonces no use este código. Pero muchas personas tienen el lujo de poder usar el software lanzado más recientemente que hace una década. Si intenta leer en un archivo cerrado, obtendrá esa excepción independientemente de si la usa TextIOWrappero no. Simplemente puede manejar la excepción.
jbg
1
tal vez llegas tarde a la fiesta pero tu respuesta está actualizada con la versión actual de Python, ty
Dusan Gligoric
20

De hecho, si resolvió el iterador, el almacenamiento en búfer ahora podría ser su problema. Podrías decirle a la pitón en el subproceso que no almacene su salida.

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

se convierte

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

He necesitado esto al llamar a python desde python.

Steve Carter
fuente
14

Desea pasar estos parámetros adicionales a subprocess.Popen:

bufsize=1, universal_newlines=True

Entonces puedes iterar como en tu ejemplo. (Probado con Python 3.5)

usuario1747134
fuente
2
@nicoulaj Debería funcionar si se usa el paquete subprocess32.
Quantum7
4

Una función que permite iterar sobre ambos stdouty stderrsimultáneamente, en tiempo real, línea por línea

En caso de que necesite obtener el flujo de salida para ambos stdouty stderral mismo tiempo, puede usar la siguiente función.

La función usa Colas para fusionar ambas tuberías de Popen en un solo iterador.

Aquí creamos la función 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() en uso:

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
fuente
2

También puede leer líneas sin bucle. Funciona en python3.6.

import os
import subprocess

process = subprocess.Popen(command, stdout=subprocess.PIPE)
list_of_byte_strings = process.stdout.readlines()
Aiven
fuente
1
O para convertir en cadenas:list_of_strings = [x.decode('utf-8').rstrip('\n') for x in iter(process.stdout.readlines())]
ndtreviv
1

Intenté esto con python3 y funcionó, fuente

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
fuente
1

La siguiente modificación de la respuesta de Rômulo funciona para mí en Python 2 y 3 (2.7.12 y 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
fuente
0

No sé cuándo esto se ha agregado al módulo de subproceso, pero con Python 3 debería estar bien usando proc.stdout.splitlines():

for line in proc.stdout.splitlines():
   print "stdout:", line
StefanQ
fuente