web-dev-qa-db-fra.com

Python lit les sous-processus stdout et stderr séparément tout en préservant l'ordre

J'ai un sous-processus python que j'essaie de lire les flux de sortie et d'erreur. Actuellement, cela fonctionne, mais je ne suis capable de lire que stderr après avoir lu stdout. Voici à quoi ça ressemble:

process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout_iterator = iter(process.stdout.readline, b"")
stderr_iterator = iter(process.stderr.readline, b"")

for line in stdout_iterator:
    # Do stuff with line
    print line

for line in stderr_iterator:
    # Do stuff with line
    print line

Comme vous pouvez le constater, la boucle stderr for ne peut pas démarrer avant la fin de la boucle stdout. Comment puis-je modifier cela pour pouvoir lire les lignes dans le bon ordre dans les deux?

Pour préciser: Je dois encore pouvoir savoir si une ligne provient de stdout ou stderr car elles seront traitées différemment dans mon code.

22
Luke Sapan

Le code de votre question risque de se bloquer si le processus enfant génère suffisamment de sorties sur stderr (~ 100 Ko sur ma machine Linux).

Il existe une méthode communicate() qui permet de lire séparément stdout et stderr:

from subprocess import Popen, PIPE

process = Popen(command, stdout=PIPE, stderr=PIPE)
output, err = process.communicate()

Si vous devez lire les flux alors que le processus enfant est toujours en cours d'exécution, la solution portable consiste à utiliser des threads (non testés):

from subprocess import Popen, PIPE
from threading import Thread
from Queue import Queue # Python 2

def reader(pipe, queue):
    try:
        with pipe:
            for line in iter(pipe.readline, b''):
                queue.put((pipe, line))
    finally:
        queue.put(None)

process = Popen(command, stdout=PIPE, stderr=PIPE, bufsize=1)
q = Queue()
Thread(target=reader, args=[process.stdout, q]).start()
Thread(target=reader, args=[process.stderr, q]).start()
for _ in range(2):
    for source, line in iter(q.get, None):
        print "%s: %s" % (source, line),

Voir:

16
jfs

L'ordre dans lequel un processus écrit des données sur différents canaux est perdu après l'écriture.

Vous ne pouvez pas savoir si stdout a été écrit avant stderr.

Vous pouvez essayer de lire simultanément des données à partir de plusieurs descripteurs de fichiers de manière non bloquante dès que les données sont disponibles, mais cela ne ferait que minimiser la probabilité que la commande soit incorrecte.

Ce programme devrait démontrer ceci:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import select
import subprocess

testapps={
    'slow': '''
import os
import time
os.write(1, 'aaa')
time.sleep(0.01)
os.write(2, 'bbb')
time.sleep(0.01)
os.write(1, 'ccc')
''',
    'fast': '''
import os
os.write(1, 'aaa')
os.write(2, 'bbb')
os.write(1, 'ccc')
''',
    'fast2': '''
import os
os.write(1, 'aaa')
os.write(2, 'bbbbbbbbbbbbbbb')
os.write(1, 'ccc')
'''
}

def readfds(fds, maxread):
    while True:
        fdsin, _, _ = select.select(fds,[],[])
        for fd in fdsin:
            s = os.read(fd, maxread)
            if len(s) == 0:
                fds.remove(fd)
                continue
            yield fd, s
        if fds == []:
            break

def readfromapp(app, rounds=10, maxread=1024):
    f=open('testapp.py', 'w')
    f.write(testapps[app])
    f.close()

    results={}
    for i in range(0, rounds):
        p = subprocess.Popen(['python', 'testapp.py'], stdout=subprocess.PIPE
                                                     , stderr=subprocess.PIPE)
        data=''
        for (fd, s) in readfds([p.stdout.fileno(), p.stderr.fileno()], maxread):
            data = data + s
        results[data] = results[data] + 1 if data in results else 1

    print 'running %i rounds %s with maxread=%i' % (rounds, app, maxread)
    results = sorted(results.items(), key=lambda (k,v): k, reverse=False)
    for data, count in results:
        print '%03i x %s' % (count, data)


print
print "=> if output is produced slowly this should work as whished"
print "   and should return: aaabbbccc"
readfromapp('slow',  rounds=100, maxread=1024)

print
print "=> now mostly aaacccbbb is returnd, not as it should be"
readfromapp('fast',  rounds=100, maxread=1024)

print
print "=> you could try to read data one by one, and return"
print "   e.g. a whole line only when LF is read"
print "   (b's should be finished before c's)"
readfromapp('fast',  rounds=100, maxread=1)

print
print "=> but even this won't work ..."
readfromapp('fast2', rounds=100, maxread=1)

et sort quelque chose comme ceci:

=> if output is produced slowly this should work as whished
   and should return: aaabbbccc
running 100 rounds slow with maxread=1024
100 x aaabbbccc

=> now mostly aaacccbbb is returnd, not as it should be
running 100 rounds fast with maxread=1024
006 x aaabbbccc
094 x aaacccbbb

=> you could try to read data one by one, and return
   e.g. a whole line only when LF is read
   (b's should be finished before c's)
running 100 rounds fast with maxread=1
003 x aaabbbccc
003 x aababcbcc
094 x abababccc

=> but even this won't work ...
running 100 rounds fast2 with maxread=1
003 x aaabbbbbbbbbbbbbbbccc
001 x aaacbcbcbbbbbbbbbbbbb
008 x aababcbcbcbbbbbbbbbbb
088 x abababcbcbcbbbbbbbbbb
4
Jörg Schulz

J'ai écrit quelque chose pour le faire un il y a longtemps . Je ne l'ai pas encore porté sur Python 3, mais cela ne devrait pas être trop difficile (correctifs acceptés!)

Si vous l'exécutez seul, vous verrez beaucoup d'options différentes. Dans tous les cas, cela vous permet de distinguer stdout de stderr.

0
Patrick Maupin

Je sais que cette question est très ancienne, mais cette réponse peut aider les autres personnes qui tombent sur cette page en cherchant une solution à une situation similaire. Je la poste quand même.

J'ai construit un simple fragment de code en python qui fusionnera un nombre illimité de canaux en un seul. Bien sûr, comme indiqué ci-dessus, l'ordre ne peut être garanti, mais c'est aussi proche que je pense que vous pouvez obtenir en Python. 

Il crée un thread pour chacun des canaux, les lit ligne par ligne et les met dans une file d'attente (qui est FIFO). Le thread principal effectue une boucle dans la file d'attente, générant chaque ligne.

import threading, queue
def merge_pipes(**named_pipes):
    r'''
    Merges multiple pipes from subprocess.Popen (maybe other sources as well).
    The keyword argument keys will be used in the output to identify the source
    of the line.

    Example:
    p = subprocess.Popen(['some', 'call'],
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    outputs = {'out': log.info, 'err': log.warn}
    for name, line in merge_pipes(out=p.stdout, err=p.stderr):
        outputs[name](line)

    This will output stdout to the info logger, and stderr to the warning logger
    '''

    # Constants. Could also be placed outside of the method. I just put them here
    # so the method is fully self-contained
    PIPE_OPENED=1
    PIPE_OUTPUT=2
    PIPE_CLOSED=3

    # Create a queue where the pipes will be read into
    output = queue.Queue()

    # This method is the run body for the threads that are instatiated below
    # This could be easily rewritten to be outside of the merge_pipes method,
    # but to make it fully self-contained I put it here
    def pipe_reader(name, pipe):
        r"""
        reads a single pipe into the queue
        """
        output.put( ( PIPE_OPENED, name, ) )
        try:
            for line in iter(pipe.readline,''):
                output.put( ( PIPE_OUTPUT, name, line.rstrip(), ) )
        finally:
            output.put( ( PIPE_CLOSED, name, ) )

    # Start a reader for each pipe
    for name, pipe in named_pipes.items():
        t=threading.Thread(target=pipe_reader, args=(name, pipe, ))
        t.daemon = True
        t.start()

    # Use a counter to determine how many pipes are left open.
    # If all are closed, we can return
    pipe_count = 0

    # Read the queue in order, blocking if there's no data
    for data in iter(output.get,''):
        code=data[0]
        if code == PIPE_OPENED:
            pipe_count += 1
        Elif code == PIPE_CLOSED:
            pipe_count -= 1
        Elif code == PIPE_OUTPUT:
            yield data[1:]
        if pipe_count == 0:
            return
0
Marten Jacobs