web-dev-qa-db-ja.com

subprocess.runの出力が同じコマンドのシェルの出力と異なるのはなぜですか?

自動テストにsubprocess.run()を使用しています。主に実行を自動化する:

dummy.exe < file.txt > foo.txt
diff file.txt foo.txt

シェルで上記のリダイレクトを実行すると、2つのファイルは常に同じになります。ただし、file.txtが長すぎる場合は、以下のPythonコードは正しい結果を返しません。

これはPythonコードです:

import subprocess
import sys


def main(argv):

    exe_path = r'dummy.exe'
    file_path = r'file.txt'

    with open(file_path, 'r') as test_file:
        stdin = test_file.read().strip()
        p = subprocess.run([exe_path], input=stdin, stdout=subprocess.PIPE, universal_newlines=True)
        out = p.stdout.strip()
        err = p.stderr
        if stdin == out:
            print('OK')
        else:
            print('failed: ' + out)

if __name__ == "__main__":
    main(sys.argv[1:])

dummy.ccのC++コードは次のとおりです。

#include <iostream>


int main()
{
    int size, count, a, b;
    std::cin >> size;
    std::cin >> count;

    std::cout << size << " " << count << std::endl;


    for (int i = 0; i < count; ++i)
    {
        std::cin >> a >> b;
        std::cout << a << " " << b << std::endl;
    }
}

file.txtは次のようになります。

1 100000
0 417
0 842
0 919
...

最初の行の2番目の整数は後続の行数です。したがって、ここでfile.txtは100,001行になります。

質問:subprocess.run()を誤用していますか?

編集

私の正確なPythonコメント後のコード(改行、rb)が考慮されます:

import subprocess
import sys
import os


def main(argv):

    base_dir = os.path.dirname(__file__)
    exe_path = os.path.join(base_dir, 'dummy.exe')
    file_path = os.path.join(base_dir, 'infile.txt')
    out_path = os.path.join(base_dir, 'outfile.txt')

    with open(file_path, 'rb') as test_file:
        stdin = test_file.read().strip()
        p = subprocess.run([exe_path], input=stdin, stdout=subprocess.PIPE)
        out = p.stdout.strip()
        if stdin == out:
            print('OK')
        else:
            with open(out_path, "wb") as text_file:
                text_file.write(out)

if __name__ == "__main__":
    main(sys.argv[1:])

これが最初の差分です:

enter image description here

これが入力ファイルです: https://drive.google.com/open?id=0B--mU_EsNUGTR3VKaktvQVNtLTQ

9
user2346536

再現するには、シェルコマンド:

_subprocess.run("dummy.exe < file.txt > foo.txt", Shell=True, check=True)
_

pythonのシェルなし:

_with open('file.txt', 'rb', 0) as input_file, \
     open('foo.txt', 'wb', 0) as output_file:
    subprocess.run(["dummy.exe"], stdin=input_file, stdout=output_file, check=True)
_

任意の大きなファイルで動作します。

Python 3.5+でのみ利用可能なsubprocess.check_call()の代わりに、この場合はsubprocess.run()(Python 2以降で利用可能))を使用できます。

ありがとうございます。しかし、なぜ元のものが失敗したのですか?ケビンアンサーのようなパイプバッファーサイズ?

OSパイプバッファとは関係ありません。 @Kevin J. Chaseが引用するサブプロセスドキュメントからの警告は、subprocess.run()とは無関係です。複数のパイプストリーム(_process.stdin/.stdout/.stderr_)を介してprocess = Popen()およびmanuallyread()/ write()を使用する場合にのみ、OSパイプバッファーに注意する必要があります。

観察された動作は niversal CRTのWindowsバグ によるものであることがわかります。 Pythonなしで再現された同じ問題を次に示します。 パイプラインが失敗した場合にリダイレクトが機能する理由

バグの説明 で述べたように、それを回避するには:

  • "バイナリパイプを使用してテキストモードを実行CRLF => LFリーダー側で手動で変換"またはReadFile()を使用_std::cin_の代わりに直接
  • または、この夏のWindows 10アップデートを待ちます(バグが修正される予定です)。
  • または、別のC++コンパイラを使用します。たとえば、 Windowsで_g++_を使用しても問題ありません

このバグはテキストパイプにのみ影響します。つまり、_<>_を使用するコードは問題ありません(_stdin=input_file, stdout=output_file_は引き続き機能するか、他のバグです)。

6
jfs

免責事項から始めます。Python 3.5がないため(run関数を使用できません)、Windows(Pythonで問題を再現できませんでした) 3.4.4)またはLinux(3.1.6)。

_subprocess.PIPE_およびファミリの問題

_subprocess.run_ のドキュメントでは、これは古い_subprocess.Popen_- and -communicate()手法のフロントエンドにすぎないとしています。 _subprocess.Popen.communicate_ のドキュメントは次のことを警告しています:

読み込まれたデータはメモリにバッファリングされるため、データサイズが大きい場合や無制限の場合は、このメソッドを使用しないでください。

これは確かにあなたの問題のように聞こえます。残念ながら、ドキュメントには「大きい」データの量は記載されていません。また、「多すぎる」データが読み取られた後にwhatが発生することもありません。 「それをしないでください」。

_subprocess.call_ のドキュメントをもう少し詳しく説明します(強調は私のものです)...

この関数で_stdout=PIPE_または_stderr=PIPE_を使用しないでください。パイプが読み取られていないため、子プロセスはOSパイプバッファーをいっぱいにするのに十分な出力をパイプに生成する場合をブロックします。

... _subprocess.Popen.wait_ のドキュメントと同様に:

これは、_stdout=PIPE_または_stderr=PIPE_および子プロセスOSパイプバッファーの待機をブロックするような十分な出力を生成しますがより多くのデータを受け入れるために使用すると、デッドロックします。それを避けるためにパイプを使用するときは Popen.communicate() を使用してください。

確かに_Popen.communicate_がこの問題の解決策のように聞こえますが、communicateの独自のドキュメントは、「データサイズが大きい場合はこのメソッドを使用しないでください」--- waitのドキュメントがtocommunicateを使用します。 (おそらく、データを静かに床に落とすことで「回避」するのでしょうか?)

イライラして、私は _subprocess.PIPE_ を安全に使用する方法を考えていません。ただし、子プロセスが書き込むよりも速く読み取ることができると確信している場合を除きます。

そのメモについて...

代替: _tempfile.TemporaryFile_

あなたはallデータをメモリに保持しています...実際には2回特にファイルに既にある場合は、効率的ではありません。

一時ファイルの使用が許可されている場合は、2つのファイルを1行ずつ簡単に比較できます。これは、すべての_subprocess.PIPE_混乱を回避し、一度に少しだけRAMを使用するだけなので、はるかに高速です。(IOオペレーティングシステムが出力リダイレクトを処理する方法によっては、サブプロセスからの処理も高速になる場合があります。

繰り返しますが、runをテストすることはできません。そのため、少し古いPopen- and -communicateソリューションを使用します(mainと残りの設定を差し引いたもの):

_import io
import subprocess
import tempfile

def are_text_files_equal(file0, file1):
    '''
    Both files must be opened in "update" mode ('+' character), so
    they can be rewound to their beginnings.  Both files will be read
    until just past the first differing line, or to the end of the
    files if no differences were encountered.
    '''
    file0.seek(io.SEEK_SET)
    file1.seek(io.SEEK_SET)
    for line0, line1 in Zip(file0, file1):
        if line0 != line1:
            return False
    # Both files were identical to this point.  See if either file
    # has more data.
    next0 = next(file0, '')
    next1 = next(file1, '')
    if next0 or next1:
        return False
    return True

def compare_subprocess_output(exe_path, input_path):
    with tempfile.TemporaryFile(mode='w+t', encoding='utf8') as temp_file:
        with open(input_path, 'r+t') as input_file:
            p = subprocess.Popen(
              [exe_path],
              stdin=input_file,
              stdout=temp_file,  # No more PIPE.
              stderr=subprocess.PIPE,  # <sigh>
              universal_newlines=True,
              )
            err = p.communicate()[1]  # No need to store output.
            # Compare input and output files...  This must be inside
            # the `with` block, or the TemporaryFile will close before
            # we can use it.
            if are_text_files_equal(temp_file, input_file):
                print('OK')
            else:
                print('Failed: ' + str(err))
    return
_

残念ながら、100万行を入力しても問題を再現できないため、これがworksであるかどうかはわかりません。他に何もなければ、それはあなたに間違った答えを速く与えるべきです。

バリエーション:通常ファイル

テスト実行の出力を_foo.txt_で(コマンドラインの例から)保持したい場合は、サブプロセスの出力をTemporaryFileではなく通常のファイルに送信します。これは J.F。セバスチャンの答え で推奨されている解決策です。

wanted_foo.txt_であるか、それが2段階テストの副作用であるか、diffであるかどうかは、質問からはわかりません- -コマンドラインの例では、テスト出力をファイルに保存しますが、Pythonスクリプトでは保存しません。テストの失敗を調査したい場合は、出力を保存すると便利ですが、実行するテストごとに一意のファイル名を考え出すので、お互いの出力を上書きしません。

1
Kevin J. Chase