Подпроцесс, многократно записывать в STDIN во время чтения из 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() вместо использования общаться, но он заблокирован 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_recovered -> output -> waits_for_input...)

Благодарю.

4 ответа

Решение

Если Mecab использует C FILE Потоки с буферизацией по умолчанию, затем стандартный канал имеет 4-килобайтный буфер. Идея заключается в том, что программа может эффективно использовать небольшие операции чтения и записи произвольного размера в буферы, а базовая стандартная реализация ввода / вывода обрабатывает автоматическое заполнение и очистку гораздо больших буферов. Это минимизирует количество необходимых системных вызовов и максимизирует пропускную способность. Очевидно, вам не нужно такое поведение для интерактивной консоли или терминала ввода-вывода или записи в stderr, В этих случаях среда выполнения C использует буферизацию строки или не использует буферизацию.

Программа может переопределить это поведение, а некоторые имеют параметры командной строки для установки размера буфера. Например, в Python есть опция -u (небуферизованная) и PYTHONUNBUFFERED переменная окружения. Если у mecab нет подобной опции, то в Windows нет общего обходного пути. Ситуация во время выполнения C слишком сложна. Процесс Windows может статически или динамически связываться с одним или несколькими ЭЛТ. Ситуация в 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 отслеживать положение курсора), который активно записывается другим процессом. Такое взаимодействие не является тем, что мне когда-либо требовалось или даже экспериментировало. Но я использовал экранный буфер консоли неинтерактивно, т.е. читал буфер после выхода ребенка. Это позволяет читать до 9999 строк выходных данных из программ, которые пишут непосредственно на консоль вместо stdoutнапример, программы, которые вызывают WriteConsole или откройте "CON" или "CONOUT$".

Думаю, ответ, если не решение, можно найти здесь https://github.com/ikriv/ConsoleProxy/blob/master/src/Tools/Exec/readme.md

Я думаю, потому что у меня была аналогичная проблема, которую я обходил, и я не мог попробовать этот маршрут, потому что этот инструмент недоступен для Windows 2003, которую мне пришлось использовать (в виртуальной машине для устаревшего приложения).

Я хотел бы знать, правильно ли я угадал.

Код

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

по сути то же самое, что

print(q.get())

за исключением менее эффективного, поскольку он сжигает процессорное время во время ожидания. В явном цикле данные из подпроцесса не будут поступать раньше; он прибывает, когда прибывает.

Для работы с несовместимыми двоичными файлами у меня есть несколько предложений, от лучших до худших:

  1. Найдите библиотеку Python и используйте ее. Похоже, что в дереве исходных текстов MeCab есть официальная привязка Python, и я вижу несколько готовых пакетов в PyPI. Вы также можете найти сборку DLL, которую можно вызвать с помощьюctypesили другой Python FFI. Если это не сработает...

  2. Найдите двоичный файл, который сбрасывается после каждой строки вывода. Самая последняя сборка Win32, которую я нашел в сети, v0.98, сбрасывается после каждой строки. Если это не удастся...

  3. Создайте свой собственный двоичный файл, который сбрасывается после каждой строки. Найти основной цикл и вставить в него вызов сброса должно быть достаточно легко. Но MeCab, похоже, уже явно выполняет сброс, а git blame говорит, что последний раз оператор сброса изменялся в 2011 году, поэтому я удивлен, что у вас когда-либо была эта проблема, и подозреваю, что в вашем коде Python только что была ошибка. Если это не удастся...

  4. Асинхронная обработка вывода. Если вас беспокоит, что вы хотите обрабатывать вывод параллельно с токенизацией по соображениям производительности, вы можете сделать это в большинстве случаев после первого 4K. Просто выполняйте обработку во втором потоке вместо того, чтобы вставлять строки в очередь. Если ты не можешь этого сделать...

  5. Это ужасный прием, но он может сработать в некоторых случаях: перемежайте ваши входные данные с фиктивными входами, которые производят не менее 4K вывода. Например, вы можете вывести 2047 пустых строк после каждой реальной строки ввода (2047 CRLF плюс CRLF из реального вывода = 4K) или одну строкуb'A' * 4092 + b'\r\n', в зависимости от того, что быстрее.

В этом списке вообще нет подхода, предложенного двумя предыдущими ответами: направление вывода на консоль Win32 и очистка консоли. Это ужасная идея, потому что при парсинге вы получаете вывод в виде прямоугольного массива символов. У скребка нет способа узнать, были ли две строки изначально одной слишком длинной, которая была перенесена. Если он угадает неверно, ваши выходы не будут синхронизироваться с вашими входами. Невозможно обойти буферизацию вывода таким образом, если вы вообще заботитесь о целостности вывода.

Вот обходной путь для 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 является графическим приложением; поэтому, если нет другого способа подключиться к его вводу и выводу, возможно, придется настроить и перестроить из источников (это с открытым исходным кодом). Я не нашел другого пути; но это должно работать.

Я задал вопрос о работе в каком-то режиме консоли здесь; так что вы можете проверить эту ветку также на что-то. Автор Максимус на ТАК...

Другие вопросы по тегам