为什么 subprocess.run 输出与 shell 相同命令的输出不同?
Why is subprocess.run output different from shell output of same command?
我正在使用 subprocess.run()
进行一些自动化测试。主要是为了自动执行:
dummy.exe < file.txt > foo.txt
diff file.txt foo.txt
如果在shell中执行上述重定向,这两个文件总是相同的。但是只要 file.txt
太长,下面的 Python 代码就不会 return 正确的结果。
这是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() 吗?
编辑
考虑到评论(换行符、rb)后我的确切 Python 代码:
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
我将从免责声明开始:我没有 Python 3.5(所以我无法使用 run
功能),而且我无法重现您的问题在 Windows (Python 3.4.4) 或 Linux (3.1.6) 上。也就是说...
subprocess.PIPE
和家庭
的问题
subprocess.run
docs say that it's just a front-end for the old subprocess.Popen
-and-communicate()
technique. The subprocess.Popen.communicate
文档警告说:
The data read is buffered in memory, so do not use this method if the data size is large or unlimited.
这听起来很像你的问题。不幸的是,文档没有说明 "large" 有多少数据,也没有说明 读取 "too much" 数据后会发生什么 。只是 "don't do that, then".
subprocess.call
的文档更加详细(强调我的)...
Do not use stdout=PIPE
or stderr=PIPE
with this function. The child process will block if it generates enough output to a pipe to fill up the OS pipe buffer as the pipes are not being read from.
...subprocess.Popen.wait
的文档也是如此:
This will deadlock when using stdout=PIPE
or stderr=PIPE
and the child process generates enough output to a pipe such that it blocks waiting for the OS pipe buffer to accept more data. Use Popen.communicate()
when using pipes to avoid that.
这听起来确实像 Popen.communicate
是这个问题的解决方案,但是 communicate
自己的文档说 "do not use this method if the data size is large" --- 正是 wait
的情况文档告诉您 到 使用 communicate
。 (也许是 "avoid(s) that" 悄悄地把数据丢在地上?)
令人沮丧的是,我看不到任何安全使用 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
--- 您的命令行示例将测试输出保存到一个文件中,而您的 Python 脚本则不会。如果您想调查测试失败,保存输出会很方便,但它需要为您的每个测试提供一个唯一的文件名运行,这样它们就不会覆盖彼此的输出。
要重现,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()
。
Works very well thanks. But then why was the original failing ? Pipe buffer size as in Kevin Answer ?
它与 OS 管道缓冲区无关。 @Kevin J. Chase 引用的来自子流程文档的警告与 subprocess.run()
无关。仅当您通过多个管道流 (process.stdin/.stdout/.stderr
) 使用 process = Popen()
和 手动 read()/write() 时,您才应该关心 OS 管道缓冲区.
事实证明,观察到的行为是由于 Windows bug in the Universal CRT. Here's the same issue that is reproduced without Python: Why would redirection work where piping fails?
如 the bug description 中所述,解决方法:
- "use a binary pipe and do text mode CRLF => LF translation manually on the reader side" 或直接使用
ReadFile()
而不是 std::cin
- 或者等待Windows今年夏天的10更新(bug应该修复)
- 或使用不同的 C++ 编译器,例如 no issue if you use
g++
on Windows
该错误仅影响文本管道,即使用 <>
的代码应该没问题(stdin=input_file, stdout=output_file
应该仍然有效,否则它是其他错误)。
我正在使用 subprocess.run()
进行一些自动化测试。主要是为了自动执行:
dummy.exe < file.txt > foo.txt
diff file.txt foo.txt
如果在shell中执行上述重定向,这两个文件总是相同的。但是只要 file.txt
太长,下面的 Python 代码就不会 return 正确的结果。
这是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() 吗?
编辑
考虑到评论(换行符、rb)后我的确切 Python 代码:
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
我将从免责声明开始:我没有 Python 3.5(所以我无法使用 run
功能),而且我无法重现您的问题在 Windows (Python 3.4.4) 或 Linux (3.1.6) 上。也就是说...
subprocess.PIPE
和家庭
的问题
subprocess.run
docs say that it's just a front-end for the old subprocess.Popen
-and-communicate()
technique. The subprocess.Popen.communicate
文档警告说:
The data read is buffered in memory, so do not use this method if the data size is large or unlimited.
这听起来很像你的问题。不幸的是,文档没有说明 "large" 有多少数据,也没有说明 读取 "too much" 数据后会发生什么 。只是 "don't do that, then".
subprocess.call
的文档更加详细(强调我的)...
Do not use
stdout=PIPE
orstderr=PIPE
with this function. The child process will block if it generates enough output to a pipe to fill up the OS pipe buffer as the pipes are not being read from.
...subprocess.Popen.wait
的文档也是如此:
This will deadlock when using
stdout=PIPE
orstderr=PIPE
and the child process generates enough output to a pipe such that it blocks waiting for the OS pipe buffer to accept more data. UsePopen.communicate()
when using pipes to avoid that.
这听起来确实像 Popen.communicate
是这个问题的解决方案,但是 communicate
自己的文档说 "do not use this method if the data size is large" --- 正是 wait
的情况文档告诉您 到 使用 communicate
。 (也许是 "avoid(s) that" 悄悄地把数据丢在地上?)
令人沮丧的是,我看不到任何安全使用 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
--- 您的命令行示例将测试输出保存到一个文件中,而您的 Python 脚本则不会。如果您想调查测试失败,保存输出会很方便,但它需要为您的每个测试提供一个唯一的文件名运行,这样它们就不会覆盖彼此的输出。
要重现,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()
。
Works very well thanks. But then why was the original failing ? Pipe buffer size as in Kevin Answer ?
它与 OS 管道缓冲区无关。 @Kevin J. Chase 引用的来自子流程文档的警告与 subprocess.run()
无关。仅当您通过多个管道流 (process.stdin/.stdout/.stderr
) 使用 process = Popen()
和 手动 read()/write() 时,您才应该关心 OS 管道缓冲区.
事实证明,观察到的行为是由于 Windows bug in the Universal CRT. Here's the same issue that is reproduced without Python: Why would redirection work where piping fails?
如 the bug description 中所述,解决方法:
- "use a binary pipe and do text mode CRLF => LF translation manually on the reader side" 或直接使用
ReadFile()
而不是std::cin
- 或者等待Windows今年夏天的10更新(bug应该修复)
- 或使用不同的 C++ 编译器,例如 no issue if you use
g++
on Windows
该错误仅影响文本管道,即使用 <>
的代码应该没问题(stdin=input_file, stdout=output_file
应该仍然有效,否则它是其他错误)。