web-dev-qa-db-ja.com

ブロック-入力をpythonサブプロセスパイプラインに送信します

私はPythonでサブプロセスパイプラインをテストしています。以下のプログラムがpythonで直接実行できることは承知していますが、それは重要ではありません。パイプラインをテストしたいので、その使用方法を知っています。

私のシステムはLinuxUbuntu 9.04で、デフォルトはpython 2.6です。

私はこれから始めました ドキュメントの例

_from subprocess import Popen, PIPE
p1 = Popen(["grep", "-v", "not"], stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
output = p2.communicate()[0]
print output
_

それは機能しますが、_p1_のstdinがリダイレクトされていないため、パイプにフィードするためにターミナルに入力する必要があります。 stdinを閉じる_^D_と入力すると、必要な出力が得られます。

ただし、python文字列変数を使用してパイプにデータを送信したい。最初にstdinに書き込んでみた:

_p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
p1.stdin.write('test\n')
output = p2.communicate()[0] # blocks forever here
_

動作しませんでした。最後の行で代わりにp2.stdout.read()を使用しようとしましたが、ブロックされます。 p1.stdin.flush()p1.stdin.close()を追加しましたが、どちらも機能しませんでした。それから私はコミュニケーションに移りました:

_p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
p1.communicate('test\n') # blocks forever here
output = p2.communicate()[0] 
_

だから、それはまだそれではありません。

単一のプロセスの実行(上記の_p1_のように、_p2_の削除)が完全に機能することに気付きました。また、ファイルハンドルを_p1_(stdin=open(...))に渡すこともできます。したがって、問題は次のとおりです。

ブロックせずに、Pythonで2つ以上のサブプロセスのパイプラインにデータを渡すことは可能ですか?何故なの?

シェルを実行してパイプラインをシェルで実行できることは承知していますが、それは私が望んでいることではありません。


UPDATE 1:以下のAaron Digullaのヒントに従って、スレッドを使用して動作させようとしています。

まず、スレッドでp1.communicateを実行してみました。

_p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
t = threading.Thread(target=p1.communicate, args=('some data\n',))
t.start()
output = p2.communicate()[0] # blocks forever here
_

さて、動作しませんでした。 .write()p2.read()に変更するなどの他の組み合わせを試しました。何もありません。次に、反対のアプローチを試してみましょう。

_def get_output(subp):
    output = subp.communicate()[0] # blocks on thread
    print 'GOT:', output

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
t = threading.Thread(target=get_output, args=(p2,)) 
t.start()
p1.communicate('data\n') # blocks here.
t.join()
_

コードはどこかでブロックされてしまいます。スポーンされたスレッド、メインスレッド、またはその両方。だからそれはうまくいきませんでした。それを機能させる方法を知っているなら、機能するコードを提供できればもっと簡単になるでしょう。私はここで試しています。


UPDATE 2

Paul Du Boisがいくつかの情報で以下に答えたので、私はさらにテストを行いました。 _subprocess.py_モジュール全体を読み、それがどのように機能するかを理解しました。だから私はそれをコードに正確に適用してみました。

私はLinuxを使用していますが、スレッドを使用してテストしていたため、最初のアプローチは、_subprocess.py_のcommunicate()メソッドに表示される正確なWindowsスレッドコードを複製することでしたが、1つではなく2つのプロセスでした。これが私が試したものの全体のリストです:

_import os
from subprocess import Popen, PIPE
import threading

def get_output(fobj, buffer):
    while True:
        chunk = fobj.read() # BLOCKS HERE
        if not chunk:
            break
        buffer.append(chunk)

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)

b = [] # create a buffer
t = threading.Thread(target=get_output, args=(p2.stdout, b))
t.start() # start reading thread

for x in xrange(100000):
    p1.stdin.write('hello world\n') # write data
    p1.stdin.flush()
p1.stdin.close() # close input...
t.join()
_

上手。それはうまくいきませんでした。 p1.stdin.close()が呼び出された後でも、p2.stdout.read()はブロックされます。

次に、_subprocess.py_でposixコードを試しました。

_import os
from subprocess import Popen, PIPE
import select

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)

numwrites = 100000
to_read = [p2.stdout]
to_write = [p1.stdin]
b = [] # create buffer

while to_read or to_write:
    read_now, write_now, xlist = select.select(to_read, to_write, [])
    if read_now:
        data = os.read(p2.stdout.fileno(), 1024)
        if not data:
            p2.stdout.close()
            to_read = []
        else:
            b.append(data)

    if write_now:
        if numwrites > 0:
            numwrites -= 1
            p1.stdin.write('hello world!\n'); p1.stdin.flush()
        else:
            p1.stdin.close()
            to_write = []

print b
_

また、select.select()をブロックします。 printsを広めることで、私はこれを見つけました:

  • 読書は機能しています。コードは実行中に何度も読み取ります。
  • 書くことも働いています。データは_p1.stdin_に書き込まれます。
  • numwritesの終わりに、p1.stdin.close()が呼び出されます。
  • select()がブロックを開始すると、_to_read_だけが何か_p2.stdout_を持ちます。 _to_write_はすでに空です。
  • os.read() callは常に何かを返すため、p2.stdout.close()が呼び出されることはありません。

両方のテストからの結論:パイプラインの最初のプロセス(例ではstdin)のgrepを閉じるとバッファリングされた出力を次の出力にダンプして死ぬようにしないでください。

それを機能させる方法はありませんか?

PS:一時ファイルは使いたくありません。すでにファイルでテストしていて、機能することはわかっています。そして、私はウィンドウズを使いたくありません。

34
nosklo

私はそれを行う方法を見つけました。

それはスレッドについてではなく、select()についてでもありません。

最初のプロセス(grep)を実行すると、パイプごとに1つずつ、2つの低レベルファイル記述子が作成されます。それらをabと呼びましょう。

2番目のプロセスを実行すると、bcutsdtinに渡されます。しかし、Popen --_close_fds=False_には脳死のデフォルトがあります。

その効果は、cutaを継承することです。したがって、grepのプロセスでstdinがまだ開いているため(aは無視します)、cutを閉じてもcutは終了できません。

次のコードは完全に実行されます。

_from subprocess import Popen, PIPE

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE, close_fds=True)
p1.stdin.write('Hello World\n')
p1.stdin.close()
result = p2.stdout.read() 
assert result == "Hello Worl\n"
_

_close_fds=True_はデフォルトである必要があります UNIXシステムでは。 Windowsでは、閉じますall fdsなので、配管ができなくなります。

編集:

PS:この回答を読んで同様の問題を抱えている人のために:pooryorickがコメントで言ったように、_p1.stdin_に書き込まれたデータがバッファーよりも大きい場合もブロックされる可能性があります。その場合、データをより小さな部分にチャンクし、select.select()を使用していつ読み取り/書き込みを行うかを知る必要があります。問題のコードは、それを実装する方法についてのヒントを与えるはずです。

EDIT2:pooryorickの助けを借りて、別の解決策を見つけました-_close_fds=True_を使用して閉じる代わりに[〜#〜] all [〜#〜] fds、fdsは、2番目のプロセスを実行するときに、最初のプロセスに属し、機能します。クロージングは​​子で行う必要があるため、Popenの_preexec_fn_関数はそれを行うのに非常に便利です。 p2を実行すると、次のことができます。

_p2 = Popen(cmd2, stdin=p1.stdout, stdout=PIPE, stderr=devnull, preexec_fn=p1.stdin.close)
_
21
nosklo

大きなファイルの操作

Pythonで大きなファイルを操作する場合は、2つの原則を一律に適用する必要があります。

  1. IOルーチンはブロックできるため、パイプラインの各ステージを異なるスレッドまたはプロセスに保持する必要があります。このスレッドを使用します例ですが、サブプロセスを使用するとGILを回避できます。
  2. インクリメンタル読み取りと書き込みを使用して、進行を開始する前にEOFを待たないようにする必要があります。

別の方法は、ノンブロッキングIOを使用することですが、これは標準のPythonでは面倒です。非ブロッキングプリミティブを使用して同期IO APIを実装する軽量スレッドライブラリについては、 gevent を参照してください。

サンプルコード

大まかに言って愚かなパイプラインを構築します

{cat /usr/share/dict/words} | grep -v not              \
    | {upcase, filtered tee to stderr} | cut -c 1-10   \
    | {translate 'E' to '3'} | grep K | grep Z | {downcase}

中括弧{}の各ステージはPythonで実装され、他のステージは標準の外部プログラムを使用します。TL;DR:参照この要点

予想される輸入から始めます。

#!/usr/bin/env python
from subprocess import Popen, PIPE
import sys, threading

パイプラインのPythonステージ

パイプラインの最後のPython実装ステージを除くすべてがスレッドに入る必要があるため、IOは他のステージをブロックしません。代わりに、これらはPython =サブプロセスを実際に並行して実行したい場合(GILは避けてください)。

def writer(output):
    for line in open('/usr/share/dict/words'):
        output.write(line)
    output.close()
def filter(input, output):
    for line in input:
        if 'k' in line and 'z' in line: # Selective 'tee'
            sys.stderr.write('### ' + line)
        output.write(line.upper())
    output.close()
def leeter(input, output):
    for line in input:
        output.write(line.replace('E', '3'))
    output.close()

これらはそれぞれ独自のスレッドに配置する必要があります。これは、この便利な関数を使用して行います。

def spawn(func, **kwargs):
    t = threading.Thread(target=func, kwargs=kwargs)
    t.start()
    return t

パイプラインを作成する

PopenとPython spawnを使用したステージを使用して外部ステージを作成します。引数bufsize=-1は、システムのデフォルトのバッファリング(通常は4 kiB)を使用するように指示します。これは通常、デフォルト(バッファなし)またはラインバッファリングよりも高速ですが、遅延なしで出力を視覚的に監視する場合は、ラインバッファリングが必要になります。

grepv   = Popen(['grep','-v','not'], stdin=PIPE, stdout=PIPE, bufsize=-1)
cut     = Popen(['cut','-c','1-10'], stdin=PIPE, stdout=PIPE, bufsize=-1)
grepk = Popen(['grep', 'K'], stdin=PIPE, stdout=PIPE, bufsize=-1)
grepz = Popen(['grep', 'Z'], stdin=grepk.stdout, stdout=PIPE, bufsize=-1)

twriter = spawn(writer, output=grepv.stdin)
tfilter = spawn(filter, input=grepv.stdout, output=cut.stdin)
tleeter = spawn(leeter, input=cut.stdout, output=grepk.stdin)

パイプラインを推進する

上記のように組み立てると、パイプライン内のすべてのバッファーがいっぱいになりますが、最後から読み取っている人がいないため(grepz.stdout)、すべてブロックされます。 grepz.stdout.read()を1回呼び出すだけですべてを読み取ることができますが、大きなファイルには大量のメモリが使用されます。代わりに、増分を読み取ります。

for line in grepz.stdout:
    sys.stdout.write(line.lower())

スレッドとプロセスは、EOFに達するとクリーンアップされます。を使用して明示的にクリーンアップできます

for t in [twriter, tfilter, tleeter]: t.join()
for p in [grepv, cut, grepk, grepz]: p.wait()

Python-2.6以前

内部的には、subprocess.Popenforkを呼び出し、パイプファイル記述子を構成し、execを呼び出します。 forkの子プロセスには、親プロセス内のすべてのファイル記述子のコピーがあり、対応するリーダーがEOF。これは、パイプを手動で閉じるか(close_fds=Trueまたは適切なpreexec_fn引数をsubprocess.Popenに設定することにより)、または FD_CLOEXEC フラグを設定してexecがファイル記述子を自動的に閉じることで修正できます。このフラグはPython-2.7以降で自動的に設定されます。 issue12786 を参照してください。以前のバージョンのPythonを呼び出すことで、Python-2.7の動作を取得できます。

p._set_cloexec_flags(p.stdin)

p.stdinを引数として後続のsubprocess.Popenに渡す前。

6
Jed

パイプを期待どおりに機能させるための3つの主なトリックがあります

  1. パイプの両端が異なるスレッド/プロセスで使用されていることを確認してください(上部にある例のいくつかはこの問題に悩まされています)。

  2. 各プロセスでパイプの未使用の端を明示的に閉じます

  3. バッファリングを無効にする(Python -uオプション)か、ptyを使用するか、データに影響を与えないもの(おそらく '\ n'ですが、適切なもの)でバッファを埋めることによってバッファリングを処理します。

Python "pipeline"モジュール(私は作成者です)の例は、シナリオに正確に適合し、低レベルの手順をかなり明確にします。

http://pypi.python.org/pypi/pipeline/

最近では、プロデューサー-プロセッサー-コンシューマー-コントローラーのパターンの一部としてサブプロセスモジュールを使用しました。

http://www.darkarchive.org/w/Pub/PythonInteract

この例では、ptyを使用せずにバッファリングされた標準入力を扱い、どのパイプの端をどこで閉じる必要があるかも示しています。私はスレッド化よりもプロセスを好みますが、原則は同じです。さらに、プロデューサーにフィードし、コンシューマーから出力を収集するキューの同期と、それらをクリーンにシャットダウンする方法を示します(キューに挿入されたセンチネルに注意してください)。このパターンにより、最近の出力に基づいて新しい入力を生成できるため、再帰的な検出と処理が可能になります。

3
Poor Yorick

Noskloが提供するソリューションは、パイプの受信側に書き込まれるデータが多すぎるとすぐに壊れます。


from subprocess import Popen, PIPE

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE, close_fds=True)
p1.stdin.write('Hello World\n' * 20000)
p1.stdin.close()
result = p2.stdout.read() 
assert result == "Hello Worl\n"

このスクリプトがマシンでハングしない場合は、「20000」をオペレーティングシステムのパイプバッファのサイズを超える値に増やしてください。

これは、オペレーティングシステムが入力を「grep」にバッファリングしているためですが、そのバッファがいっぱいになると、p1.stdin.writeから何かが読み取られるまでp2.stdout呼び出しがブロックされます。おもちゃのシナリオでは、同じプロセスでパイプへの書き込み/パイプからの読み取りを行うことができますが、通常の使用法では、1つのスレッド/プロセスから書き込み、別のスレッド/プロセスから読み取る必要があります。これは、subprocess.popen、os.pipe、os.popen *などに当てはまります。

もう1つの工夫は、同じパイプの以前の出力から生成されたアイテムをパイプに供給し続けたい場合があることです。解決策は、パイプフィーダーとパイプリーダーの両方をmanプログラムと非同期にし、2つのキューを実装することです。1つはメインプログラムとパイプフィーダーの間、もう1つはメインプログラムとパイプリーダーの間です。 PythonInteract はその一例です。

サブプロセスは便利なモデルですが、os.popenとos.forkの呼び出しの詳細が内部に隠されているため、使用する下位レベルの呼び出しよりも処理が難しい場合があります。このため、サブプロセスは、プロセス間パイプが実際にどのように機能するかを知るための良い方法ではありません。

3
Poor Yorick

これはいくつかのスレッドで行う必要があります。そうしないと、データを送信できない状況になります。p2の出力を読み取らないためにp2がp1の出力を読み取らないため、子p1は入力を読み取れません。

したがって、p2が書き出すものを読み取るバックグラウンドスレッドが必要です。これにより、パイプにデータを書き込んだ後もp2を続行できるため、p1から次の入力行を読み取ることができます。これにより、p1は送信されたデータを処理できます。

または、バックグラウンドスレッドを使用してデータをp1に送信し、メインスレッドでp2からの出力を読み取ることもできます。ただし、どちらの側もスレッドである必要があります。

2
Aaron Digulla

close_fds=Trueなしでは実行できないというnoskloの主張(この質問に対する他のコメントを参照)への応答:

close_fds=Trueは、他のファイル記述子を開いたままにしている場合にのみ必要です。複数の子プロセスを開くときは、継承される可能性のある開いているファイルを追跡し、不要なファイルを明示的に閉じることをお勧めします。

from subprocess import Popen, PIPE

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p1.stdin.write('Hello World\n')
p1.stdin.close()
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
result = p2.stdout.read() 
assert result == "Hello Worl\n"

close_fdsのデフォルトはFalseです。これは、サブプロセスが呼び出し側プログラムを信頼して、開いているファイル記述子で何を行っているかを認識し、呼び出し元に、必要に応じてすべてを閉じる簡単なオプションを提供するためです。行う。

しかし、本当の問題は、おもちゃの例を除いて、パイプバッファがあなたを噛むことです。この質問に対する他の回答で述べたように、経験則では、リーダーとライターを同じプロセス/スレッドで開かないようにします。双方向通信にサブプロセスモジュールを使用したい人は、最初にos.pipeとos.forkを研究するのに役立ちます。 良い例 を見ることができれば、実際にはそれほど難しくはありません。

2
Poor Yorick

あなたは間違った問題を調べているのではないかと思います。確かに、アーロンが言うように、パイプラインの最初のプロデューサーとパイプラインの最後のコンシューマーの両方になろうとすると、デッドロック状態に陥りやすくなります。これがcommunicate()が解決する問題です。

stdinとstdoutは異なるサブプロセスオブジェクト上にあるため、communicate()は正確には正しくありません。しかし、subprocess.pyの実装を見ると、Aaronが提案したとおりに動作していることがわかります。

読み取りと書き込みの両方を通信することがわかると、2回目の試行で、communicate()がp1の出力を求めてp2と競合することがわかります。

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
# ...
p1.communicate('data\n')       # reads from p1.stdout, as does p2

私はwin32で実行しています。これは、明らかに異なるI/Oとバッファリングの特性を持っていますが、これは私にとってはうまくいきます。

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
t = threading.Thread(target=get_output, args=(p2,)) 
t.start()
p1.stdin.write('hello world\n' * 100000)
p1.stdin.close()
t.join()

ナイーブなスレッド化されていないp2.read()を使用すると、デッドロックが発生するように入力サイズを調整しました。

また、ファイルにバッファリングしてみることもできます。

fd, _ = tempfile.mkstemp()
os.write(fd, 'hello world\r\n' * 100000)
os.lseek(fd, 0, os.SEEK_SET)
p1 = Popen(["grep", "-v", "not"], stdin=fd, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
print p2.stdout.read()

これは、デッドロックなしでも機能します。

1
Paul Du Bois

上記のコメントの1つで、私はnoskloに、select.selectに関する彼の主張を裏付けるコードを投稿するか、以前に反対票を投じた私の回答に賛成するように要求しました。彼は次のコードで応答しました。

from subprocess import Popen, PIPE
import select

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE, close_fds=True)

data_to_write = 100000 * 'hello world\n'
to_read = [p2.stdout]
to_write = [p1.stdin]
b = [] # create buffer
written = 0


while to_read or to_write:
    read_now, write_now, xlist = select.select(to_read, to_write, [])
    if read_now:
        data = p2.stdout.read(1024)
        if not data:
            p2.stdout.close()
            to_read = []
        else:
            b.append(data)

    if write_now:
        if written < len(data_to_write):
            part = data_to_write[written:written+1024]
            written += len(part)
            p1.stdin.write(part); p1.stdin.flush()
        else:
            p1.stdin.close()
            to_write = []

print b

このスクリプトの問題の1つは、システムパイプバッファのサイズ/性質を2番目に推測することです。スクリプトが1024のようなマジックナンバーを削除できれば、失敗は少なくなります。

大きな問題は、このスクリプトコードが、データ入力と外部プログラムの正しい組み合わせでのみ一貫して機能することです。 grepとcutはどちらも行で機能するため、内部バッファーの動作は少し異なります。 「cat」のようなより一般的なコマンドを使用し、パイプに小さなデータを書き込むと、致命的な競合状態がより頻繁に発生します。

from subprocess import Popen, PIPE
import select
import time

p1 = Popen(["cat"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cat"], stdin=p1.stdout, stdout=PIPE, close_fds=True)

data_to_write = 'hello world\n'
to_read = [p2.stdout]
to_write = [p1.stdin]
b = [] # create buffer
written = 0


while to_read or to_write:
    time.sleep(1)
    read_now, write_now, xlist = select.select(to_read, to_write, [])
    if read_now:
        print 'I am reading now!'
        data = p2.stdout.read(1024)
        if not data:
            p1.stdout.close()
            to_read = []
        else:
            b.append(data)

    if write_now:
        print 'I am writing now!'
        if written < len(data_to_write):
            part = data_to_write[written:written+1024]
            written += len(part)
            p1.stdin.write(part); p1.stdin.flush()
        else:
            print 'closing file'
            p1.stdin.close()
            to_write = []

print b

この場合、2つの異なる結果が現れます。

write, write, close file, read -> success
write, read -> hang

繰り返しになりますが、私はnoskloに、select.selectを使用して単一のスレッドからの任意の入力とパイプのバッファリングを処理することを示すコードを投稿するか、応答に賛成するように要求します。

結論:単一のスレッドからパイプの両端を操作しようとしないでください。それだけの価値はありません。これを正しく行う方法の優れた低レベルの例については、 pipeline を参照してください。

1
Poor Yorick

SpooledTemporaryFileの使用はどうですか?これは問題を回避します(しかしおそらく解決しません):

http://docs.python.org/library/tempfile.html#tempfile.SpooledTemporaryFile

ファイルのように書き込むこともできますが、実際にはメモリブロックです。

それとも私は完全に誤解していますか...

0
Adam Nelson