子进程,从 STDOUT 读取时重复写入 STDIN (Windows)

Subprocess, repeatedly write to STDIN while reading from STDOUT (Windows)

我想从 python 调用外部进程。我正在调用的进程读取输入字符串并给出标记化结果,然后等待另一个输入(如果有帮助,二进制是 MeCab 标记器)。

我需要通过调用此过程来标记数千行字符串。

问题是 Popen.communicate() 有效,但在给出 STDOUT 结果之前等待进程结束。我不想继续关闭和打开新的子流程数千次。 (而且我不想发送整个文本,它可能很容易在未来增长到数万行。)

from subprocess import PIPE, Popen

with Popen("mecab -O wakati".split(), stdin=PIPE,
           stdout=PIPE, stderr=PIPE, close_fds=False,
           universal_newlines=True, bufsize=1) as proc:
    output, errors = proc.communicate("foobarbaz")

print(output)

我试过阅读 proc.stdout.read() instead of using communicate but it is blocked by stdin and doesn't return any results before proc.stdin.close() 被调用。这又意味着我每次都需要创建一个新进程。

我已经尝试通过如下类似的问题实现队列和线程,但它要么没有 return 任何东西,所以它卡在 While True 上,或者当我强制 stdin 缓冲区到通过重复发送字符串进行填充,一次输出所有结果。

from subprocess import PIPE, Popen
from threading import Thread
from queue import Queue, Empty

def enqueue_output(out, queue):
    for line in iter(out.readline, b''):
        queue.put(line)
    out.close()

p = Popen('mecab -O wakati'.split(), stdout=PIPE, stdin=PIPE,
          universal_newlines=True, bufsize=1, close_fds=False)
q = Queue()
t = Thread(target=enqueue_output, args=(p.stdout, q))
t.daemon = True
t.start()

p.stdin.write("foobarbaz")
while True:
    try:
        line = q.get_nowait()
    except Empty:
        pass
    else:
        print(line)
        break

也查看了 Pexpect 路由,但它的 windows 端口不支持一些重要的模块(基于 pty 的模块),所以我也无法应用它。

我知道有很多类似的答案,我已经尝试了其中的大部分。但我尝试过的任何东西似乎都无法在 Windows.

上工作

编辑:关于我正在使用的二进制文件的一些信息,当我通过命令行使用它时。它运行并标记我给出的句子,直到我完成并强行关闭程序。

(...waits_for_input -> input_recieved -> 输出 -> waits_for_input...)

谢谢。

如果 mecab 使用带有默认缓冲的 C FILE 流,则管道标准输出有 4 KiB 缓冲区。这里的想法是一个程序可以有效地使用小的,arbitrary-sized 读取和写入缓冲区,并且底层标准 I/O 实现处理自动填充和刷新 much-larger 缓冲区。这最大限度地减少了所需的系统调用次数并最大限度地提高了吞吐量。显然,您不希望交互式控制台或终端 I/O 或写入 stderr 时出现这种行为。在这些情况下,C 运行时使用 line-buffering 或不使用缓冲。

程序可以覆盖此行为,有些程序确实有 command-line 选项来设置缓冲区大小。例如,Python 具有“-u”(无缓冲)选项和 PYTHONUNBUFFERED 环境变量。如果 mecab 没有类似的选项,那么 Windows 上就没有通用的解决方法。 C运行时的情况太复杂了。一个 Windows 进程可以 link 静态或动态地连接到一个或多个 CRT。 Linux 上的情况不同,因为 Linux 进程通常将单个系统 CRT(例如 GNU libc.so.6)加载到全局符号 table 中,这允许 LD_PRELOAD 库来配置 C FILE 流。 Linux stdbuf 使用这个技巧,例如stdbuf -o0 mecab -O wakati.


一个试验选项是调用 CreateConsoleScreenBuffer 并从 msvcrt.open_osfhandle 获取句柄的文件描述符。然后将其作为 stdout 而不是使用管道传递。 child 进程会将其视为 TTY 并使用行缓冲而不是全缓冲。然而,管理这个是 non-trivial。这将涉及读取(即 ReadConsoleOutputCharacter)一个由另一个进程主动写入的滑动缓冲区(调用 GetConsoleScreenBufferInfo 以跟踪光标位置)。这种互动不是我曾经需要甚至尝试过的。但我使用了控制台屏幕缓冲区 non-interactively,即在 child 退出后读取缓冲区。这允许从直接写入控制台而不是 stdout 的程序读取多达 9,999 行输出,例如调用 WriteConsole 或打开 "CON" 或 "CONOUT$".

的程序

这是 Windows 的解决方法。这也应该适用于其他操作系统。 下载像 ConEmu 这样的控制台模拟器 (https://conemu.github.io/) 启动它而不是 mecab 作为您的子进程。

p = Popen(['conemu'] , stdout=PIPE, stdin=PIPE,
      universal_newlines=True, bufsize=1, close_fds=False)

然后发送以下内容作为第一个输入:

mecab -O wakafi & exit

您正在让模拟器为您处理文件输出问题;当您手动与它交互时它通常的方式。 我还在研究这个;但看起来很有希望...

唯一的问题是conemu是一个gui应用程序;因此,如果没有其他方法可以连接到它的输入和输出,则可能需要从源代码(它是开源的)进行调整和重建。我还没有找到其他方法;但这应该有效。

我在某种控制台模式下问过关于 运行 的问题 here;所以你也可以检查那个线程的东西。作者 Maximus 在 SO...

密码

while True:
    try:
        line = q.get_nowait()
    except Empty:
        pass
    else:
        print(line)
        break

基本相同
print(q.get())

除了效率较低,因为它会在等待时消耗 CPU 时间。显式循环不会使来自子进程的数据更快到达;该到就到。

对于处理不合作的二进制文件,我有一些建议,从好到坏:

  1. 找到一个 Python 库并改用它。 MeCab 源代码树中似乎有 an official Python binding,我在 PyPI 上看到了一些预构建的包。您还可以查找可以使用 ctypes 或另一个 Python FFI 调用的 DLL 构建。如果这不起作用...

  2. 找到一个在每行输出后刷新的二进制文件。我在网上找到的最新 Win32 版本 v0.98 在每行之后刷新。否则...

  3. 构建您自己的二进制文件,在每行之后刷新。找到主循环并在其中插入刷新调用应该很容易。但是 MeCab seems to explicitly flush already 和 git blame 说 flush 语句最后一次更改是在 2011 年,所以我很惊讶你遇到过这个问题,我怀疑你的 Python代码。否则...

  4. 异步处理输出。如果您担心出于性能原因,您希望在处理标记化的同时并行处理输出,那么在第一个 4K 之后,您通常可以这样做。只需在第二个线程中进行处理,而不是将行填充到队列中。如果你做不到...

  5. 这是一个糟糕的 hack,但它在某些情况下可能会起作用:在您的输入中穿插产生至少 4K 输出的虚拟输入。例如,您可以在每个实际输入行之后输出 2047 个空行(2047 个 CRLF 加上实际输出的 CRLF = 4K),或者单行 b'A' * 4092 + b'\r\n',以更快的为准。

前两个答案建议的方法根本不在此列表中:将输出定向到 Win32 控制台并抓取控制台。这是一个糟糕的主意,因为抓取会让你输出一个矩形字符数组。刮板无法知道两条线是否原本是一条缠绕的超长线。如果它猜错了,您的输出将与您的输入不同步。如果您非常关心输出的完整性,则不可能以这种方式解决输出缓冲问题。