サブプロセスからリアルタイムでstdoutをキャッチする


87

subprocess.Popen()Windowsでrsync.exeを実行し、Pythonでstdoutを出力したいと思います。

私のコードは機能しますが、ファイル転送が完了するまで進行状況をキャッチしません!各ファイルの進捗状況をリアルタイムで印刷したい。

Python 3.1を使用すると、IOの処理に優れていると聞いたので。

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


1
(グーグルから来ていますか?)PIPEのバッファの1つがいっぱいになり、読み取られなくなると、すべてのPIPEがデッドロックします。たとえば、stderrがいっぱいになったときのstdoutデッドロック。読むつもりのないパイプは絶対に渡さないでください。
Nasser Al-Wohaibi 2014年

subprocess.PIPEではなくsys.stdoutにstdoutを設定できない理由を誰かが説明できますか?
マイク

回答:


96

の経験則subprocess

  • 絶対に使用しないでくださいshell=True。プログラムを呼び出すために、不必要に追加のシェルプロセスを呼び出します。
  • プロセスを呼び出すとき、引数はリストとして渡されます。sys.argvPythonではリストでありargv、Cでも同様です。したがって、文字列ではなく、サブプロセスを呼び出すためにリストを渡しPopenます。
  • リダイレクトしないでくださいstderrPIPEあなたがそれを読んでいないとき。
  • stdin書き込みを行っていないときはリダイレクトしないでください。

例:

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

とはいえ、rsyncは、端末ではなくパイプに接続されていることを検出すると、出力をバッファリングする可能性があります。これはデフォルトの動作です。パイプに接続する場合、プログラムはリアルタイムの結果を得るためにstdoutを明示的にフラッシュする必要があります。そうしないと、標準Cライブラリがバッファリングします。

それをテストするには、代わりにこれを実行してみてください。

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

test_out.pyの内容のファイルを作成します。

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

そのサブプロセスを実行すると、「Hello」が表示され、10秒待ってから「World」が表示されます。上記のPythonコードで発生し、では発生しない場合は、それ自体が出力をバッファリングしrsyncていることを意味するrsyncため、運が悪いことになります。

解決策はpty、のようなものを使用して、に直接接続することですpexpect


12
shell=False特にユーザーが入力したデータからコマンドラインを作成する場合は正しいことです。ただし、それでもshell=True、信頼できるソースからコマンドライン全体を取得する場合(スクリプトにハードコードされている場合など)にも役立ちます。
Denis Otkidach 2009年

10
@Denis Otkidach:の使用を正当化するとは思わないshell=True。考えてみてください。文字列分割するためだけに、メモリ割り当て、ディスク使用量、プロセッサスケジューリングなど、OSで別のプロセスを呼び出しています。そして、あなたが参加したもの!Pythonで分割することもできますが、とにかく各パラメーターを個別に記述する方が簡単です。また、手段をリストを使用して、あなたは特別なシェル文字をエスケープする必要はありません:スペース、;><&...あなたのパラメータは、それらの文字を含めることができますし、心配する必要はありません!shell=Trueシェルのみのコマンドを実行していない限り、実際に使用する理由がわかりません。
nosklo 2009年

nosklo、次のようになります。p= subprocess.Popen(cmd、stdout = subprocess.PIPE、stderr = subprocess.STDOUT)
Senthil Kumaran

1
@mathtick:なぜこれらの操作を別々のプロセスとして実行するのかわかりません...csvモジュールを使用することで、Pythonでファイルの内容を切り取り、最初のフィールドを簡単に抽出できます。ただし、例として、Pythonのパイプラインは次のようになりp = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultます。シェルが関与していないため、エスケープせずに長いファイル名とシェルの特殊文字を操作できることに注意してください。また、プロセスが1つ少ないため、はるかに高速です。
nosklo 2010年

11
Python 2のfor line in iter(p.stdout.readline, b'')代わりに使用してください。for line in p.stdoutそうしないと、ソースプロセスが出力をバッファリングしなくても、行がリアルタイムで読み取られません。
jfs 2013年

41

これは古いトピックですが、現在解決策があります。オプション--outbuf = Lを指定してrsyncを呼び出します。例:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())

3
これは機能し、将来の読者が上記のすべてのダイアログをスクロールするのを防ぐために賛成する必要があります。
vectorVictor 2016年

1
@VectorVictor何が起こっているのか、なぜ起こっているのかについては説明していません。次の状態になるまで、プログラムが機能する可能性があります。1。preexec_fn=os.setpgrpプログラムを親スクリプトから存続させるために追加する2.プロセスのパイプからの読み取りをスキップする3.プロセスが大量のデータを出力し、パイプを埋める4.何時間もスタックする、実行中のプログラムがランダムな時間の後に終了する理由を理解しようとしています。@noskloからの回答は私を大いに助けました。
danuker 2017

15

Linuxでは、バッファリングを取り除くという同じ問題がありました。私はついに「stdbuf-o0」(またはexpectからのバッファリング解除)を使用して、PIPEバッファリングを取り除きました。

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

次に、stdoutでselect.selectを使用できます。

/unix/25372/も参照してください


2
PythonからCコードstdoutを取得しようとしている人にとって、このソリューションが私のために機能した唯一のソリューションであったことを確認できます。明確にするために、私はPopenの既存のコマンドリストに「stdbuf」、「-o0」を追加することについて話しています。
無謀な2017

ありがとうございました!C ++アプリを生成し、特定のログステートメントを出力することを確認するために作成した一連のpytest / pytest-bddテストstdbuf -o0非常に役立つことが証明されました。がないとstdbuf -o0、これらのテストはC ++プログラムから(バッファリングされた)出力を取得するのに7秒かかりました。今、彼らはほぼ瞬時に実行されます!
evadeflow

11

ユースケースによっては、サブプロセス自体のバッファリングを無効にすることもできます。

サブプロセスがPythonプロセスになる場合は、呼び出しの前にこれを行うことができます。

os.environ["PYTHONUNBUFFERED"] = "1"

または、これをenv引数に渡しますPopen

それ以外の場合は、Linux / Unixを使用している場合は、stdbufツールを使用できます。例:

cmd = ["stdbuf", "-oL"] + cmd

または他のオプションについてもここを参照してくださいstdbuf


1
あなたはPYTHONUNBUFFERED = 1のために、私の一日感謝を救う
diewland

9
for line in p.stdout:
  ...

次の改行まで常にブロックします。

「リアルタイム」の動作を行うには、次のような操作を行う必要があります。

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

whileループは、子プロセスが標準出力を閉じるか終了すると終了します。 read()/read(-1)子プロセスが標準出力を閉じるか終了するまでブロックします。


1
inchar代わりにNone使用されることはありませんif not inchar:read()EOFで空の文字列を返します)。ところで、for line in p.stdoutPython 2ではリアルタイムで全行も印刷されないのはさらに悪いことですfor line in (代わりにiter(p.stdout.readline、 '') `を使用できます)。
jfs 2013年

1
私はこれをosx上のpython3.4でテストしましたが、機能しません。
qed 2014年

1
@qed:for line in p.stdout:Python 3で動作します。''(Unicode文字列)とb''(バイト)の違いを必ず理解してください。Pythonを
jfs 2016年

8

あなたの問題は:

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

イテレータ自体には追加のバッファリングがあります。

このようにしてみてください:

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

5

stdoutをバッファなしでパイプに出力することはできないので(stdoutに出力するプログラムを書き直すことができない限り)、これが私の解決策です。

stdoutをバッファリングされていないsterrにリダイレクトします。 '<cmd> 1>&2'それをする必要があります。次のようにプロセスを開きますmyproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
。stdoutまたはstderrと区別することはできませんが、すべての出力をすぐに取得できます。

これがこの問題に取り組む人の助けになることを願っています。


4
試しましたか?...標準出力がそのプロセスにバッファリングされている場合、それは仕事が..それはそれはパイプまたはファイルにリダイレクトされていないのと同じ方法で標準エラー出力にリダイレクトされることはありませんしていないため
フィリペピナ

5
これは明らかに間違っています。stdoutバッファリングは、プログラム自体の内部で発生します。シェル構文1>&2は、プログラムを起動する前に、ファイル記述子が指すファイルを変更するだけです。プログラム自体は、stdoutをstderr(1>&2)にリダイレクトするか、その逆(2>&1)にリダイレクトするかを区別できないため、これはプログラムのバッファリング動作に影響を与えません。どちらの方法でも、1>&2構文はシェルによって解釈されます。 subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)指定していないため失敗しますshell=True
マンリーは2016

人々がこれを読んでいる場合:stdoutの代わりにstderrを使用してみましたが、まったく同じ動作を示しています。
martinthenext 2016年

3

rsyncプロセスからstdoutをバッファリング解除に変更します。

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)

3
バッファリングはrsync側で発生し、Python側でbufsize属性を変更しても役に立ちません。
nosklo 2009年

14
他の検索者にとって、noskloの答えは完全に間違っています。rsyncの進行状況の表示はバッファリングされません。本当の問題は、サブプロセスがファイルオブジェクトを返し、ファイルイテレータインターフェイスにbufsize = 0の場合でも十分に文書化されていない内部バッファがあり、バッファがいっぱいになる前に結果が必要な場合はreadline()を繰り返し呼び出す必要があることです。
クリスアダムス

3

出力のキャッシュを回避するには、pexpectを試してみてください。

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

PS:私はこの質問がかなり古いことを知っています、それでも私のために働いた解決策を提供します。

PPS:別の質問からこの回答を得ました


3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

私はPythonでrsyncのGUIを書いていますが、同じプローブを持っています。この問題は、pyDocでこれを見つけるまで、数日間私を悩ませてきました。

Universal_newlinesがTrueの場合、ファイルオブジェクトstdoutおよびstderrは、ユニバーサル改行モードでテキストファイルとして開かれます。行は、Unixの行末規則である「\ n」、古いMacintoshの規則である「\ r」、またはWindowsの規則である「\ r \ n」のいずれかで終了できます。これらの外部表現はすべて、Pythonプログラムでは「\ n」と見なされます。

変換が行われているとき、rsyncは '\ r'を出力するようです。


1

一時ファイルを中間として使用することについての言及がないことに気づきました。以下は、一時ファイルに出力することでバッファリングの問題を回避し、ptyに接続せずにrsyncからのデータを解析できるようにします。Linuxボックスで以下をテストしましたが、rsyncの出力はプラットフォーム間で異なる傾向があるため、出力を解析するための正規表現は異なる場合があります。

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...

リアルタイムではありません。ファイルはrsync側のバッファリングの問題を解決しません。
jfs 2012

tempfile.TemporaryFileは、例外が発生した場合のクリーンアップを容易にするために自分自身を削除できます
jfs 2012

3
while not p.poll()サブプロセスが0で正常に終了した場合、無限ループが発生します。p.poll() is None代わりに使用してください
jfs 2012

Windowsは、すでに開いているファイルを開くことを禁止しているopen(file_name)可能性があるため、失敗する可能性があります
jfs 2012

1
残念ながらLinuxでのみこの答えを見つけましたが、チャームリンクのように機能する ので、コマンドを次のように拡張します。 command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argv呼び出し:popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) バッファリングなしで読み取ることができます
Arvid Terzibaschian 2016

0

このようなものをスレッドで実行し、ffmpeg_timeプロパティをメソッドのプロパティに保存してアクセスできるようにすると、非常にうまく機能します。次のような出力が得られます 。tkinterでスレッドを使用する場合の出力

input = 'path/input_file.mp4'
output = 'path/input_file.mp4'
command = "ffmpeg -y -v quiet -stats -i \"" + str(input) + "\" -metadata title=\"@alaa_sanatisharif\" -preset ultrafast -vcodec copy -r 50 -vsync 1 -async 1 \"" + output + "\""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
for line in self.process.stdout:
    reg = re.search('\d\d:\d\d:\d\d', line)
    ffmpeg_time = reg.group(0) if reg else ''
    print(ffmpeg_time)

-1

Python 3のソリューションは次のとおりです。コマンドラインからコマンドを削除し、受信時にリアルタイムで適切にデコードされた文字列を配信します。

レシーバー(receiver.py):

import subprocess
import sys

cmd = sys.argv[1:]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in p.stdout:
    print("received: {}".format(line.rstrip().decode("utf-8")))

リアルタイム出力を生成できる簡単なプログラムの例(dummy_out.py):

import time
import sys

for i in range(5):
    print("hello {}".format(i))
    sys.stdout.flush()  
    time.sleep(1)

出力:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.