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