Fusionner les sous-processus stdout et stderr d'un script Python tout en les gardant distinguables

j'aimerais diriger le sous-processus stdout et stdin d'un script python dans le même fichier. Ce que je ne sais pas, c'est comment distinguer les lignes des deux sources? (Par exemple, préfixez les lignes de stderr par un point d'exclamation.)

dans mon cas particulier, il n'est pas nécessaire de surveiller en direct le sous-processus, le script Python exécutant peut attendre la fin de son exécution.

28
demandé sur beemtee 2011-07-25 00:43:09

6 réponses

tsk = subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)

subprocess.STDOUT est un drapeau spécial qui indique à subprocess de router toutes les sorties stderr vers stdout, combinant ainsi vos deux flux.

btw, select n'a pas de poll() dans windows. subprocess utilise seulement le numéro de poignée de dossier, et n'appelle pas la méthode d'écriture de votre objet de sortie de dossier.

pour capturer la sortie, faire quelque chose comme:

logfile = open(logfilename, 'w')

while tsk.poll() is None:
    line = tsk.stdout.readline()
    logfile.write(line)
31
répondu mossman 2012-12-28 13:22:46

je me suis trouvé avoir à aborder ce problème récemment, et il a fallu un certain temps pour obtenir quelque chose que je me suis senti travailler correctement dans la plupart des cas, donc voilà! (Il a également l'effet secondaire agréable de traiter la sortie via un logger python, ce que j'ai remarqué est une autre question commune ici sur Stackoverflow).

Voici le code:

import sys
import logging
import subprocess
from threading import Thread

logging.basicConfig(stream=sys.stdout,level=logging.INFO)
logging.addLevelName(logging.INFO+2,'STDERR')
logging.addLevelName(logging.INFO+1,'STDOUT')
logger = logging.getLogger('root')

pobj = subprocess.Popen(['python','-c','print 42;bargle'], 
    stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def logstream(stream,loggercb):
    while True:
        out = stream.readline()
        if out:
            loggercb(out.rstrip())       
        else:
            break

stdout_thread = Thread(target=logstream,
    args=(pobj.stdout,lambda s: logger.log(logging.INFO+1,s)))

stderr_thread = Thread(target=logstream,
    args=(pobj.stderr,lambda s: logger.log(logging.INFO+2,s)))

stdout_thread.start()
stderr_thread.start()

while stdout_thread.isAlive() and stderr_thread.isAlive():
     pass

Voici la sortie:

STDOUT:root:42
STDERR:root:Traceback (most recent call last):
STDERR:root:  File "<string>", line 1, in <module>
STDERR:root:NameError: name 'bargle' is not defined

vous pouvez remplacer l'appel de sous-processus pour faire ce que vous voulez, j'ai juste choisi l'exécution python avec une commande que je savais imprimer à la fois stdout et stderr. La clé est de lire stderr et stdout chacun dans un thread séparé. Dans le cas contraire, il se peut que vous bloquiez la lecture de l'une alors qu'il y a des données prêtes à être lues de l'autre.

11
répondu guyincognito 2012-03-28 00:52:40

si vous voulez intercaler pour obtenir à peu près le même ordre que si vous exécutiez le processus de façon interactive alors vous devez faire ce que le shell fait et Poller stdin/stdout et écrire dans l'ordre dans lequel ils pollent.

voici un peu de code qui fait quelque chose dans le sens de ce que vous voulez - dans ce cas, envoyer le stdout/stderr à un flux logger info/error.

tsk = subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE)

poll = select.poll()
poll.register(tsk.stdout,select.POLLIN | select.POLLHUP)
poll.register(tsk.stderr,select.POLLIN | select.POLLHUP)
pollc = 2

events = poll.poll()
while pollc > 0 and len(events) > 0:
  for event in events:
    (rfd,event) = event
    if event & select.POLLIN:
      if rfd == tsk.stdout.fileno():
        line = tsk.stdout.readline()
        if len(line) > 0:
          logger.info(line[:-1])
      if rfd == tsk.stderr.fileno():
        line = tsk.stderr.readline()
        if len(line) > 0:
          logger.error(line[:-1])
    if event & select.POLLHUP:
      poll.unregister(rfd)
      pollc = pollc - 1
    if pollc > 0: events = poll.poll()
tsk.wait()
9
répondu T.Rojan 2011-07-26 08:04:48

pour le moment, toutes les autres réponses ne gèrent pas la mise en tampon du côté du sous-processus enfant si le sous-processus n'est pas un script Python qui accepte -u drapeau. Voir " Q: Pourquoi ne pas simplement utiliser un tuyau (popen())?"dans la documentation de pexpect.

Pour simuler -u drapeau pour certains C stdio (FILE*) programmes que vous pouvez essayer stdbuf.

si vous ignorez ceci alors votre sortie ne sera pas correctement entreleaved et pourrait regarder comme:

stderr
stderr
...large block of stdout including parts that are printed before stderr...

vous pouvez essayer avec le programme client suivant, remarquez la différence avec / sans -u drapeau (['stdbuf', '-o', 'L', 'child_program'] corrige aussi la sortie):

#!/usr/bin/env python
from __future__ import print_function
import random
import sys
import time
from datetime import datetime

def tprint(msg, file=sys.stdout):
    time.sleep(.1*random.random())
    print("%s %s" % (datetime.utcnow().strftime('%S.%f'), msg), file=file)

tprint("stdout1 before stderr")
tprint("stdout2 before stderr")
for x in range(5):
    tprint('stderr%d' % x, file=sys.stderr)
tprint("stdout3 after stderr")

sous Linux vous pouvez utiliser pty pour obtenir le même comportement que lorsque le sous-processus s'exécute de manière interactive par exemple, voici une modification réponse de@T. Rojan:

import logging, os, select, subprocess, sys, pty

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

master_fd, slave_fd = pty.openpty()
p = subprocess.Popen(args,stdout=slave_fd, stderr=subprocess.PIPE, close_fds=True)
with os.fdopen(master_fd) as stdout:
    poll = select.poll()
    poll.register(stdout, select.POLLIN)
    poll.register(p.stderr,select.POLLIN | select.POLLHUP)

    def cleanup(_done=[]):
        if _done: return
        _done.append(1)
        poll.unregister(p.stderr)
        p.stderr.close()
        poll.unregister(stdout)
        assert p.poll() is not None

    read_write = {stdout.fileno(): (stdout.readline, logger.info),
                  p.stderr.fileno(): (p.stderr.readline, logger.error)}
    while True:
        events = poll.poll(40) # poll with a small timeout to avoid both
                               # blocking forever and a busy loop
        if not events and p.poll() is not None:
            # no IO events and the subprocess exited
            cleanup()
            break

        for fd, event in events:
            if event & select.POLLIN: # there is something to read
                read, write = read_write[fd]
                line = read()
                if line:
                    write(line.rstrip())
            elif event & select.POLLHUP: # free resources if stderr hung up
                cleanup()
            else: # something unexpected happened
                assert 0
sys.exit(p.wait()) # return child's exit code

il suppose que stderr est toujours non tamponné / tamponné par ligne et que stdout est tamponné par ligne dans un le mode interactif. Seules les lignes complètes sont lues. Le programme peut bloquer s'il y a des lignes non terminées dans la sortie.

2
répondu jfs 2017-12-18 12:06:35

je vous suggère d'écrire vos propres gestionnaires, quelque chose comme (pas testé, j'espère que vous attrapez l'idée):

class my_buffer(object):
    def __init__(self, fileobject, prefix):
        self._fileobject = fileobject
        self.prefix = prefix
    def write(self, text):
        return self._fileobject.write('%s %s' % (self.prefix, text))
    # delegate other methods to fileobject if necessary

log_file = open('log.log', 'w')
my_out = my_buffer(log_file, 'OK:')
my_err = my_buffer(log_file, '!!!ERROR:')
p = subprocess.Popen(command, stdout=my_out, stderr=my_err, shell=True)
1
répondu Guard 2011-07-24 23:54:07

Vous pouvez écrire le stdout / err dans un fichier après l'exécution de la commande. Dans l'exemple ci-dessous, j'utilise pickling donc je suis sûr que je serai en mesure de lire sans parsing particulier pour différencier entre le stdout/err et à un moment donné je pourrais dumo le code d'Exit et la commande elle-même.

import subprocess
import cPickle

command = 'ls -altrh'
outfile = 'log.errout'
pipe = subprocess.Popen(command, stdout = subprocess.PIPE,
                        stderr = subprocess.PIPE, shell = True)
stdout, stderr = pipe.communicate()

f = open(outfile, 'w')
cPickle.dump({'out': stdout, 'err': stderr},f)
f.close()
0
répondu Cinquo 2011-07-24 20:59:26