子进程,重复从标准输出读取并写入标准输入(Windows)

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上似乎没有任何我尝试过的方法可以工作。

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

(...等待输入 -> 输入接收 -> 输出 -> 等待输入...)

谢谢。


1
既然你只是在“分词”模式下运行MeCab,那么你是否可以将输入的所有行(包括换行符)直接传输到该进程的标准输入中? - Ahmed Fasih
@AhmedFasih 我可以这样做,但输入是用户数据库中的评论、帖子等,因此所有输入加在一起会形成一个非常大的文件,并且可能呈指数级增长,很快就会超过内存。如果可能的话,我更愿意按顺序处理,因为这也有益于我的代码逻辑(对每个用户进行标记化处理->处理用户->等等...)。 - umutto
1
如果mecab使用默认缓冲的C FILE流,则管道stdout具有4 KiB缓冲区。您是否尝试过重复写入输入,直到mecab填充并刷新其stdout缓冲区?mecab是否有命令行选项来强制使用无缓冲或行缓冲而不是完全缓冲? - Eryk Sun
1
在Windows上,没有通用的方法可以修改FILE流使用的输出缓冲区大小。C运行时情况太复杂了。一个进程可以静态或动态地链接到一个或多个CRT。Linux上的情况不同,因此有像stdbuf这样的命令可以尝试修改标准FILE流的缓冲。 - Eryk Sun
1
就此而言,Windows的道义认为正确的解决方案是将外部进程重建为DLL。当然,这并不总是切实可行的。 - Harry Johnston
显示剩余3条评论
4个回答

3
如果 mecab 使用带有默认缓冲的 C FILE 流,则管道 stdout 具有 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 库配置 C FILE 流。Linux 的 stdbuf 使用了这个技巧,例如 stdbuf -o0 mecab -O wakati


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


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应用程序,所以如果没有其他方法来钩入它的输入和输出,那么可能需要从源代码进行调整和重建(它是开源的)。我还没有找到其他的方法,但这应该可以解决问题。

我已经在这里问过关于运行在某种控制台模式中的问题,所以你也可以查看那个线程。作者Maximus在SO上...


不会有任何影响。重要的是输出到控制台的内容会被不同对待;命令提示符实例是否存在都没有影响。另外,分号是怎么回事? - Harry Johnston
我的想法是你不应该直接运行mecab,而是运行cmd.exe,然后将运行mecab的命令发送给它(在运行mecab后退出)。这样就像手动启动cmd.exe并输入命令一样。或者当像那样运行时,输出缓冲区问题会导致问题吗? - Seyi Shoboyejo
我是指,如果一个程序可以将某些内容打印到屏幕上,那么它也可以将其写入文件中。当然,如果该程序被设计为标准输出可能会被重定向到文件或管道,则没有问题,只需要关闭缓冲即可如此处所述。只有当子程序不是为以这种方式使用而设计时,才会遇到麻烦。 - Harry Johnston
是的,你确实向我展示了一些我不清楚的东西:cmd.exe只是另一个控制台应用程序,那个黑屏幕并不属于它。尽管如此,有些事情应该像那样良好地工作,应该有一个包装器。这肯定会效率低下,但肯定仍然非常有益。Eryksun建议的这种代码对于需要解决其他问题的开发人员来说太低级了。 - Seyi Shoboyejo
许多控制台应用程序的开发者可能不知道如何关闭缓冲区,直到他们发现自己处于另一端。这并不需要“终端”开发人员提供复杂的解决方案... - Seyi Shoboyejo
显示剩余7条评论

0

我猜答案,如果不是解决方案,可以在这里找到 https://github.com/ikriv/ConsoleProxy/blob/master/src/Tools/Exec/readme.md

我猜是因为我遇到了类似的问题,我绕过了它,无法尝试这条路线,因为这个工具不适用于Windows 2003,这是我必须使用的操作系统(在VM中运行遗留应用程序)。

我想知道我是否猜对了。


0

代码

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

本质上与

print(q.get())

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

对于处理不合作的二进制文件,我有几个建议,从最好到最差:

  1. 找一个Python库并使用它。看起来在MeCab源代码树中有官方的Python绑定,我在PyPI上也看到了一些预构建的包。你还可以寻找一个DLL构建,然后用ctypes或其他Python FFI调用它。如果这样不行...

  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控制台并抓取控制台。这是一个可怕的想法,因为抓取会得到矩形字符数组形式的处理过的输出。抓取程序无法知道两行是否最初是一个过长的换行行。如果它猜错了,你的输出将与输入不同步。如果您关心输出的完整性,以这种方式解决输出缓冲区问题是不可能的。


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