为什么subprocess.run输出与同一命令的shell输出不同?

时间:2016-06-09 19:18:39

标签: python c++ python-3.x subprocess io-redirection

我正在使用subprocess.run()进行一些自动化测试。主要是自动执行:

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

如果在shell中执行上述重定向,则这两个文件始终相同。但是,只要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
...

第一行的第二个整数是后面的行数,因此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

2 个答案:

答案 0 :(得分:5)

要重现,shell命令:

subprocess.run("dummy.exe < file.txt > foo.txt", shell=True, check=True)

没有Python中的shell:

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)

适用于任意大文件。

在这种情况下你可以使用subprocess.check_call()(从Python 2开始使用),而不是仅在Python 3.5 +中可用的subprocess.run()

  

非常感谢。但那么为什么原来失败了呢?管道缓冲区大小,如Kevin Answer?

它与OS管道缓冲区无关。来自@Kevin J. Chase引用的子流程文档的警告与subprocess.run()无关。只有在通过多个管道流(process = Popen())手动使用process.stdin/.stdout/.stderr手动 read()/ write()时,才应该关心OS管道缓冲区。

事实证明,观察到的行为归因于Windows bug in the Universal CRT。以下是没有Python的同一问题:Why would redirection work where piping fails?

正如the bug description中所述,要解决它:

  • “使用二进制管道并在阅读器端手动执行文本模式CRLF =&gt; LF翻译”或直接使用ReadFile()代替std::cin
  • 或等待今年夏天的Windows 10更新(应该修复错误)
  • 或使用其他C ++编译器,例如no issue if you use g++ on Windows

该错误仅影响文本管道,即使用<>的代码应该没问题(stdin=input_file, stdout=output_file应该仍然有用或者是其他错误)。

答案 1 :(得分:1)

我将以免责声明开头:我没有Python 3.5(因此我无法使用run功能),而且我无法复制您在Windows(Python 3.4.4)或Linux(3.1.6)上的问题。那说......

subprocess.PIPE和家庭

的问题

subprocess.run文档说它只是旧subprocess.Popen - 和 - communicate()技术的前端。 subprocess.Popen.communicate文档警告说:

  

读取的数据缓冲在内存中,因此如果数据量很大或无限制,请不要使用此方法。

这肯定听起来像你的问题。不幸的是,文档并没有说明在#34;太多&#34;之后会发生多少数据&#34;以及将会发生什么?读取数据。只是&#34;不要那样做,然后&#34;。

subprocess.call的文档更详细一点(强调我的)......

  

请勿对此功能使用stdout=PIPEstderr=PIPE。子进程将阻止如果它为管道生成足够的输出以填充OS管道缓冲区,因为没有读取管道。

... subprocess.Popen.wait的文档也是如此:

  

当使用stdout=PIPEstderr=PIPE并且子进程生成足够的输出到管道以阻止等待OS管道缓冲区接受更多数据时,这将会死锁。使用管道时使用Popen.communicate()来避免这种情况。

确实听起来像Popen.communicate是解决此问题的方法,但communicate自己的文档说&#34;如果数据量很大,请不要使用此方法&#34 ; ---确切地说wait文档告诉你 使用communicate的情况。 (也许它&#34;通过静默地丢弃地板上的数据来避免&#34;)

令人沮丧的是,我没有看到任何安全使用subprocess.PIPE的方法,除非你确定你可以比你的子进程写入它更快地读取它。

就此而言......

替代方案:tempfile.TemporaryFile

您实际上已将所有数据保存在内存中......两次。这不会有效,特别是如果它已经存在于文件中。

如果您允许使用临时文件,则可以非常轻松地比较这两个文件,一次一行。这避免了所有subprocess.PIPE混乱,而且速度更快,因为它一次只使用一点RAM。 (子进程中的IO也可能更快,具体取决于操作系统处理输出重定向的方式。)

同样,我无法对run进行测试,因此这里的Popen - 和 - 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 你的设置):

foo.txt

不幸的是,由于我无法重现您的问题,即使使用百万行输入,我也无法判断此是否有效。如果不出意外,它应该更快地给你错误的答案。

变体:常规文件

如果您希望将测试运行的输出保持在TemporaryFile(来自您的命令行示例),那么您将指导您的子流程&#39;输出到普通文件而不是foo.txt。这是J.F. Sebastian's answer中推荐的解决方案。

如果您想要 diff,或者如果它只是两步测试的副作用,我就无法告诉您问题 - {{1}你的命令行示例将测试输出保存到文件中,而你的Python脚本却没有。如果您想要调查测试失败,保存输出会很方便,但它需要为您运行的每个测试提供唯一的文件名,因此它们不会覆盖彼此的输出。