120 votes

récupérer stdout en temps réel à partir d'un sous-processus

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

Mon code fonctionne, mais il n'affiche pas la progression jusqu'à ce qu'un transfert de fichier soit effectué ! Je veux imprimer la progression pour chaque fichier en temps réel.

J'utilise maintenant Python 3.1 car j'ai entendu dire qu'il devrait mieux gérer les entrées-sorties.

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

2 votes

(Venant de google ?) tous les PIPEs se bloqueront lorsque le tampon d'un des PIPEs sera rempli et ne sera pas lu. par exemple stdout se bloque lorsque stderr est rempli. Ne passez jamais un PIPE que vous n'avez pas l'intention de lire.

0 votes

Quelqu'un pourrait-il expliquer pourquoi vous ne pouviez pas simplement définir stdout sur sys.stdout au lieu de subprocess.PIPE ?

125voto

nosklo Points 75862

Quelques règles de base pour subprocess .

  • Jamais utiliser shell=True . Il invoque 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.argv en python est une liste, de même que argv en C. Vous passez donc un liste a Popen pour appeler les sous-processus, et non une chaîne de caractères.
  • Ne pas rediriger stderr à un PIPE quand tu ne le lis pas.
  • Ne pas rediriger stdin quand tu n'écris pas dessus.

Ejemplo:

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 mette en mémoire tampon sa sortie lorsqu'il détecte qu'il est connecté à un tuyau et non à un terminal. C'est le comportement par défaut - lorsqu'ils sont connectés à un tuyau, les programmes doivent explicitement vider stdout pour obtenir des résultats en temps réel, sinon la bibliothèque C standard mettra en mémoire tampon.

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

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

et créer un test_out.py 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 c'est-à-dire rsync lui-même met en tampon la sortie, donc vous n'avez pas de chance.

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

15 votes

shell=False est une bonne chose lorsque vous construisez une ligne de commande, notamment à partir de données saisies par l'utilisateur. Mais néanmoins shell=True est également utile lorsque vous obtenez la ligne de commande complète à partir d'une source fiable (par exemple, codée en dur dans le script).

13 votes

@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 programmation du processeur, juste pour diviser une chaîne de caractères ! Et un que vous avez joint vous-même ! Vous pourriez diviser en python, mais il est plus facile d'écrire chaque paramètre séparément de toute façon. De plus, utiliser une liste signifie que vous n'avez pas à échapper les caractères spéciaux de l'interpréteur de commandes : les espaces, ; , > , < , & .. Vos paramètres peuvent contenir ces caractères et vous n'avez pas à vous inquiéter ! Je ne vois pas de raison d'utiliser shell=True vraiment, à moins que vous n'exécutiez une commande uniquement en shell.

0 votes

Nosklo,cela devrait être : p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

9voto

IBue Points 51
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 se termine lorsque le processus enfant ferme son stdout ou sort. read()/read(-1) bloquerait jusqu'à ce que le processus enfant ferme son stdout ou sorte.

1 votes

inchar n'est jamais None utiliser if not inchar: à la place ( read() renvoie une chaîne vide sur EOF). En fait, c'est encore pire for line in p.stdout n'imprime même pas des lignes complètes en temps réel dans Python 2 ( for line in iter(p.stdout.readline, '')` pourrait être utilisé à la place).

1 votes

J'ai testé cela avec python 3.4 sur osx, et cela ne fonctionne pas.

1 votes

@qed : for line in p.stdout: fonctionne sur Python 3. Veillez à bien comprendre la différence entre '' (chaîne de caractères Unicode) et b'' (octets). Voir Python : lecture de l'entrée en continu à partir de subprocess.communicate()

7voto

zviadm Points 809

Votre problème est :

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

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

Essayez de faire comme ça :

while True:
  line = p.stdout.readline()
  if not line:
     break
  print line

3voto

Erik Points 41

Vous ne pouvez pas faire en sorte que stdout imprime sans tampon vers un tuyau (à moins que vous puissiez réécrire le programme qui imprime vers stdout), voici donc ma solution :

Redirige 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 distinguer stdout ou stderr, mais vous obtenez toutes les sorties immédiatement.

J'espère que cela aidera toute personne confrontée à ce problème.

4 votes

Vous avez essayé ? Parce que ça ne marche pas.. Si stdout est mis en 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

5 votes

C'est tout simplement faux. La mise en mémoire tampon stdout se fait au sein même du programme. La syntaxe du shell 1>&2 change juste les fichiers vers lesquels les descripteurs de fichiers pointent avant de lancer le programme. Le programme lui-même ne peut pas faire la différence entre rediriger stdout vers stderr ( 1>&2 ) ou vice-versa ( 2>&1 ), ce qui n'aura aucun effet sur le comportement du programme en matière de mise en mémoire tampon. 1>&2 est interprétée par l'interpréteur de commandes. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE) échouerait parce que vous n'avez pas spécifié shell=True .

0 votes

Au cas où des gens liraient ceci : J'ai essayé d'utiliser stderr au lieu de stdout, cela montre exactement le même comportement.

1voto

Will Points 30630

Change le stdout du processus rsync pour qu'il soit non bufferisé.

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)

4 votes

La mise en mémoire tampon se produit du côté de rsync, changer l'attribut bufsize du côté de python n'aidera pas.

17 votes

Pour tous ceux qui cherchent, 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 subprocess renvoie un objet fichier et que l'interface file iterator a un tampon interne mal documenté même avec bufsize=0, ce qui vous oblige à appeler readline() à plusieurs reprises si vous avez besoin de résultats avant que le tampon ne se remplisse.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X