Python中从标准输入流进行无缓冲读取

14

我正在编写一个Python脚本,可以通过管道从另一个命令中读取输入,如下所示

batch_job | myparser

我的脚本 myparser 处理 batch_job 的输出并将其写入自己的标准输出。我的问题是,我想立即看到输出(逐行处理 batch_job 的输出),但似乎存在这个臭名昭著的 stdin 缓冲区(据称为 4KB,我没有验证),导致所有东西都被延迟。

这个问题已经在这里这里这里讨论过了。

我尝试了以下方法:

  • 使用 os.fdopen(sys.stdin.fileno(), 'r', 0) 打开标准输入
  • 在 hashbang 中使用 -u#!/usr/bin/python -u
  • 在调用脚本之前设置 export PYTHONUNBUFFERED=1
  • 在读取每行的输出后刷新我的 输出(以防输出缓冲区而不是输入缓冲区引起问题)

我的 Python 版本是 2.4.3 - 我无法升级或安装任何其他程序或软件包。如何消除这些延迟?


2
你确定缓冲是在Python的stdin上而不是批处理作业的stdout上发生吗?有时应用程序会检查stdout的设备类型,并根据其进行缓冲,因此仅仅因为在向终端写入时可能出现行缓冲并不意味着当管道传输到另一个进程时会做同样的事情。 - Tom Karzes
这是一个有趣的建议。我会尝试验证。我能说的是,该应用程序本身是一个shell脚本。 - Glemi
它还创建了一个日志文件,其内容与通常写入终端的内容相同。我观察到这个日志文件更新得更快,也就是说,它已经包含了我的脚本仍在等待的行。 - Glemi
1
为什么不直接在你的 myparser 中作为子进程启动 batch_job,这样你就完全控制 STDOUT / STDIN 了呢?你现有的设置不仅依赖于 Python,还依赖于 shell 缓冲本身。 - zwer
显示剩余3条评论
3个回答

3

我在遇到旧代码时也遇到了同样的问题。这似乎是 Python 2 的 file 对象的 __next__ 方法实现的问题;它使用了一个 Python 级别的缓冲区(-u/PYTHONUNBUFFERED=1 并不影响它,因为它们只是取消了 stdioFILE* 自身的缓冲,但是 file.__next__ 的缓冲与此无关;同样,stdbuf/unbuffer 不能改变任何缓冲,因为 Python 替换了 C 运行时制作的默认缓冲区;对于新打开的文件,file.__init__ 最后一件事就是调用 PyFile_SetBufSize,它使用 setvbuf/setbuf [API] 来替换默认的 stdio 缓冲区)。

当你有一个如下形式的循环时,就会出现问题:

for line in sys.stdin:

在编程中,第一次调用__next__(由for循环隐式调用以获取每个line)会阻塞以填充块,然后才能生成单个行。

有三种可能的解决方法:

(仅适用于Python 2.6+)使用io模块(从Python 3中作为内置模块回溯)重新包装sys.stdin,以完全绕过file,而采用(实际上更优秀的)Python 3设计(每次使用单个系统调用填充缓冲区,而不会阻塞整个请求读取的时间;如果请求4096字节并获得3个字节,则它将查看是否有可用行并在有时生成它),因此:
import io import sys
# 如果您不总是完全消耗stdin,则添加buffering = 0参数,以便您无法在包装器的缓冲区中丢失数据。但是,使用buffering = 0会更慢。 with io.open(sys.stdin.fileno(), 'rb', closefd=False) as stdin: for line in stdin: # 处理该行
这通常比选项2更快,但更冗长,并需要Python 2.6+。通过将模式更改为'r'并可选地传递输入的已知编码(如果它不是区域设置默认值),可以使重新包装对Unicode友好,以无缝获取unicode行而不是(仅限ASCII)str。
(任何版本的Python)通过使用file.readline解决file.__next__的问题;尽管意图相似,但readline不会执行自己的(过度)缓冲,它将委托给C stdio的fgets(默认构建设置)或手动循环调用getc / getc_unlocked到一个缓冲区,该缓冲区在精确命中行尾时停止。通过将其与两个参数的iter结合使用,您可以获得几乎相同的代码而不会有过多的冗余(它可能比先前的解决方案慢,这取决于fgets是否在后台使用以及C运行时如何实现它):
# ''是结束循环的标记;readline在EOF处返回'' for line in iter(sys.stdin.readline, ''): # 处理该行
迁移到Python 3,它没有这个问题。 :-)

注意:显然,如果batch_job有缓冲输出,您需要取消缓冲或确保它进行手动刷新,以便Python程序可以看到任何内容。但我确实见过先前的进程明确未缓冲,并且Python 2的for line in sys.stdin:负责缓冲(非Python 2程序替换到管道中,使用原始I/O或纯C stdio,不会出现此问题)。 - ShadowRanger

0
在Linux的bash中,您要查找的似乎是stdbuf命令。
如果您想要无缓冲(即无缓冲流),请尝试以下操作,
# batch_job | stdbuf -o0 myparser

如果您想要进行行缓冲,请尝试这个:
# batch_job | stdbuf -oL myparser

这并没有帮助。问题不在于 Python 的输出缓冲(如果是的话,使用 -u 标志或在调用脚本之前执行 export PYTHONUNBUFFERED=1 将修复它;stdbuf [命令行工具] 无法处理使用 setvbuf/setbuf [API] 修改默认 stdio 缓冲的程序,而 Python 可以并且确实这样做),而是 Python 缓冲 输入。并且输入缓冲是在 Python 用户模式缓冲区中完成的,stdbuf(命令行工具)无法影响它。 - ShadowRanger
@ShadowRanger 嗯,它确实有效。我通过在两个Python程序之间传递数据进行了测试,使用和不使用stdbu -o0,差异非常明显。所以,这就是事实。你基于猜测而不尝试就投反对票是不公平的。 - DrM
在某些情况下可能有效,但在不使用-u/PYTHONUNBUFFERED=1的情况下,无法在Python 2.x中运行。您可能会被一个与OP不同的测试用例所迷惑(例如,在您的情况下,您的输入管道也是Python;而OP只有输出管道)。以下是一个简单的bash一行代码示例,它不起作用:(for ((i = 0; i < 10; ++i)); do echo $i && sleep 1; done) | stdbuf -o0 python2 -c 'for line in __import__("sys").stdin: print line,'; 您将在10秒内得不到任何输出。原因是file.__next__中的缓冲,而stdbuf对其没有影响。 - ShadowRanger
__import__("sys").stdin替换为iter(__import__("sys").stdin.readline, ""),您将每秒获得一个输出。如果您能向我展示一个单一的例子,在该例子中,管道右侧的stdbuf解决了OP尝试过的各种问题,我将很高兴地将我的反对票转换为赞成票。但我不认为这样的情况存在(正如stdbuf的手册所指出的那样:“如果COMMAND调整其标准流的缓冲区(例如'tee'),那么它将覆盖'stdbuf'更改的相应设置。”;Python 2就是这样做的)。 - ShadowRanger
@ShadowRanger 输入来自C程序,是Python2。这是一个部署的成像系统,从C中的MIMO阵列到Python中的图像显示和人工智能。 - DrM

0

你可以取消缓冲输出:

unbuffer batch_job | myparser

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接