Подпроцесс, повторяющаяся запись в 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.

РЕДАКТИРОВАТЬ: некоторая информация о двоичном файле, который я использую, когда я использую его через командную строку. Он запускает и маркирует предложения, которые я даю, пока я не закончу и принудительно не закрою программу.

(...ожидание_ввода -> получение_ввода -> вывод -> ожидание_ввода...)

Спасибо.


person umutto    schedule 24.03.2017    source источник
comment
Поскольку вы просто запускаете MeCab в режиме wakati, не можете ли вы просто передать все строки вашего ввода (переводы строк и все) в стандартный ввод процесса?   -  person Ahmed Fasih    schedule 24.03.2017
comment
@AhmedFasih Я могу, но входные данные — это комментарии, сообщения и т. Д. В пользовательской базе данных, поэтому все входные данные вместе представляют собой очень большой файл и могут расти в геометрической прогрессии до такой степени, что вскоре он может стать больше, чем память. Я бы предпочел делать это последовательно, если могу, так как это также приносит пользу моей общей логике кода (выполнение токенизации для каждого пользователя -> обработка пользователя -> и т. д.).   -  person umutto    schedule 24.03.2017
comment
Если mecab использует потоки C FILE с буферизацией по умолчанию, то piped stdout имеет буфер размером 4 КиБ. Пробовали ли вы повторно вводить данные, пока mecab не заполнит и не очистит свой буфер stdout? Есть ли в mecab параметр командной строки для принудительного использования без буферизации или буферизации строки вместо полной буферизации?   -  person Eryk Sun    schedule 24.03.2017
comment
@eryksun Проверяя документацию, в ней указан размер входного буфера (8 КБ). Но нет размера выходного буфера. Я попытался заполнить свой запрос stdin.write 8 КБ пустого пространства, что сработало (ура), но это кажется хакерским. Могу ли я заставить его сбросить буфер другим способом? Когда я использую его в командной строке, он правильно токенизирует мои входы, не закрывая процесс.   -  person umutto    schedule 24.03.2017
comment
В Windows нет универсального способа изменить размер выходного буфера, используемого FILE потоками. Ситуация со временем выполнения C слишком сложна. Процесс может быть статически или динамически связан с одним или несколькими CRT. В Linux ситуация иная, поэтому есть такие команды, как stdbuf, которые могут попытаться изменить буферизацию стандартных FILE потоков.   -  person Eryk Sun    schedule 24.03.2017
comment
@eryksun Спасибо за ответы, я пока пропущу это так. Можете ли вы опубликовать свои комментарии в качестве ответа, чтобы я мог принять его.   -  person umutto    schedule 24.03.2017
comment
FWIW, Дао Windows говорит, что правильное решение - перестроить внешний процесс как DLL. Конечно, это не всегда практично.   -  person Harry Johnston    schedule 24.03.2017
comment
@HarryJohnston спасибо! это на самом деле выглядит многообещающе, я создал dll и попытался импортировать ее с помощью ctypes, но потерпел неудачу с возвращаемыми типами, потому что я не очень хорошо знаком с C. Я поработаю над этим еще немного.   -  person umutto    schedule 27.03.2017


Ответы (3)


Если mecab использует потоки C FILE с буферизацией по умолчанию, то конвейерный стандартный вывод имеет буфер размером 4 КиБ. Идея здесь заключается в том, что программа может эффективно использовать небольшие операции чтения и записи произвольного размера в буферы, а лежащая в основе стандартная реализация ввода-вывода обрабатывает автоматическое заполнение и очистку гораздо больших буферов. Это сводит к минимуму количество необходимых системных вызовов и максимизирует пропускную способность. Очевидно, вам не нужно такое поведение для ввода/вывода интерактивной консоли или терминала или записи в 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 для отслеживания позиции курсора), который активно записывается другим процессом. Я никогда не нуждался в такого рода взаимодействии и даже не экспериментировал с ним. Но я использовал буфер экрана консоли не в интерактивном режиме, т.е. читая буфер после выхода ребенка. Это позволяет считывать до 9999 строк вывода из программ, которые пишут непосредственно в консоль вместо stdout, например. программы, которые вызывают WriteConsole или открывают "CON" или "CONOUT$".

person Eryk Sun    schedule 24.03.2017

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

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

person Seyi Shoboyejo    schedule 20.07.2017
comment
Не будет никакой разницы. Вывод, поступающий на консоль, обрабатывается по-разному; независимо от того, присутствует ли экземпляр командной строки, не имеет значения. Кроме того, что с точкой с запятой? - person Harry Johnston; 21.07.2017
comment
Я полагаю, что вы не должны иметь ничего общего с запуском mecab напрямую; но вместо этого запустите cmd.exe, а затем просто отправьте ему команду для запуска mecab (выход после запуска mecab). Таким образом, это должно быть похоже на ручной запуск cmd.exe и ввод команды. Или проблема с выходным буфером вызывает проблемы при таком запуске? - person Seyi Shoboyejo; 22.07.2017
comment
Тогда есть подход грубой силы, чтобы просто запустить cmd.exe (не как подпроцесс); отправить на него нажатия клавиш, а затем выполнить команду запуска mecab, чтобы перенаправить вывод в файл (команда ›out.txt); и получите свой токенизированный вывод оттуда. Можно ли вообще не запускать mecab из командной строки?? - person Seyi Shoboyejo; 22.07.2017
comment
Проблема возникает всякий раз, когда вывод перенаправляется. Неважно, перенаправляется ли вывод Python или командным процессором, т. е. когда вы говорите >out.txt - все равно, что касается дочерней программы. Если вы не перенаправляете выходные данные, как это обычно бывает, когда программа запускается вручную, проблем нет, за исключением того, что в этом сценарии родительскому процессу трудно увидеть, какие выходные данные . Ответ Эриксуна более подробно описан. - person Harry Johnston; 22.07.2017
comment
Хорошо, я понял тебя. Но я бы подумал, что сложный процесс использования экранного буфера консоли для связи с дочерним элементом будет обрабатываться такой важной программой, как cmd.exe. Он будет использовать это, чтобы получить вывод от дочернего элемента, а затем записать в указанный вами выходной файл. Конечно, Microsoft достаточно велика, чтобы написать весь этот код за час. Здесь нет необходимости перенаправлять вывод с помощью канала. Если это реализация cmd.exe, как насчет powershell? Я имею в виду, что если программа может вывести что-то на экран, она также может записать это в файл. Возможно ли, что они не могут этого сделать; Зачем?? - person Seyi Shoboyejo; 22.07.2017
comment
Или дочерний процесс печатает прямо на экран cmd.exe без какого-либо вмешательства со стороны cmd.exe?? - person Seyi Shoboyejo; 22.07.2017
comment
Если вывод не был перенаправлен, дочерний процесс записывает непосредственно в окно консоли. Если выходные данные были перенаправлены, дочерний процесс записывает непосредственно в файл или канал. Интерпретатор команд (cmd.exe) никогда не считывает содержимое из экранного буфера консоли, а экранный буфер консоли никогда не предназначался для использования в IPC! (Имейте в виду, что проблема возникает только тогда, когда вы пытаетесь запустить программу, предназначенную для интерактивного использования, не в интерактивном режиме, т. е. когда вы используете ее способом, для которого она не предназначена. Это не должно удивлять. что это сложно) - person Harry Johnston; 23.07.2017
comment
Да, вы действительно показали мне кое-что, чего я не понял: cmd.exe — это просто еще одно консольное приложение, и этот черный экран ему не принадлежит. Все таки какие-то вещи должны просто так хорошо работать и где-то должна была быть обертка. Это, конечно, будет менее эффективно, но, безусловно, будет очень полезно. Этот тип кода, который предложил Эриксун, слишком низкоуровневый, чтобы требовать от разработчиков решения других проблем. - person Seyi Shoboyejo; 23.07.2017
comment
Многие производители консольных приложений могут не знать, как отключить буферизацию; по крайней мере, пока они не окажутся на другой стороне. Это не требует сложных решений от «конечных» разработчиков... - person Seyi Shoboyejo; 23.07.2017
comment
Только очень небольшая часть пользователей Windows когда-либо хотела это сделать, поэтому я думаю, что это случай Минус 100 баллов. В Windows правильным решением (когда вы ожидаете, что ваша программа будет использоваться другой программой) является предоставление ее в виде DLL или, возможно, COM-объекта. В любом случае, учитывая, что мы установили, что ваш ответ неверен, рекомендую вам удалить его до того, как он начинает привлекать минусы. (Без обид, просто так работает Stack Overflow.) - person Harry Johnston; 24.07.2017
comment
Может ли эмулятор консоли, такой как conemu, не делать того, что я ожидал от cmd.exe? Я действительно многому научился из всего этого. Спасибо! - person Seyi Shoboyejo; 24.07.2017

Код

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 fault говорит, что последний раз оператор flush был изменен в 2011 году, поэтому я удивлен, что у вас когда-либо возникала эта проблема, и я подозреваю, что в вашем коде Python могла быть ошибка. В противном случае...

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

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

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

person benrg    schedule 17.06.2020