Come utilizzo subprocess.Popen per connettere più processi tramite pipe?
-
08-07-2019 - |
Domanda
Come posso eseguire il seguente comando shell usando Python sottoprocesso
modulo?
echo "input data" | awk -f script.awk | sort > outfile.txt
I dati di input provengono da una stringa, quindi in realtà non ho bisogno di echo
. Sono arrivato così lontano, qualcuno può spiegare come riesco a passare anche attraverso sort
?
p_awk = subprocess.Popen(["awk","-f","script.awk"],
stdin=subprocess.PIPE,
stdout=file("outfile.txt", "w"))
p_awk.communicate( "input data" )
AGGIORNAMENTO : nota che, sebbene la risposta accettata di seguito non risponda effettivamente alla domanda come posta, credo che S.Lott abbia ragione ed è meglio evitare di dover risolvere il problema in primo luogo !
Soluzione
Saresti un po 'più felice di quanto segue.
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" )
Delega parte del lavoro alla shell. Lascia che connetta due processi con una pipeline.
Saresti molto più felice di riscrivere 'script.awk' in Python, eliminando awk e la pipeline.
Modifica . Alcuni dei motivi per suggerire che awk non aiuta.
[Ci sono troppi motivi per rispondere tramite commenti.]
-
Awk sta aggiungendo un passaggio senza valore significativo. Non c'è niente di unico nell'elaborazione di awk che Python non gestisce.
-
Il pipelining da awk a sort, per grandi serie di dati, può migliorare il tempo di elaborazione trascorso. Per brevi set di dati, non ha vantaggi significativi. Una rapida misurazione del file
awk >; ordina i file
eawk | sort
rivelerà gli aiuti della concorrenza. Con l'ordinamento, aiuta raramente perché l'ordinamento non è un filtro una tantum. -
La semplicità di " Python per ordinare " l'elaborazione (invece di " Python in awk per ordinare ") impedisce il tipo esatto di domande poste qui.
-
Python - sebbene più intenso di awk - è anche esplicito in cui awk ha alcune regole implicite che sono opache per i neofiti e confuse per i non specialisti.
-
Awk (come lo stesso script di shell) aggiunge l'ennesimo linguaggio di programmazione. Se tutto ciò può essere fatto in una sola lingua (Python), l'eliminazione della shell e la programmazione awk eliminano due linguaggi di programmazione, consentendo a qualcuno di concentrarsi sulle parti che generano valore dell'attività.
In conclusione: awk non può aggiungere un valore significativo. In questo caso, awk è un costo netto; ha aggiunto abbastanza complessità da rendere necessaria questa domanda. Rimuovere awk sarà un guadagno netto.
Barra laterale Perché costruire una pipeline ( a | b
) è così difficile.
Quando la shell si confronta con a | b
deve eseguire le seguenti operazioni.
-
Fork un processo figlio della shell originale. Questo alla fine diventerà b.
-
Crea una pipe os. (non un sottoprocesso di Python.PIPE) ma chiama
os.pipe ()
che restituisce due nuovi descrittori di file collegati tramite un buffer comune. A questo punto il processo ha stdin, stdout, stderr dal suo genitore, oltre a un file che sarà "stdout di" a " e "stdin di b". -
Fork un bambino. Il bambino sostituisce il suo stdout con il nuovo stdout della a. Eseguire il processo
a
. -
Il bambino b chiude sostituisce il suo stdin con il nuovo b dello stdin. Eseguire il processo
b
. -
Il bambino b attende il completamento di a.
-
Il genitore sta aspettando il completamento di b.
Penso che quanto sopra possa essere usato ricorsivamente per generare a | b | c
, ma devi racchiudere implicitamente tra parentesi lunghe, trattandole come se fossero a | (b | c)
.
Poiché Python ha os.pipe ()
, os.exec ()
e os.fork ()
e puoi sostituire < code> sys.stdin e sys.stdout
, c'è un modo per fare quanto sopra in puro Python. In effetti, potresti essere in grado di elaborare alcune scorciatoie usando os.pipe ()
e subprocess.Popen
.
Tuttavia, è più semplice delegare tale operazione alla shell.
Altri suggerimenti
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)
Per emulare una pipeline di shell:
from subprocess import check_call
check_call('echo "input data" | a | b > outfile.txt', shell=True)
senza invocare la shell (consultare 17.1.4.2. Sostituzione della shell conduttura ):
#!/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
fornisce un po 'di zucchero di sintassi:
#!/usr/bin/env python
from plumbum.cmd import a, b # magic
(a << "input data" | b > "outfile.txt")()
L'analogo di:
#!/bin/sh
echo "input data" | awk -f script.awk | sort > outfile.txt
è:
#!/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 ha trattato abbastanza bene. C'è una parte di questo che non hai capito?
Il tuo programma sarebbe abbastanza simile, ma il secondo Popen
avrebbe stdout = in un file e non avresti bisogno dell'output del suo .communicate ()
.
Ispirato dalla risposta di @ Cristian. Ho incontrato lo stesso problema, ma con un comando diverso. Quindi sto dando il mio esempio testato, che credo possa essere 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()
Questo è testato.
Cosa è stato fatto
- Esecuzione dichiarata lazy
grep
con stdin dalla pipe. Questo comando verrà eseguito durante l'esecuzione del comandops
quando la pipe verrà riempita con lo stdout dips
. - Chiamato il comando principale
ps
con stdout diretto alla pipe utilizzata dal comandogrep
. - Grep ha comunicato per ottenere stdout dalla pipe.
Mi piace in questo modo perché è una concezione naturale delle pipe delicatamente avvolta dalle interfacce subprocess
.
La risposta accettata sta evitando il problema. ecco uno snippet che concatena l'output di più processi: Si noti che stampa anche il comando (in qualche modo equivalente) della shell in modo da poterlo eseguire e assicurarsi che l'output sia corretto.
#!/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)
EDIT: pipe
è disponibile su Windows ma, soprattutto, non sembra effettivamente funzionare su Windows. Vedi i commenti qui sotto.
La libreria standard Python ora include il modulo pipe
per la gestione di questo:
https://docs.python.org/2/library/pipes.html , https://docs.python.org/3.4/library/pipes.html
Non sono sicuro da quanto tempo questo modulo è in circolazione, ma questo approccio sembra essere molto più semplice di quello che si fa con sottoprocesso
.
Le risposte precedenti hanno mancato un punto importante. La sostituzione della pipeline della shell è sostanzialmente corretta, come sottolineato di geocar. È quasi sufficiente eseguire communic
sull'ultimo elemento della pipe.
Il problema rimanente sta passando i dati di input alla pipeline. Con più sottoprocessi, un semplice communic (input_data)
sull'ultimo elemento non funziona - si blocca per sempre. Devi creare una pipeline e un figlio manualmente in questo modo:
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())
Ora il figlio fornisce l'input attraverso la pipe e le chiamate principali comunicano (), che funziona come previsto. Con questo approccio, puoi creare condutture lunghe arbitrarie senza ricorrere a "delegare parte del lavoro alla shell". Sfortunatamente la documentazione sui sottoprocessi non menziona questo.
Esistono modi per ottenere lo stesso effetto senza pipe:
from tempfile import TemporaryFile
tf = TemporaryFile()
tf.write(input)
tf.seek(0, 0)
Ora usa stdin = tf
per p_awk
. È una questione di gusti ciò che preferisci.
Quanto sopra non è ancora equivalente al 100% alle pipeline di bash perché la gestione del segnale è diversa. Puoi vederlo se aggiungi un altro elemento pipe che tronca l'output di sort
, ad es. head -n 10
. Con il codice sopra, sort
stamperà un " Broken pipe " messaggio di errore a stderr
. Non vedrai questo messaggio quando esegui la stessa pipeline nella shell. (Questa è l'unica differenza, il risultato in stdout
è lo stesso). Il motivo sembra essere che Popen
di Python imposta SIG_IGN
per SIGPIPE
, mentre la shell lo lascia su SIG_DFL
, e La gestione del segnale di sort
è diversa in questi due casi.
Per me, l'approccio di seguito è il più pulito e facile da leggere
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()
che può essere chiamato così:
string_to_2_procs_to_file('input data', ['awk', '-f', 'script.awk'], ['sort'], 'output.txt')