web-dev-qa-db-fra.com

Comment utiliser subprocess.Popen pour connecter plusieurs processus par des tuyaux?

Comment exécuter la commande Shell suivante à l'aide du module Python subprocess ?

echo "input data" | awk -f script.awk | sort > outfile.txt

Les données d'entrée proviendront d'une chaîne, donc je n'ai pas vraiment besoin de echo. Je suis arrivé jusqu'ici, est-ce que quelqu'un peut expliquer comment je le fais passer par sort aussi?

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 bien que la réponse acceptée ci-dessous ne réponde pas réellement à la question posée, je pense que S .Lott a raison et il vaut mieux éviter d'avoir à résoudre ce problème en premier lieu!

46
Tom

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éguez 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, éliminant ainsi awk et le pipeline.

Modifier . Certaines des raisons pour lesquelles suggérer que awk n'aide pas.

[Il y a trop de raisons de répondre via des commentaires.]

  1. Awk ajoute une étape sans valeur significative. Il n'y a rien d'unique dans le traitement de awk que Python ne gère pas.

  2. Le pipelining de awk au tri, pour les grands ensembles de données, peut améliorer le temps de traitement écoulé. Pour les ensembles de données courts, cela n'a aucun avantage significatif. Une mesure rapide de awk >file ; sort file Et awk | sort Révèlera des aides simultanées. Avec le tri, cela aide rarement car le tri n'est pas un filtre à passage unique.

  3. La simplicité du traitement "Python à trier" (au lieu de "Python à awk pour trier") empêche le type exact de questions posées ici.

  4. Python - bien que plus verbeux que awk - est également explicite lorsque awk a certaines règles implicites qui sont opaques pour les débutants et déroutantes pour les non-spécialistes.

  5. Awk (comme le script Shell lui-même) ajoute encore un autre langage de programmation. Si tout cela peut être fait dans un seul langage (Python), l'élimination du Shell et de la programmation awk élimine deux langages de programmation, permettant à quelqu'un de se concentrer sur les parties génératrices de valeur de la tâche.

Conclusion: awk ne peut pas ajouter de valeur significative. Dans ce cas, awk est un coût net; cela ajoutait suffisamment de complexité pour qu'il soit nécessaire de poser cette question. La suppression de awk sera un gain net.

Barre latérale Pourquoi construire un pipeline (a | b) Est si difficile.

Lorsque le shell est confronté à a | b, Il doit procéder comme suit.

  1. Démarrez un processus enfant du Shell d'origine. Cela deviendra éventuellement b.

  2. Construisez un tuyau os. (pas un Python subprocess.PIPE) mais appelez os.pipe() qui retourne deux nouveaux descripteurs de fichiers qui sont connectés via un tampon commun. À ce stade, le processus a stdin, stdout, stderr de son parent, plus un fichier qui sera "a's stdout" et "b's stdin".

  3. Fourchez un enfant. L'enfant remplace sa sortie standard par la nouvelle sortie standard de a. Exécutez le processus a.

  4. L'enfant b ferme remplace son stdin par le nouveau stdin de b. Exécutez le processus b.

  5. L'enfant b attend la fin de a.

  6. Le parent attend que b se termine.

Je pense que ce qui précède peut être utilisé récursivement 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(), et vous pouvez remplacer sys.stdin Et sys.stdout, Il existe un moyen de faire ce qui précède en Python pur. En effet, vous pourrez peut-être trouver des raccourcis en utilisant os.pipe() et subprocess.Popen.

Cependant, il est plus facile de déléguer cette opération au shell.

40
S.Lott
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)
24
Cristian

Pour émuler un pipeline Shell:

from subprocess import check_call

check_call('echo "input data" | a | b > outfile.txt', Shell=True)

sans invoquer le Shell (voir 17.1.4.2. Remplacement du pipeline Shell ):

#!/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 du sucre de syntaxe:

#!/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")()
17
jfs

http://www.python.org/doc/2.5.2/lib/node535.html a assez bien couvert cela. Y a-t-il une partie de cela 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 sa .communicate().

3
geocar

La réponse acceptée élude le problème. voici un extrait qui enchaîne la sortie de plusieurs processus: Notez qu'il imprime également la commande Shell (quelque peu) équivalente afin que vous puissiez l'exécuter et vous assurer que la sortie est correcte.

#!/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)
2
Omry Yadan

Inspiré par la réponse de @ Cristian. J'ai rencontré le même problème, mais avec une commande différente. Je mets donc 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 paresseuse grep déclarée avec stdin à partir du canal. Cette commande sera exécutée lors de l'exécution de la commande ps lorsque le tube sera rempli avec la sortie standard de ps.
  • Appelé la commande principale ps avec stdout dirigé vers le canal utilisé par la commande grep.
  • Grep a communiqué pour sortir de la pipe.

J'aime cette façon parce que c'est une conception de pipe naturelle doucement enveloppée d'interfaces subprocess.

2
I159

Les réponses précédentes ont manqué un point important. Remplacement du pipeline Shell est fondamentalement correct, comme l'a souligné Geocar. Il suffit presque d'exécuter communicate sur le dernier élément du tube.

Le problème restant consiste à transmettre les données d'entrée au pipeline. Avec plusieurs sous-processus, une simple communicate(input_data) sur le dernier élément ne fonctionne pas - elle se bloque pour toujours. Vous devez créer manuellement un pipeline et un enfant comme ceci:

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

L'enfant fournit maintenant l'entrée via le canal 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 documentation du sous-processus ne le mentionne pas.

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 ce que vous préférez.

Ce qui précède n'est toujours pas équivalent à 100% aux pipelines bash car la gestion du signal est différente. Vous pouvez le voir si vous ajoutez un autre élément de canal qui tronque la sortie de sort, par exemple head -n 10. Avec le code ci-dessus, sort affichera un message d'erreur "Broken pipe" dans 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 les ensembles Popen de python SIG_IGN pour SIGPIPE, tandis que le shell le laisse à SIG_DFL, et sort la gestion du signal est différente dans ces deux cas.

1
uncleremus

EDIT: pipes est disponible sur Windows mais, surtout, ne semble pas réellement fonctionner sous Windows. Voir les commentaires ci-dessous.

La bibliothèque standard Python inclut désormais 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 nettoyer avec subprocess.

1
Kyle Strand

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é ainsi:

string_to_2_procs_to_file('input data', ['awk', '-f', 'script.awk'], ['sort'], 'output.txt')
0
mwag