Comment utiliser subprocess.Popen pour connecter plusieurs processus par des canaux?
-
08-07-2019 - |
Question
Comment puis-je exécuter la commande shell suivante avec le sous-processus
module?
echo "input data" | awk -f script.awk | sort > outfile.txt
Les données d'entrée proviendront d'une chaîne, je n'ai donc pas besoin de echo
. J’en suis encore là. Quelqu'un peut-il expliquer comment je parviens à le faire passer aussi par tri
?
p_awk = subprocess.Popen(["awk","-f","script.awk"],
stdin=subprocess.PIPE,
stdout=file("outfile.txt", "w"))
p_awk.communicate( "input data" )
MISE À JOUR : notez que même si la réponse acceptée ci-dessous ne répond pas à la question posée, je pense que S.Lott a raison et qu'il est préférable d'éviter de devoir résoudre ce problème en premier lieu. !
La solution
Vous seriez un peu plus heureux avec ce qui suit.
import subprocess
awk_sort = subprocess.Popen( "awk -f script.awk | sort > outfile.txt",
stdin=subprocess.PIPE, shell=True )
awk_sort.communicate( b"input data\n" )
Déléguer une partie du travail au shell. Laissez-le connecter deux processus avec un pipeline.
Vous seriez beaucoup plus heureux de réécrire 'script.awk' en Python, en éliminant awk et le pipeline.
Modifier . Certaines des raisons de suggérer que awk n’aide pas.
[Il y a trop de raisons de répondre via des commentaires.]
-
Awk ajoute une étape sans valeur significative. Le traitement de awk n'a rien d'unique que Python ne gère pas.
-
Le traitement en pipeline de awk à trier, pour de grands ensembles de données, peut améliorer le temps de traitement écoulé. Pour de courts ensembles de données, cela ne présente aucun avantage significatif. Une mesure rapide de
awk > file; trier le fichier
etawk | sort
révélera les aides de concurrence. Avec le tri, cela aide rarement car le tri n’est pas un filtre à passage unique. -
La simplicité de "python pour trier" Le traitement (au lieu de "Python pour effacer pour trier") empêche le type exact de questions posées ici.
-
Python - bien que plus verbeux que awk - est également explicite là où awk a certaines règles implicites qui sont opaques pour les débutants et qui déroutent les non-spécialistes.
-
Awk (comme le script shell lui-même) ajoute Yet Another Programming. Si tout cela peut être fait dans un seul langage (Python), éliminer le shell et la programmation awk supprime deux langages de programmation, permettant ainsi à quelqu'un de se concentrer sur les parties de la tâche productrices de valeur.
En bout de ligne: awk ne peut pas ajouter de valeur significative. Dans ce cas, awk est un coût net; cela a ajouté suffisamment de complexité pour qu'il soit nécessaire de poser cette question. Supprimer awk sera un gain net.
Barre latérale Pourquoi la création d'un pipeline ( a | b
) est-elle si difficile?
Lorsque le shell est confronté à a | b
il doit faire ce qui suit.
-
Lancez un processus enfant du shell d'origine. Cela deviendra éventuellement b.
-
Construisez un tuyau en os. (pas un sous-processus Python.PIPE) mais appelez
os.pipe ()
qui renvoie deux nouveaux descripteurs de fichier connectés via un tampon commun. À ce stade, le processus a stdin, stdout, stderr de son parent, plus un fichier qui sera "une a stdout". et "b's stdin". -
Fourchez un enfant. L'enfant remplace sa sortie standard par la nouvelle sortie standard a. Exécutez le processus
a
. -
L’enfant b se ferme remplace son stdin par le nouveau stdin de b. Exécutez le processus
b
. -
L’enfant b attend la fin de a.
-
Le parent attend que b soit terminé.
Je pense que ce qui précède peut être utilisé de manière récursive pour générer a | b | c
, mais vous devez implicitement mettre entre parenthèses de longs pipelines, en les traitant comme s'ils étaient a | (b | c)
.
Puisque Python a os.pipe ()
, os.exec ()
et os.fork ()
, vous pouvez remplacer < code> sys.stdin et sys.stdout
, il existe un moyen de faire ce qui précède en pur Python. En effet, vous pourrez peut-être trouver des raccourcis avec os.pipe ()
et subprocess.Popen
.
Cependant, il est plus facile de déléguer cette opération au shell.
Autres conseils
import subprocess
some_string = b'input_data'
sort_out = open('outfile.txt', 'wb', 0)
sort_in = subprocess.Popen('sort', stdin=subprocess.PIPE, stdout=sort_out).stdin
subprocess.Popen(['awk', '-f', 'script.awk'], stdout=sort_in,
stdin=subprocess.PIPE).communicate(some_string)
Pour émuler un pipeline shell:
from subprocess import check_call
check_call('echo "input data" | a | b > outfile.txt', shell=True)
sans appeler le shell (voir 17.1.4.2. Remplacement du shell pipeline ):
#!/usr/bin/env python
from subprocess import Popen, PIPE
a = Popen(["a"], stdin=PIPE, stdout=PIPE)
with a.stdin:
with a.stdout, open("outfile.txt", "wb") as outfile:
b = Popen(["b"], stdin=a.stdout, stdout=outfile)
a.stdin.write(b"input data")
statuses = [a.wait(), b.wait()] # both a.stdin/stdout are closed already
plumbum
fournit un sucre syntaxique:
#!/usr/bin/env python
from plumbum.cmd import a, b # magic
(a << "input data" | b > "outfile.txt")()
L'analogue de:
#!/bin/sh
echo "input data" | awk -f script.awk | sort > outfile.txt
est:
#!/usr/bin/env python
from plumbum.cmd import awk, sort
(awk["-f", "script.awk"] << "input data" | sort > "outfile.txt")()
http://www.python.org/doc/2.5.2/lib/node535.html a très bien couvert cela. Y a-t-il une partie de ce que vous n'avez pas comprise?
Votre programme serait assez similaire, mais le second Popen
aurait stdout = dans un fichier, et vous n’auriez pas besoin de la sortie de son .communicate ()
.
Inspiré par la réponse de @ Cristian. J'ai rencontré le même problème, mais avec une commande différente. Donc, je mets mon exemple testé, qui, je pense, pourrait être utile:
grep_proc = subprocess.Popen(["grep", "rabbitmq"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
subprocess.Popen(["ps", "aux"], stdout=grep_proc.stdin)
out, err = grep_proc.communicate()
Ceci est testé.
Ce qui a été fait
- Exécution déclarée paresseuse
grep
avec stdin from pipe. Cette commande sera exécutée à l'exécution de la commandeps
lorsque le canal sera rempli avec la sortie standard deps
. - Appelé la commande principale
ps
avec stdout dirigée vers le canal utilisé par la commandegrep
. - Grep a communiqué pour obtenir la sortie standard du tuyau.
J'aime cette façon, car il s'agit d'une conception de tuyau naturelle, enveloppée dans des interfaces sous-processus
.
La réponse acceptée est de contourner le problème. Voici un extrait qui enchaîne la sortie de plusieurs processus: Notez qu'il imprime également la commande (quelque peu) équivalente du shell afin que vous puissiez l'exécuter et vous assurer que le résultat est correct.
#!/usr/bin/env python3
from subprocess import Popen, PIPE
# cmd1 : dd if=/dev/zero bs=1m count=100
# cmd2 : gzip
# cmd3 : wc -c
cmd1 = ['dd', 'if=/dev/zero', 'bs=1M', 'count=100']
cmd2 = ['tee']
cmd3 = ['wc', '-c']
print(f"Shell style : {' '.join(cmd1)} | {' '.join(cmd2)} | {' '.join(cmd3)}")
p1 = Popen(cmd1, stdout=PIPE, stderr=PIPE) # stderr=PIPE optional, dd is chatty
p2 = Popen(cmd2, stdin=p1.stdout, stdout=PIPE)
p3 = Popen(cmd3, stdin=p2.stdout, stdout=PIPE)
print("Output from last process : " + (p3.communicate()[0]).decode())
# thoretically p1 and p2 may still be running, this ensures we are collecting their return codes
p1.wait()
p2.wait()
print("p1 return: ", p1.returncode)
print("p2 return: ", p2.returncode)
print("p3 return: ", p3.returncode)
MODIFIER: Les tubes
sont disponibles sous Windows, mais il est à noter que cela ne semble pas réellement fonctionner sous Windows. Voir les commentaires ci-dessous.
La bibliothèque standard Python inclut maintenant le module pipes
pour gérer ceci:
https://docs.python.org/2/library/pipes.html , https://docs.python.org/3.4/library/pipes.html
Je ne sais pas depuis combien de temps ce module existe, mais cette approche semble être beaucoup plus simple que de bidouiller avec le sous-processus
.
Les réponses précédentes ont manqué un point important. Le remplacement du pipeline shell est fondamentalement correct, comme indiqué par géocar. Il est presque suffisant d'exécuter communic
sur le dernier élément du canal.
Le problème qui reste à résoudre consiste à transmettre les données d'entrée au pipeline. Avec plusieurs sous-processus, une simple communication (input_data)
sur le dernier élément ne fonctionne pas - elle se bloque pour toujours. Vous devez créer un pipeline et un enfant manuellement, comme suit:
import os
import subprocess
input = """\
input data
more input
""" * 10
rd, wr = os.pipe()
if os.fork() != 0: # parent
os.close(wr)
else: # child
os.close(rd)
os.write(wr, input)
os.close(wr)
exit()
p_awk = subprocess.Popen(["awk", "{ print $2; }"],
stdin=rd,
stdout=subprocess.PIPE)
p_sort = subprocess.Popen(["sort"],
stdin=p_awk.stdout,
stdout=subprocess.PIPE)
p_awk.stdout.close()
out, err = p_sort.communicate()
print (out.rstrip())
Maintenant, l'enfant fournit les informations par le biais du canal de communication et les appels parents communiquent (), ce qui fonctionne comme prévu. Avec cette approche, vous pouvez créer de longs pipelines arbitraires sans avoir recours à la "délégation d'une partie du travail au shell". Malheureusement, la la documentation sur les sous-processus ne mentionne pas cela.
Il existe des moyens d'obtenir le même effet sans tuyaux:
from tempfile import TemporaryFile
tf = TemporaryFile()
tf.write(input)
tf.seek(0, 0)
Utilisez maintenant stdin = tf
pour p_awk
. C'est une question de goût que vous préférez.
Ce qui précède n’est toujours pas équivalent à 100% aux pipelines bash car le traitement du signal est différent. Vous pouvez le voir si vous ajoutez un autre élément de tuyau qui tronque la sortie de sort
, par exemple. head -n 10
. Avec le code ci-dessus, sort
imprimera un " Tube cassé " message d'erreur à stderr
. Vous ne verrez pas ce message lorsque vous exécutez le même pipeline dans le shell. (C'est la seule différence cependant, le résultat dans stdout
est le même). La raison semble être que Popen
de python définit SIG_IGN
pour SIGPIPE
, alors que le shell le laisse à SIG_DFL
, et Le traitement du signal de sort
est différent dans ces deux cas.
Pour moi, l'approche ci-dessous est la plus propre et la plus facile à lire
from subprocess import Popen, PIPE
def string_to_2_procs_to_file(input_s, first_cmd, second_cmd, output_filename):
with open(output_filename, 'wb') as out_f:
p2 = Popen(second_cmd, stdin=PIPE, stdout=out_f)
p1 = Popen(first_cmd, stdout=p2.stdin, stdin=PIPE)
p1.communicate(input=bytes(input_s))
p1.wait()
p2.stdin.close()
p2.wait()
qui peut être appelé comme suit:
string_to_2_procs_to_file('input data', ['awk', '-f', 'script.awk'], ['sort'], 'output.txt')