Небуферизованное чтение со стандартного ввода в python

Я пишу сценарий Python, который может читать ввод через канал из другой команды, например, так

batch_job | myparser

Мой сценарий myparser обрабатывает вывод batch_job и написать в свой стандартный вывод. Моя проблема в том, что я хочу увидеть вывод немедленно (вывод batch_job обрабатывается построчно), но, похоже, существует эта пресловутая буферизация stdin (якобы 4KB, я не проверял), которая задерживает все.

Проблема уже обсуждалась здесь и здесь.

Я попробовал следующее:

  • открыть стандартный ввод os.fdopen(sys.stdin.fileno(), 'r', 0)
  • с помощью -u в моем hashbang: #!/usr/bin/python -u
  • установка export PYTHONUNBUFFERED=1 прямо перед вызовом скрипта
  • сбрасывать вывод после каждой прочитанной строки (на случай, если проблема связана с буферизацией вывода, а не с буферизацией ввода)

Моя версия Python 2.4.3 - у меня нет возможности обновлять или устанавливать какие-либо дополнительные программы или пакеты. Как я могу избавиться от этих задержек?

1 ответ

Я столкнулся с той же проблемой с устаревшим кодом. Похоже, это проблема с реализацией Python 2 file объекты __next__метод; он использует буфер уровня Python (который -u/PYTHONUNBUFFERED=1 не влияет, потому что они только снимают буферизацию stdio FILE*сами, но file.__next__буферизация не связана; так же, stdbuf/unbufferвообще не может изменить какую-либо буферизацию, потому что Python заменяет буфер по умолчанию, созданный средой выполнения C; Последнее дело file.__init__ делает для вновь открытого файла вызов PyFile_SetBufSize который использует setvbuf/setbuf [API] для замены по умолчанию stdio буфер).

Проблема видна, когда у вас есть цикл формы:

for line in sys.stdin:

где первый звонок __next__ (вызывается неявно for цикл, чтобы получить каждый line) заканчивается блокировкой, чтобы заполнить блок перед созданием одной строки.

Есть три возможных исправления:

  1. (Только на Python 2.6+) sys.stdio с io модуль (перенесенный из Python 3 как встроенный) для обхода file полностью в пользу (откровенно превосходного) дизайна Python 3 (который использует один системный вызов за раз для заполнения буфера без блокировки для выполнения полного запрошенного чтения; если он запрашивает 4096 байтов и получает 3, он увидит если строка доступна и произведите ее, если так) так:

    import io
    import sys
    
    # Add buffering=0 argument if you won't always consume stdin completely, so you 
    # can't lose data in the wrapper's buffer. It'll be slower with buffering=0 though.
    with io.open(sys.stdin.fileno(), 'rb', closefd=False) as stdin:
        for line in stdin:
            # Do stuff with the line
    

    Обычно это быстрее, чем вариант 2, но он более подробный и требует Python 2.6+. Это также позволяет использовать Unicode для перенастройки, изменив режим на 'r' и при желании передать известные encoding ввода (если это не локаль по умолчанию), чтобы легко получить unicode строки вместо (только ASCII) str.

  2. (Любая версия Python) Решение проблем с file.__next__ используя file.readlineвместо; несмотря на почти идентичное предполагаемое поведение, readline не выполняет свою собственную (избыточную) буферизацию, он делегирует C stdioс fgets (настройки сборки по умолчанию) или ручной вызов цикла getc/getc_unlockedв буфер, который останавливается именно тогда, когда достигает конца строки. Объединив его с двумя аргументами iter вы можете получить почти идентичный код без лишней многословности (возможно, это будет медленнее, чем предыдущее решение, в зависимости от того, fgets используется под капотом, и как среда выполнения C реализует его):

    # '' is the sentinel that ends the loop; readline returns '' at EOF
    for line in iter(sys.stdin.readline, ''):
        # Do stuff with line
    
  3. Перейдите на Python 3, в котором этой проблемы нет.:-)

В Linux bash, то, что вы ищете, кажется командой stdbuf.

Если вы не хотите буферизации (то есть небуферизованного потока), попробуйте это,

# batch_job | stdbuf -o0 myparser

Если вы хотите буферизацию строки, попробуйте это,

# batch_job | stdbuf -oL myparser

Вы можете разблокировать вывод:

unbuffer batch_job | myparser
Другие вопросы по тегам