【问题标题】:Why is subprocess.run output different from shell output of same command?为什么 subprocess.run 输出与同一命令的 shell 输出不同?
【发布时间】:2016-10-10 14:36:45
【问题描述】:

我正在使用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:])

这是第一个差异:

这里是输入文件:https://drive.google.com/open?id=0B--mU_EsNUGTR3VKaktvQVNtLTQ

【问题讨论】:

  • 您可能没有正确刷新缓冲区。
  • 你是什么意思?我该如何冲洗它?
  • @user2346536 如果是冲洗问题,您可以使用sys.stdout.flush()(不确定是否存在)。这个文件有多长?
  • 10000 行太长了。测试冲洗...
  • 刷新不起作用。选择的答案解释了为什么,因为无论如何,我在刷新之前都在内存中。但是不错的尝试,无论如何+1 :)

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


【解决方案1】:

要重现,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 中的管道缓冲区大小?

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

事实证明,观察到的行为是由于Windows bug in the Universal CRT。这是在没有 Python 的情况下重现的相同问题:Why would redirection work where piping fails?

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

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

该错误仅影响文本管道,即使用 &lt;&gt; 的代码应该没问题(stdin=input_file, stdout=output_file 应该仍然可以工作,或者它是其他一些错误)。

【讨论】:

  • 效果很好,谢谢。但是为什么原来的失败了? Kevin Answer 中的管道缓冲区大小?
  • subprocess.run 文档(我每天都不太信任它)说:“完整的函数签名与 Popen 构造函数的签名基本相同 [...] 所有参数到这个函数被传递到那个接口。”因此,在Popen(包括communicatewait)中发现的任何警告都必须适用于run,包括“PIPE 中的输出过多”警告。也就是说,subprocess 文档在某些地方完全自相矛盾......
  • @KevinJ.Chase:错了。警告(关于操作系统管道缓冲区)不适用于.run(),因为它已经调用了.communicate() 方法。您确实在文档中看到了“使用管道时使用 Popen.communicate() 来避免这种情况。”
  • "您确实在文档中看到了“使用 Popen.communicate()...”。" ---是的,我在回答中引用了它。这就是我上面提到的直接矛盾...... waitcall 文档告诉您使用 communicate 来“避免”将大量数据写入 PIPE 的问题,而 communicate 文档专门告诉你将大量数据写入PIPE。一个说“使用这个 --- 它解决了问题”,而另一个说“不要 使用这个 --- 它无法解决这个问题”。 (当我今晚晚些时候有机会时,我会单独写一个关于这个的问题。)
  • @KevinJ.Chase:同样,正如我之前所说的,“OS 管道缓冲区”问题与 “内存不足” 问题不同.communicate() 说您不应该尝试读取不适合内存的数据:这很简单:它将该数据作为 str/bytes 对象返回,该对象必须在 Python 的内存中。 “操作系统管道缓冲区”(通常)比可用内存小得多,为了避免这个问题,如果您使用stdout=PIPE,您只需消耗管道即可。 call(), .wait() 不消耗管道,因此您不应将 PIPE 与它们一起使用。文档在这里并不自相矛盾。
【解决方案2】:

我将首先声明:我没有 Python 3.5(所以我不能使用 run 函数),并且我无法在 Windows 上重现您的问题(Python 3.4.4)或 Linux (3.1.6)。那就是……

subprocess.PIPE 和家人的问题

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

读取的数据是缓存在内存中的,所以如果数据量很大或者没有限制,不要使用这种方法。

这听起来确实是您的问题。不幸的是,文档没有说有多少数据是“大”的,也没有说在读取“太多”数据后会发生什么。只是“不要那样做,那么”。

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

不要在此函数中使用stdout=PIPEstderr=PIPE如果子进程生成足够的输出到管道以填满操作系统管道缓冲区,则子进程将阻塞,因为管道没有被读取。

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

这将在使用stdout=PIPEstderr=PIPE 时出现死锁,并且子进程向管道生成足够的输出,从而阻塞等待操作系统管道缓冲区 接受更多数据。使用管道时使用Popen.communicate() 来避免这种情况。

这听起来确实像Popen.communicate 是这个问题的解决方案,但是communicate 自己的文档说“如果数据量很大,请不要使用这种方法” --- 正是wait 的情况文档告诉你使用communicate。 (也许它通过默默地将数据丢在地板上来“避免这种情况”?)

令人沮丧的是,我看不到任何安全使用 subprocess.PIPE 的方法,除非您确定读取它的速度比您的子进程写入它的速度要快。

关于那个笔记...

备选:tempfile.TemporaryFile

您将所有您的数据保存在内存中...实际上是两次。这不会是有效的,尤其是如果它已经在一个文件中。

如果允许您使用临时文件,您可以非常轻松地比较这两个文件,一次一行。这避免了所有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

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

变体:常规文件

如果您想将测试运行的输出保存在foo.txt(来自您的命令行示例),那么您可以将子进程的输出定向到普通文件而不是TemporaryFile。这是J.F. Sebastian's answer推荐的解决方案。

我无法从您的问题中判断您是否想要 foo.txt,或者这只是两步测试的副作用-然后-diff --- 您的命令-line 示例将测试输出保存到文件中,而您的 Python 脚本不会。如果您想调查测试失败,保存输出会很方便,但它需要为您运行的每个测试提供一个唯一的文件名,这样它们就不会覆盖彼此的输出。

【讨论】:

  • @user2346536:我修复了我的are_text_files_equal 函数中的一个错误 --- 如果两个长度不等的文件在较短的文件结束之前是相同的,它可能会被愚弄。在返回 True 之前,它没有验证两个文件是否都已结束。
  • @J.F.Sebastian:我从没说过。当 user2346536 从真实文件中重定向子进程的 input 时;它的输出仍然是subprocess.PIPEcommunicate / run 文档特别说不要为“大或无限”输出做。我提出TemporaryFile 是为了避免PIPE 的问题。作为奖励,TemporaryFile 避免将大量数据加载到内存中(两次!),因为这些数据一次使用的次数不超过一行。
  • @J.F.Sebastian:也许你在谈论我引用Popen.waitcall 文档?没错,user23456536 从未使用过这些功能。我引用了它们,因为它们是subprocess 模块文档中唯一解决PIPE 中存储“太多”数据的想法的地方。它们是唯一描述后果的地方。他们指出问题是一个永久阻塞的子进程,communicate 通过替换其他一些未指定的问题来避免这种情况。这些文档最接近于描述真正的问题是什么,更不用说如何避免它了。
  • @KevinJ.Chase:1- 文档中您用粗体格式突出显示的警告:“如果它生成足够的输出到管道以填满操作系统管道缓冲区”subprocess.run() 无关——它只是不适用——如果你不明白为什么然后问一个单独的 SO 问题。 2- 此外,OPs 代码中的错误不是由于内存不足(这在一般情况下适用于subprocess.run(),但在这种情况下并不重要:输入/输出确实适合内存)。我猜问题是通用换行符模式。
  • @user2346536:如果 J.F. Sebastian 的回答既重现了您的问题 解决了它,那么您应该接受他的回答。 (即使在 Windows 或 Linux 上使用他的方法,我仍然无法重现您的问题。我仍然必须使用 subprocess.callPopen 而不是 run 因为我没有 Python 3.5,所以也许这就是区别。)
猜你喜欢
  • 1970-01-01
  • 2012-06-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-05
  • 2013-02-22
  • 2020-10-01
相关资源
最近更新 更多