【问题标题】:How do I pipe to a file or shell program via Pythons subprocess?如何通过 Python 子进程管道到文件或 shell 程序?
【发布时间】:2016-07-01 13:08:44
【问题描述】:

我正在处理一些相当大的 gzip 压缩文本文件,我必须对其进行解压缩、编辑和重新压缩。我使用 Python 的 gzip 模块进行解压和压缩,但我发现我目前的实现远非最佳:

input_file = gzip.open(input_file_name, 'rb')

output_file = gzip.open(output_file_name, 'wb')

for line in input_file:
    # Edit line and write to output_file

这种方法慢得让人难以忍受——可能是因为使用 gzip 模块进行每行迭代会产生巨大的开销:我最初还运行了一个行计数例程,其中我使用 gzip 模块读取文件的块并然后计算每个块中换行符的数量,这非常快!

因此,其中一项优化绝对应该是以块的形式读取我的文件,然后仅在解压缩块后才进行每行迭代。

作为额外的优化,我看到了一些通过子进程在 shell 命令中解压缩的建议。使用这种方法,上面第一行的等价物可能是:

from subprocess import Popen, PIPE

file_input = Popen(["zcat", fastq_filename], stdout=PIPE)

input_file = file_input.stdout

使用这种方法 input_file 变成了一个类似文件的对象。我不知道它在可用属性和方法方面与真实文件对象有何不同,但一个区别是您显然不能使用 seek,因为它是流而不是文件。

这确实运行得更快,而且应该 - 除非您在声称的单核机器上运行您的脚本。后者一定意味着子进程会尽可能自动将不同的线程发送到不同的内核,但我不是那里的专家。

现在解决我当前的问题:我想以类似的方式压缩我的输出。也就是说,我不想使用 Python 的 gzip 模块,而是将其通过管道传输到子进程,然后调用 shell gzip。这样我就有可能在不同的内核中进行阅读、编辑和写作,这对我来说听起来非常有效。 我对此进行了微不足道的尝试,但尝试写入 output_file 会导致一个空文件。最初,我使用 touch 命令创建了一个空文件,因为如果文件不存在,Popen 会失败:

call('touch ' + output_file_name, shell=True)

output = Popen(["gzip", output_file_name], stdin=PIPE)

output_file = output.stdin

非常感谢任何帮助,顺便说一下,我使用的是 Python 2.7。谢谢。

【问题讨论】:

  • 如果您有性能问题,然后创建一个可用作基准的最小代码示例并说明所需的目标(例如,使其处理(gzipped)数据 100MB/s -- 我怀疑你的磁盘比这快得多——处理(压缩)数据的速度比你的磁盘读/写的速度更快)——并将其作为一个单独的问题发布。
  • 不是真的 - 只是提供一些背景,以防我处理这一切都错了。我真正想知道的是如何通过管道传输到 shell 程序,在我的情况下最终得到一个不错的 gzip 文件。读/写速度不是这里的问题。如果输入和输出文件是纯文本文件,即具有更多数据要读/写的更大文件,它运行得更快。总体而言,在 shell 中解压缩输入文件、运行脚本和在 shell 中压缩输出文件也更快。

标签: python bash shell subprocess popen


【解决方案1】:

这是一个如何做到这一点的工作示例:

#!/usr/bin/env python

from subprocess import Popen, PIPE

output = ['this', 'is', 'a', 'test']

output_file_name = 'pipe_out_test.txt.gz'

gzip_output_file = open(output_file_name, 'wb', 0)

output_stream = Popen(["gzip"], stdin=PIPE, stdout=gzip_output_file)  # If gzip is supported

for line in output:
    output_stream.stdin.write(line + '\n')

output_stream.stdin.close()
output_stream.wait()

gzip_output_file.close()

如果我们的脚本只写到控制台并且我们想要压缩输出,那么与上述等效的 shell 命令可能是:

script_that_writes_to_console | gzip > output.txt.gz

【讨论】:

  • 1- 这是我的建议(你应该给予信任)。 2- 如果您的输入是事先知道的,您可以使用 gzip_process.communicate("\n".join(output)) 代替 3- gzip_output_file.close() 可以在父级中的 Popen() 之后立即调用 4- gzip_process 是比 output_stream 更好的名称(gzip 过程是不是流本身(它不是类似文件的对象),它是一个可能具有与之关联的 stdin/stdout/stderr 流的进程。
  • @J.F.塞巴斯蒂安 事情就是这样。当我最终破解这个问题时,并不是因为你的回答。我完全误解了Popen,这就是我一直误解你的原因。起作用的是,我完全被你居高临下的语气激怒了,这促使我再次阅读课程的文档,直到突然一切都点击了,我可以让我的代码工作了。我没有给你信用,因为对我来说,你的回答不清楚,我不明白。你明白正确和清楚是有区别的吗?你应该多注意这样的细节。
  • 感谢您的反馈。我假设this comment offends you。您将如何以不那么“浓缩的语气” 传达相同的信息? (output_filegzip_output_file 是不同的名称,指代不同的对象——您无需了解有关 subprocess 模块的任何内容即可理解它)
  • 它不会冒犯我。您似乎关注的对象讨论从来都不是问题。我在区分对象方面没有问题,我同意你的观点,你不必了解 subprocess 或任何其他模块就可以在 Python 中区分一个对象和另一个对象。我误解了 Popen 以及如何正确使用可用的参数。
【解决方案2】:

你的意思是output_file = gzip_process.stdin。之后,您可以像以前使用 gzip.open() 对象一样使用 output_file(不查找)。

如果结果文件为空,请检查您在 Python 脚本末尾是否调用了 output_file.close()gzip_process.wait()。此外,gzip 的用法可能不正确:如果 gzip 将压缩输出写入其标准输出,则传递 stdout=gzip_output_file where gzip_output_file = open(output_file_name, 'wb', 0)

【讨论】:

  • 是的,您对第一部分的看法是正确的。我已经编辑了它,所以现在更清楚了。另外,我添加了之前省略的调用命令。我之前确实关闭了文件并尝试了 wait() 方法 - 文件仍然是空的。
  • @StephenMiller 正如我已经说过的:您的 gzip 使用可能不正确。使用Popen(["gzip"], stdout=gzip_output_file,...)。放弃touch 电话(没用)。
  • 好的,我现在知道了。我尝试了您的建议,但似乎生成的对象不像文件。当我这样做时: output_file.stdout.write(line) 我得到: AttributeError: 'NoneType' object has no attribute 'write'。
  • @StephenMiller 请仔细阅读我的回答:outfile_file = gzip_process.stdin(你写的是进程的标准输入,而不是标准输出)
  • 好吧,我试着关注你的最后一条评论——你在 Popen 调用中使用标准输出。无论如何,将其更改回标准输入不会将其更改为类似文件的对象。我犯了同样的错误。也许我误解了你?当我在 Popen 调用中使用 stdout 时 - 受您的评论启发 - 我显然也将 output.stdin.write(line) 更改为 output.stdout.write(line)。我担心的是只有在使用 PIPE 时才能创建类似文件的对象?
猜你喜欢
  • 2012-05-11
  • 2015-07-18
  • 2013-12-15
  • 1970-01-01
  • 2017-02-18
  • 2012-03-21
  • 2021-12-16
  • 2017-03-22
  • 2011-12-22
相关资源
最近更新 更多