capture stdout en temps réel à partir du sous-processus

87

Je veux subprocess.Popen()rsync.exe sous Windows et imprimer le stdout en Python.

Mon code fonctionne, mais il n'attrape pas la progression tant qu'un transfert de fichier n'est pas effectué! Je souhaite imprimer la progression de chaque fichier en temps réel.

En utilisant Python 3.1 maintenant depuis que j'ai entendu dire qu'il devrait être meilleur pour gérer les E / S.

import subprocess, time, os, sys

cmd = "rsync.exe -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()
John A
la source
1
(Venant de Google?) Tous les PIPE seront bloqués lorsque l'un des tampons des PIPE se remplit et ne sera pas lu. Par exemple, stdout blocage lorsque stderr est rempli. Ne passez jamais un PIPE que vous n'avez pas l'intention de lire.
Nasser Al-Wohaibi
Quelqu'un pourrait-il expliquer pourquoi vous ne pouvez pas simplement définir stdout sur sys.stdout au lieu de subprocess.PIPE?
Mike

Réponses:

98

Quelques règles de base pour subprocess.

  • Ne jamais utiliser shell=True. Il appelle inutilement un processus shell supplémentaire pour appeler votre programme.
  • Lors de l'appel de processus, les arguments sont transmis sous forme de listes. sys.argven python est une liste, tout comme argven C. Vous passez donc une liste à Popenpour appeler des sous-processus, pas une chaîne.
  • Ne redirigez pas vers stderrun PIPElorsque vous ne le lisez pas.
  • Ne redirigez pas stdinlorsque vous n'y écrivez pas.

Exemple:

import subprocess, time, os, sys
cmd = ["rsync.exe", "-vaz", "-P", "source/" ,"dest/"]

p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT)

for line in iter(p.stdout.readline, b''):
    print(">>> " + line.rstrip())

Cela dit, il est probable que rsync tamponne sa sortie lorsqu'il détecte qu'il est connecté à un tube au lieu d'un terminal. C'est le comportement par défaut - lorsqu'ils sont connectés à un tube, les programmes doivent explicitement vider stdout pour les résultats en temps réel, sinon la bibliothèque C standard sera mise en mémoire tampon.

Pour tester cela, essayez d'exécuter ceci à la place:

cmd = [sys.executable, 'test_out.py']

et créez un test_out.pyfichier avec le contenu:

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

L'exécution de ce sous-processus devrait vous donner "Hello" et attendre 10 secondes avant de donner "World". Si cela se produit avec le code python ci-dessus et non avec rsync, cela signifie qu'il rsyncmet en mémoire tampon la sortie, vous n'avez donc pas de chance.

Une solution serait de se connecter directement à un pty, en utilisant quelque chose comme pexpect.

nosklo
la source
12
shell=Falseest la bonne chose lorsque vous construisez une ligne de commande, en particulier à partir de données entrées par l'utilisateur. Mais néanmoins shell=Trueest également utile lorsque vous obtenez toute la ligne de commande à partir d'une source fiable (par exemple, codée en dur dans le script).
Denis Otkidach
10
@Denis Otkidach: Je ne pense pas que cela justifie l'utilisation de shell=True. Pensez-y - vous invoquez un autre processus sur votre système d'exploitation, impliquant l'allocation de mémoire, l'utilisation du disque, la planification du processeur, juste pour diviser une chaîne ! Et celui que vous vous êtes joint !! Vous pouvez diviser en python, mais il est de toute façon plus facile d'écrire chaque paramètre séparément. En outre, en utilisant un moyen liste vous ne devez pas échapper à caractères shell spéciaux: espaces, ;, >, <, &.. Vos paramètres peuvent contenir ces caractères et vous n'avez pas à vous inquiéter! Je ne vois pas shell=Truevraiment de raison d'utiliser , à moins que vous n'exécutiez une commande shell uniquement.
nosklo
nosklo, cela devrait être: p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
Senthil Kumaran
1
@mathtick: Je ne sais pas pourquoi vous feriez ces opérations en tant que processus séparés ... vous pouvez couper le contenu du fichier et extraire facilement le premier champ en python en utilisant le csvmodule. Mais à titre d'exemple, votre pipeline en python serait: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultNotez que vous pouvez travailler avec des noms de fichiers longs et des caractères spéciaux de shell sans avoir à vous échapper, maintenant que le shell n'est pas impliqué. De plus, c'est beaucoup plus rapide car il y a un processus de moins.
nosklo
11
utiliser à la for line in iter(p.stdout.readline, b'')place de for line in p.stdoutPython 2 sinon les lignes ne sont pas lues en temps réel même si le processus source ne met pas en mémoire tampon sa sortie.
jfs
41

Je sais que c'est un vieux sujet, mais il y a une solution maintenant. Appelez rsync avec l'option --outbuf = L. Exemple:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())
Elvin
la source
3
Cela fonctionne et devrait être voté pour éviter aux futurs lecteurs de faire défiler toutes les boîtes de dialogue ci-dessus.
VectorVictor
1
@VectorVictor Cela n'explique pas ce qui se passe et pourquoi cela se passe. Il se peut que votre programme fonctionne, jusqu'à ce que: 1. vous ajoutez preexec_fn=os.setpgrppour que le programme survienne à son script parent 2. vous sautez la lecture du tube du processus 3. le processus génère beaucoup de données, remplissant le tube 4. vous êtes bloqué pendant des heures , en essayant de comprendre pourquoi le programme que vous exécutez se ferme après un certain laps de temps . La réponse de @nosklo m'a beaucoup aidé.
danuker
15

Sous Linux, j'ai eu le même problème de se débarrasser de la mise en mémoire tampon. J'ai finalement utilisé "stdbuf -o0" (ou, unbuffer from expect) pour me débarrasser du tampon PIPE.

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

Je pourrais alors utiliser select.select sur stdout.

Voir aussi /unix/25372/

Lingue
la source
2
Pour tous ceux qui essaient de récupérer le code C stdout de Python, je peux confirmer que cette solution était la seule qui a fonctionné pour moi. Pour être clair, je parle d'ajouter «stdbuf», «-o0» à ma liste de commandes existante dans Popen.
Reckless
Merci! stdbuf -o0s'est avéré vraiment utile avec un tas de tests pytest / pytest-bdd que j'ai écrits qui génèrent une application C ++ et vérifient qu'elle émet certaines instructions de journal. Sans stdbuf -o0, ces tests ont nécessité 7 secondes pour obtenir la sortie (mise en mémoire tampon) du programme C ++. Maintenant, ils fonctionnent presque instantanément!
evadeflow
11

Selon le cas d'utilisation, vous pouvez également désactiver la mise en mémoire tampon dans le sous-processus lui-même.

Si le sous-processus est un processus Python, vous pouvez le faire avant l'appel:

os.environ["PYTHONUNBUFFERED"] = "1"

Ou transmettez-le dans l' envargument à Popen.

Sinon, si vous êtes sous Linux / Unix, vous pouvez utiliser l' stdbufoutil. Par exemple, comme:

cmd = ["stdbuf", "-oL"] + cmd

Voir aussi ici à propos stdbufou d'autres options.

Albert
la source
1
Vous sauvez ma journée, merci pour PYTHONUNBUFFERED = 1
diewland
9
for line in p.stdout:
  ...

bloque toujours jusqu'au prochain saut de ligne.

Pour un comportement "en temps réel", vous devez faire quelque chose comme ceci:

while True:
  inchar = p.stdout.read(1)
  if inchar: #neither empty string nor None
    print(str(inchar), end='') #or end=None to flush immediately
  else:
    print('') #flush for implicit line-buffering
    break

La boucle while est laissée lorsque le processus enfant ferme sa sortie standard ou se termine. read()/read(-1)bloquerait jusqu'à ce que le processus fils ferme sa sortie stdout ou quitte.

IBue
la source
1
incharn'est jamais Noneutilisé à la if not inchar:place ( read()renvoie une chaîne vide sur EOF). btw, c'est pire for line in p.stdoutne pas imprimer même des lignes complètes en temps réel dans Python 2 ( for line in iter (p.stdout.readline, '') `pourrait être utilisé à la place).
jfs
1
J'ai testé cela avec python 3.4 sur osx, et cela ne fonctionne pas.
qed
1
@qed: for line in p.stdout:fonctionne sur Python 3. Assurez-vous de bien comprendre la différence entre ''(chaîne Unicode) et b''(octets). Voir Python: lire l'entrée en continu de subprocess.communicate ()
jfs
8

Votre problème est:

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()

l'itérateur lui-même a une mémoire tampon supplémentaire.

Essayez de faire comme ceci:

while True:
  line = p.stdout.readline()
  if not line:
     break
  print line
zviadm
la source
5

Vous ne pouvez pas obtenir stdout pour imprimer sans tampon sur un tube (à moins que vous ne puissiez réécrire le programme qui imprime sur stdout), voici donc ma solution:

Redirigez stdout vers sterr, qui n'est pas mis en mémoire tampon. '<cmd> 1>&2'devrait le faire. Ouvrez le processus comme suit: myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
Vous ne pouvez pas faire la distinction entre stdout ou stderr, mais vous obtenez immédiatement toutes les sorties.

J'espère que cela aidera quiconque à résoudre ce problème.

Erik
la source
4
L'as tu essayé? Parce que cela ne fonctionne pas. Si stdout est mis en mémoire tampon dans ce processus, il ne sera pas redirigé vers stderr de la même manière qu'il n'est pas redirigé vers un PIPE ou un fichier.
Filipe Pina
5
C'est tout simplement faux. La mise en mémoire tampon stdout se produit dans le programme lui-même. La syntaxe du shell 1>&2change simplement les fichiers vers lesquels pointent les descripteurs de fichiers avant de lancer le programme. Le programme lui-même ne peut pas faire la distinction entre la redirection de stdout vers stderr ( 1>&2) ou vice-versa ( 2>&1) donc cela n'aura aucun effet sur le comportement de mise en mémoire tampon du programme. Et de toute façon, la 1>&2syntaxe est interprétée par le shell. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)échouerait parce que vous ne l'avez pas spécifié shell=True.
Will Manley du
Au cas où les gens liraient ceci: j'ai essayé d'utiliser stderr au lieu de stdout, cela montre exactement le même comportement.
martinthenext
3

Modifiez la sortie stdout du processus rsync pour qu'elle ne soit pas tamponnée.

p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=0,  # 0=unbuffered, 1=line-buffered, else buffer-size
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)
Volonté
la source
3
La mise en mémoire tampon se produit du côté rsync, changer l'attribut bufsize du côté python n'aidera pas.
nosklo
14
Pour quiconque recherche, la réponse de nosklo est complètement fausse: l'affichage de la progression de rsync n'est pas mis en mémoire tampon; le vrai problème est que le sous-processus retourne un objet fichier et que l'interface de l'itérateur de fichier a un tampon interne mal documenté même avec bufsize = 0, vous obligeant à appeler readline () à plusieurs reprises si vous avez besoin de résultats avant que le tampon ne se remplisse.
Chris Adams
3

Pour éviter la mise en cache de la sortie, vous pouvez essayer pexpect,

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

PS : Je sais que cette question est assez ancienne, fournissant toujours la solution qui a fonctionné pour moi.

PPS : a obtenu cette réponse d'une autre question

Nithin
la source
3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

J'écris une interface graphique pour rsync en python, et j'ai les mêmes problèmes. Ce problème me préoccupe depuis plusieurs jours jusqu'à ce que je trouve cela dans pyDoc.

Si universal_newlines vaut True, les objets fichier stdout et stderr sont ouverts en tant que fichiers texte en mode de retours à la ligne universels. Les lignes peuvent être terminées par l'un des '\ n', la convention de fin de ligne Unix, '\ r', l'ancienne convention Macintosh ou '\ r \ n', la convention Windows. Toutes ces représentations externes sont considérées comme '\ n' par le programme Python.

Il semble que rsync affichera '\ r' lorsque la traduction est en cours.

xmc
la source
1

J'ai remarqué qu'il n'est pas fait mention de l'utilisation d'un fichier temporaire comme intermédiaire. Ce qui suit permet de contourner les problèmes de mise en mémoire tampon en sortant dans un fichier temporaire et vous permet d'analyser les données provenant de rsync sans vous connecter à un pty. J'ai testé ce qui suit sur une boîte Linux, et la sortie de rsync a tendance à différer d'une plate-forme à l'autre, de sorte que les expressions régulières pour analyser la sortie peuvent varier:

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...
MikeGM
la source
ce n'est pas en temps réel. Un fichier ne résout pas le problème de mise en mémoire tampon du côté de rsync.
jfs
tempfile.TemporaryFile peut se supprimer pour un nettoyage plus facile en cas d'exceptions
jfs
3
while not p.poll()conduit à une boucle infinie si le sous-processus se p.poll() is None
termine
Windows peut interdire d'ouvrir un fichier déjà ouvert, donc open(file_name)peut échouer
jfs
1
Je viens de trouver cette réponse, malheureusement uniquement pour Linux, mais fonctionne comme un lien de charme Donc, je prolonge simplement ma commande comme suit: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argvet appelle: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) et maintenant je peux lire sans aucune mise en mémoire tampon
Arvid
0

si vous exécutez quelque chose comme ça dans un thread et enregistrez la propriété ffmpeg_time dans une propriété d'une méthode afin que vous puissiez y accéder, cela fonctionnerait très bien.J'obtiens des sorties comme celle-ci: la sortie est comme si vous utilisez le threading dans tkinter

input = 'path/input_file.mp4'
output = 'path/input_file.mp4'
command = "ffmpeg -y -v quiet -stats -i \"" + str(input) + "\" -metadata title=\"@alaa_sanatisharif\" -preset ultrafast -vcodec copy -r 50 -vsync 1 -async 1 \"" + output + "\""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
for line in self.process.stdout:
    reg = re.search('\d\d:\d\d:\d\d', line)
    ffmpeg_time = reg.group(0) if reg else ''
    print(ffmpeg_time)
Erfan
la source
-1

Dans Python 3, voici une solution, qui supprime une commande de la ligne de commande et délivre en temps réel des chaînes bien décodées à mesure qu'elles sont reçues.

Receveur (receiver.py ):

import subprocess
import sys

cmd = sys.argv[1:]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in p.stdout:
    print("received: {}".format(line.rstrip().decode("utf-8")))

Exemple de programme simple qui pourrait générer une sortie en temps réel (dummy_out.py ):

import time
import sys

for i in range(5):
    print("hello {}".format(i))
    sys.stdout.flush()  
    time.sleep(1)

Production:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
watsonic
la source