3

我想从 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()而不是使用通信,但它被阻止并且在调用stdin之前不返回任何结果。proc.stdin.close()这又意味着我每次都需要创建一个新流程。

我尝试从下面类似的问题中实现队列和线程,但是它要么不返回任何内容,因此卡在上面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...)

谢谢。

4

3 回答 3

3

如果 mecab 使用FILE具有默认缓冲的 C 流,则管道标准输出具有 4 KiB 缓冲区。这里的想法是程序可以有效地使用对缓冲区的小、任意大小的读取和写入,并且底层标准 I/O 实现会自动填充和刷新更大的缓冲区。这最大限度地减少了所需系统调用的数量并最大限度地提高了吞吐量。显然,对于交互式控制台或终端 I/O 或写入stderr. 在这些情况下,C 运行时使用行缓冲或不使用缓冲。

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


尝试的一种选择是CreateConsoleScreenBuffermsvcrt.open_osfhandle. 然后将其作为stdout而不是使用管道传递。子进程会将其视为 TTY 并使用行缓冲而不是完全缓冲。然而,管理这一点并非易事。这将涉及读取(即)由另一个进程主动写入ReadConsoleOutputCharacter的滑动缓冲区(调用以跟踪光标位置)。GetConsoleScreenBufferInfo这种互动不是我曾经需要甚至尝试过的。但是我非交互地使用了控制台屏幕缓冲区,即在孩子退出后读取缓冲区。这允许从直接写入控制台的程序中读取多达 9,999 行输出,而不是stdout例如调用WriteConsole或打开“CON”或“CONOUT$”。

于 2017-03-24T07:30:19.443 回答
0

这是适用于 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 应用程序;因此,如果没有其他方法可以连接其输入和输出,则可能必须从源代码进行调整和重建(它是开源的)。我还没有找到任何其他方法;但这应该可行。

我在这里问了关于在某种控制台模式下运行的问题;所以你也可以检查那个线程。作者马克西姆斯在...

于 2017-07-20T21:26:44.233 回答
0

编码

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

本质上是一样的

print(q.get())

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

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

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

  2. 查找在每行输出之后刷新的二进制文件。我在网上找到的最新 Win32 版本 v0.98 在每一行之后都会刷新。做不到...

  3. 构建您自己的二进制文件,在每行之后刷新。找到主循环并在其中插入刷新调用应该很容易。但是MeCab 似乎已经明确地刷新了,并且 git blame 说刷新语句最后一次更改是在 2011 年,所以我很惊讶你曾经遇到过这个问题,我怀疑你的 Python 代码中可能只是有一个错误。做不到...

  4. 异步处理输出。如果您担心出于性能原因想要与标记化并行处理输出,您可以在第一个 4K 之后执行此操作。只需在第二个线程中进行处理,而不是将行填充到队列中。如果你不能这样做...

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

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

于 2020-06-17T21:53:01.147 回答