ГлавнаяБлогПочему exit code 0 — это ложь: как отличить пустое извлечение текста от успеха
Python

Почему exit code 0 — это ложь: как отличить пустое извлечение текста от успеха

Узнайте, почему exit code 0 не гарантирует успех извлечения текста из PDF. Научитесь измерять плотность текста и избегать ложных срабатываний. Читайте статью и применяйте на практике!

Al
Редакция Algolitalgolit.ru
8 мин чтения20 августа 2026 г.

Почему exit code 0 не означает успех: ловушка пустого извлечения

Вы когда-нибудь получали от инструмента конвертации PDF нулевой результат, но при этом exit code 0? Это классическая ловушка: процесс завершился успешно, но данных нет. В этой статье разберем, почему нельзя доверять коду возврата, как измерять реальный выход и какие пороги использовать, чтобы отличить пустой PDF от настоящего документа.

Реальный случай: пустой PDF, который не был ошибкой

Я попросил Claude Code извлечь даты из PDF, сохраненного с веб-страницы. Инструмент ответил: «Документ пуст — в нем нет извлекаемого текста». Странно, ведь PDF весил 4,5 МБ и был заполнен текстом. Интересно не то, что ответ был неверным, а то, что ни один компонент не сообщил об ошибке. Каждый шаг отработал штатно, и сумма этих успехов дала ложь, которой я поверил.

Воспроизведение проблемы

Я использовал markitdown — конвертер Microsoft. Запустил вручную:

markitdown screenshot.pdf -o out.md
echo $?
# 0
wc -c out.md
# 0 out.md

Exit code 0, нулевой файл, ни предупреждений, ни stderr. Моя интеграция проверила код возврата, увидела успех, закэшировала результат и передала модели путь к пустому файлу. Модель прочитала файл, ничего не нашла и сообщила, что документ пуст. С её позиции это было разумно: ей дали пустой файл и сказали, что конвертация прошла успешно.

Почему это не баг markitdown

PDF был скриншотом страницы, экспортированным в PDF. Он содержит растровые изображения и не имеет текстового слоя. pdfminer и PyMuPDF сообщают 0 символов. markitdown извлекает только встроенный текст и не делает OCR — это документированное решение. Находить ноль — не ошибка. Конвертер, который падал бы на каждом пустом документе, был бы гораздо более раздражающим.

Настоящий баг — в каждой интеграции, которая трактует exit code как доказательство результата. Exit code отвечает на вопрос «завершился ли процесс?», а мы читаем его как «получили ли мы текст?». Это разные вопросы. Для конвертера, встретившего отсканированную страницу, ответы разные. Моя интеграция была одной из таких. Вероятно, и ваша тоже: pdftotext, pandoc и большинство инструментов извлечения имеют ту же форму, и это правильно.

Это переосмысление — вся суть. Всё ниже — следствия.

Правило: измеряйте выход, а не код возврата

Просто сказать «измеряйте выход» — мало. Нужен порог, а пороги — это место, где честная инженерия становится произвольной. Любой может написать if len(text) == 0: fail. Это поймает скриншот, но не поймает случай, который стоил мне времени.

Сложный случай: почти пустой документ

Сертификат о прохождении курса: одна страница, декоративная графика и строка заголовка настоящим текстом. Конвертация даёт:

Certificate of Completion

39 символов. Не ноль. Он проходит проверку на пустоту, кэшируется как успешный, и модель сообщает, что сертификат содержит только «Certificate of Completion». Это технически то, что ей дали.

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

Вопрос: как отличить «извлечение не удалось» от «документ действительно короткий»?

Байтовый размер — неверный инструмент

Очевидное решение — минимальный размер, скажем, 500 байт. Но это не работает, и причина точна: сырое количество байт смешивает длину документа с качеством извлечения. 800 байт — полная корректная конвертация одностраничной записки. 800 байт из 200-страничного отчёта — катастрофический сбой. Одно и то же число означает противоположные вещи, и мера не может их различить.

Вам нужна плотность, а не объём. Символов на страницу нормализует длину документа и оставляет только вопрос: «на каждой странице мы восстановили текст страницы?»

Измерение плотности

Я прогнал четыре реальных документа, включая проблемный:

  • Скриншот веб-страницы в PDF: 1 стр., 0 симв., 0 симв/стр.
  • Сертификат (графика + заголовок): 1 стр., 39 симв., 39 симв/стр.
  • Двухстраничный текстовый документ: 2 стр., 1864 симв., 932 симв/стр.
  • Двадцатистраничная презентация: 20 стр., 13289 симв., 664 симв/стр.

Две популяции, нет пересечения, порядок разницы. Этот разрыв — ключевой вывод. Это не тонкое статистическое разделение, требующее настроенного классификатора: извлекатель текста, встретив страницу текста, даёт сотни символов на страницу, а встретив картинку — десятки или ноль. Между ними ничего нет, потому что не существует документа, который на 40% состоит из текста.

Порог 100 символов на страницу находится в этом разрыве с запасом примерно в 6 раз с обеих сторон. Сертификат в 2,5 раза ниже, самый разреженный реальный документ в 6,6 раза выше. Небольшие вариации стиля — крупный шрифт, больше пробелов, титульный слайд — не могут пересечь такой широкий разрыв.

Этот запас — всё оправдание числа. Я не защищаю 100 как оптимальное, но как комфортно внутри разрыва, где ничего не живёт — это лучшее свойство для порога, чем точная настройка.

В какую сторону ошибаться

Каждый порог даёт ошибки. Вы выбираете, в какую сторону. Ложное срабатывание — разреженный PDF помечается как изображение — заставляет модель читать оригинал через зрение. Это дороже по токенам, но ничего не теряет: пользователь получает правильный ответ по более высокой цене. Ложное пропускание — пустая конвертация представлена как реальная — модель уверенно сообщает о пустом документе. Контент потерян полностью, и пользователь не видит сигнала, что что-то пошло не так. Это та ошибка, с которой я начал.

Эти затраты не симметричны, поэтому порог намеренно выбран в пользу первого. Когда он ошибается, он ошибается в сторону денег, а не правды.

Ещё одна асимметрия, обнаруженная на ошибках: тест плотности должен применяться только к PDF. Применять его ко всем форматам выглядит логично, но это ошибка. Однострочное письмо, голосовая заметка на 10 секунд, таблица из четырёх ячеек — всё корректно конвертируется в очень мало текста. Отбрасывать их — значит выбрасывать хорошие конвертации ради защиты от сбоя, которого у них не может быть. Только PDF несёт специфический риск, когда извлекатель текста встречает картинку. Всё остальное отбрасывается только при буквальном нуле.

Ещё три ловушки из той же серии

Когда начинаешь искать «успех, который не успех», оказывается, что это жанр.

Кэширование плохого результата хуже, чем его создание

Моя первая версия кэшировала по времени модификации. Нулевая конвертация затем подавалась на каждый будущий запрос — постоянно, без повторов, даже после исправления причины. Временный сбой стал постоянным из-за кэша. Теперь конвертации переоцениваются перед повторным использованием, непригодные результаты удаляются, а нулевые артефакты от старых версий очищаются при следующем запуске — апгрейд лечит кэш без вмешательства.

Тишина — это режим отказа

Хук не должен блокировать промпт, поэтому неожиданные ошибки завершаются с exit 0. Это правильно почти для всего, но катастрофично для одного случая: если markitdown не установлен, тихий no-op неотличим от «документ пуст» — именно того сбоя, ради предотвращения которого всё задумано. Отсутствие зависимостей теперь единственная ошибка, о которой сообщается громко, с командой установки.

Баг, который не давал вывода вовсе

На Windows PowerShell 5.1 добавляет UTF-8 BOM при передаче в нативную команду. json.load падает на BOM. Это исключение попало в обработчик «не блокировать промпт» и было проглочено, поэтому хук молча не делал ничего на каждой промпте на целой платформе. Два невидимых сбоя скомпоновались в третий. Теперь парсинг ввода устойчив к BOM, но урок тот же: обработчик ошибок, гарантирующий тишину, в конце концов гарантирует её для того, о чём вам нужно было услышать.

Лицензия — это решение о зависимостях

PyMuPDF читает некоторые PDF, которые pdfminer не может, поэтому он используется, если присутствует, но никогда не обязателен. Он имеет двойную лицензию AGPL-3.0/коммерческую, что не подходит для зависимостей MIT-проекта. Подсчёт страниц, необходимый для оценки, использует pdfminer, от которого markitdown уже зависит. Основной путь не добавляет ни зависимостей, ни copyleft.

Как это выглядит, когда работает

Когда конвертация извлекает реальный контент, модель получает указатель, а не текст:

[markitdown] /path/report.pdf сконвертирован в markdown по пути
~/.claude/markitdown-cache/report-a1b2c3d4.md (14973 байт, 304 строки,
14472 символа, 20 страниц). Если нужен контент документа, прочитайте
или выполните grep по .md файлу (не оригиналу).

Указатель важнее, чем кажется. Промпты упоминают документы предположительно — «сравните эти три отчёта» может реально требовать только один. Загрузка всех трёх в контекст стоит токенов на каждом последующем ходе разговора, даже если они не используются. Указатель стоит около 400 символов и оплачивается один раз. Модель читает или грепает файл, с offset и limit для больших, только если контент действительно нужен.

А когда извлечение не даёт ничего, файл не пишется, ничего не кэшируется, и модели сообщается правда:

[markitdown] НЕТ ПОЛЕЗНОГО ТЕКСТА извлечено из /path/screenshot.pdf
(текст не извлечён). Это PDF на основе изображений/сканированный —
извлечение текста не видит его, .md не создан. Прочитайте ОРИГИНАЛ
нативно через Read (используйте параметр pages для длинных PDF);
зрение Claude может прочитать его. НЕ сообщайте, что документ пуст.

Последняя строка нужна, потому что без неё модель именно так и делает.

Что вынести общего

Всё это не только про PDF. Любой конвейер, передающий вывод одного инструмента другому, подвержен этой опасности, как только первый инструмент может успешно ничего не сделать. Exit codes — это утверждение о завершении процесса. Они никогда не были утверждением о результате, и мы все читали их как таковые, потому что для большинства инструментов они совпадают.

Привычка, которую стоит взять: после любого шага извлечения измерьте, что вернулось, и решите, правдоподобно ли это для входа. Не «была ли ошибка» — ошибки легки, они сами о себе заявляют. Опасный исход — тот, что возвращает 0, пишет файл и пуст.

И когда пусто, скажите об этом. Модель, которой дали пустой файл, не задумается, что что-то пошло не так. Она скажет вашему пользователю, что документ пуст, тем же уверенным голосом, каким говорит, когда права.

Хук, который я построил на этом, имеет лицензию MIT и работает на Windows, macOS и Linux: claude-markitdown-hook. Измерения за порогом, включая калибровочные примеры, — в docs/DESIGN.md.

Что делать прямо сейчас

  1. Проверьте свой конвейер: используете ли вы только exit code для определения успеха? Если да — добавьте проверку плотности.
  2. Установите порог 100 символов на страницу для PDF-документов и логируйте случаи ниже порога.
  3. Не кэшируйте результаты, пока не убедитесь, что они не пустые. Храните только успешные конвертации.
  4. При пустом результате сообщайте об этом явно, чтобы модель не вводила пользователя в заблуждение.
#извлечение текста#PDF#exit code#обработка ошибок#интеграция
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →