lire le sous-processus stdout ligne par ligne

235

Mon script python utilise un sous-processus pour appeler un utilitaire Linux très bruyant. Je veux stocker toute la sortie dans un fichier journal et en montrer une partie à l'utilisateur. Je pensais que ce qui suit fonctionnerait, mais la sortie n'apparaît dans mon application que lorsque l'utilitaire a produit une quantité importante de sortie.

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

Le comportement que je veux vraiment est que le script de filtre imprime chaque ligne telle qu'elle est reçue du sous-processus. Sorta comme quoi, teemais avec du code python.

Qu'est-ce que je rate? Est-ce seulement possible?


Mettre à jour:

Si a sys.stdout.flush()est ajouté à fake_utility.py, le code a le comportement souhaité en python 3.1. J'utilise python 2.6. On pourrait penser que l'utilisation proc.stdout.xreadlines()fonctionnerait de la même manière que py3k, mais ce n'est pas le cas.


Mise à jour 2:

Voici le code de travail minimal.

#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
la source
4
vous pouvez utiliser à la print line,place de print line.rstrip()(note: virgule à la fin).
jfs
2
La mise à jour 2 indique qu'elle fonctionne avec python 3.0+ mais utilise l'ancienne instruction print, elle ne fonctionne donc pas avec python 3.0+.
Rooky
Aucune des réponses répertoriées ici n'a fonctionné pour moi, mais stackoverflow.com/questions/5411780/… l'a fait!
boxé

Réponses:

179

Cela fait longtemps que je n'ai pas travaillé avec Python pour la dernière fois, mais je pense que le problème vient de l'instruction for line in proc.stdout, qui lit l'intégralité de l'entrée avant de l'itérer. La solution est d'utiliser à la readline()place:

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

Bien sûr, vous devez toujours gérer la mise en mémoire tampon du sous-processus.

Remarque: selon la documentation, la solution avec un itérateur devrait être équivalente à l'utilisation readline(), à l'exception du tampon de lecture anticipée, mais (ou exactement à cause de cela) la modification proposée a produit des résultats différents pour moi (Python 2.5 sur Windows XP).

Rômulo Ceccon
la source
11
pour file.readline()vs for line in filevoir bugs.python.org/issue3907 (en bref: cela fonctionne sur Python3; utiliser io.open()sur Python 2.6+)
jfs
5
Le test le plus pythonique pour un EOF, selon les "recommandations de programmation" dans PEP 8 ( python.org/dev/peps/pep-0008 ), serait 'sinon ligne:'.
Jason Mock,
14
@naxa: pour les tubes: for line in iter(proc.stdout.readline, ''):.
jfs
3
@ Jan-PhilipGehrcke: oui. 1. vous pouvez utiliser for line in proc.stdoutsur Python 3 (il n'y a pas de bogue de lecture anticipée) 2. '' != b''sur Python 3 - ne copiez-collez pas le code à l'aveugle - pensez à ce qu'il fait et comment il fonctionne.
jfs
2
@JFSebastian: bien sûr, la iter(f.readline, b'')solution est plutôt évidente (et fonctionne également sur Python 2, si quelqu'un est intéressé). Le but de mon commentaire n'était pas de blâmer votre solution (désolé si elle apparaissait comme ça, je l'ai lu maintenant aussi!), Mais de décrire l'étendue des symptômes, qui sont assez sévères dans ce cas (la plupart des Py2 / 3 problèmes entraînent des exceptions, alors qu'ici une boucle bien tenue a changé pour être sans fin, et la collecte des ordures a du mal à lutter contre le flot d'objets nouvellement créés, produisant des oscillations d'utilisation de la mémoire avec une longue période et une grande amplitude).
Dr.Jan-Philip Gehrcke
45

Un peu tard pour la fête, mais j'ai été surpris de ne pas voir ce que je pense être la solution la plus simple ici:

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

(Cela nécessite Python 3.)

jbg
la source
25
Je voudrais utiliser cette réponse mais je reçois: AttributeError: 'file' object has no attribute 'readable' py2.7
Dan Garthwaite
3
Fonctionne avec python 3
matanster
De toute évidence, ce code n'est pas valide pour plusieurs raisons, compatibilité py3 / py3 et risque réel d'obtenir ValueError: opération d'E / S sur un fichier fermé
sorin
3
@sorin aucune de ces choses ne le rend "non valide". Si vous écrivez une bibliothèque qui doit toujours prendre en charge Python 2, n'utilisez pas ce code. Mais beaucoup de gens ont le luxe de pouvoir utiliser un logiciel sorti il ​​y a plus de dix ans. Si vous essayez de lire sur un fichier fermé, vous obtiendrez cette exception, que vous l'utilisiez TextIOWrapperou non. Vous pouvez simplement gérer l'exception.
jbg
1
vous êtes peut-être en retard à la fête mais vous répondez est à jour avec la version actuelle de Python, ty
Dusan Gligoric
20

En effet, si vous triez l'itérateur, la mise en mémoire tampon peut maintenant être votre problème. Vous pouvez dire au python du sous-processus de ne pas mettre sa sortie en mémoire tampon.

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

devient

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

J'en avais besoin lors de l'appel de python depuis python.

Steve Carter
la source
14

Vous souhaitez transmettre ces paramètres supplémentaires à subprocess.Popen:

bufsize=1, universal_newlines=True

Ensuite, vous pouvez répéter comme dans votre exemple. (Testé avec Python 3.5)

user1747134
la source
2
@nicoulaj Cela devrait fonctionner si vous utilisez le package subprocess32.
Quantum7
4

Une fonction qui permet d'itérer sur les deux stdoutet stderrsimultanément, en temps réel, ligne par ligne

Si vous avez besoin d'obtenir le flux de sortie pour les deux stdoutet stderren même temps, vous pouvez utiliser la fonction suivante.

La fonction utilise des files d'attente pour fusionner les deux canaux Popen en un seul itérateur.

Ici, nous créons la fonction 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() utilisé:

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
la source
2

Vous pouvez également lire des lignes sans boucle. Fonctionne en python3.6.

import os
import subprocess

process = subprocess.Popen(command, stdout=subprocess.PIPE)
list_of_byte_strings = process.stdout.readlines()
aiven
la source
1
Ou à convertir en chaînes:list_of_strings = [x.decode('utf-8').rstrip('\n') for x in iter(process.stdout.readlines())]
ndtreviv
1

J'ai essayé cela avec python3 et cela a fonctionné, 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()
shakram02
la source
1

La modification suivante de la réponse de Rômulo fonctionne pour moi sur Python 2 et 3 (2.7.12 et 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
la source
0

Ne sais pas quand cela a été ajouté au module de sous-processus, mais avec Python 3, vous devriez bien utiliser proc.stdout.splitlines():

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