Стандартный пайплайн обработки PDF-документов устроен просто: берём один парсер и гоним через него все страницы подряд. Результат предсказуем - 80% бюджета уходит на обработку страниц, которым дорогой инструмент не нужен. Эта статья показывает альтернативу: каскад из трёх бесплатных эвристик, который за доли секунды определяет, справится ли дешёвый парсер с конкретной страницей или пора подключать Azure Document Intelligence либо vision LLM.
Мы разберём конкретные алгоритмы - подсчёт символов, обнаружение пустых bounding box изображений и кластеризацию координат строк. Каждый метод сопровождается кодом на Python, который можно скопировать и запустить. В конце - сквозной кейс на финансовом отчёте с цифрами экономии.
Почему один парсер на все страницы - это переплата
Типичный документ состоит из трёх типов страниц. Первый - чистый текст: абзацы, заголовки, списки. Простой парсер вроде PyMuPDF извлекает его мгновенно и бесплатно. Второй - таблицы и диаграммы, где текст есть, но структура потеряна. Третий - отсканированные изображения с текстом, где текстовый слой отсутствует.
Проблема возникает, когда мы отправляем весь документ в Azure Document Intelligence. Стоимость одного вызова - от $1.50 за 1000 страниц для prebuilt-layout. Для документа на 500 страниц это $0.75, но реальная цена растёт из-за задержек и лимитов. Если 400 из этих страниц - чистый текст, мы переплачиваем за них в 5-10 раз по сравнению с локальным парсингом.
Vision LLM, такие как GPT-4V или Claude, обходятся ещё дороже: $0.01-0.03 за изображение при разрешении high. Обработка 100 страниц с диаграммами через vision LLM стоит $1-3, что сопоставимо с месячным бюджетом на парсинг небольшого проекта. При этом текстовые страницы, скормленные vision LLM, не дают прироста качества - модель просто переписывает то, что уже извлёк PyMuPDF.
Каскадный подход решает эту проблему. Мы прогоняем каждую страницу через три быстрых проверки. Если страница «чистая» - парсим локально. Если обнаружена аномалия - отправляем в платный API. Результат: платные вызовы сокращаются на 70-90%, а качество извлечения данных остаётся на том же уровне.
Для тех, кто работает с большими корпусами документов, этот подход критичен. В анализе 146 тысяч тикетов Service Desk мы показали, как учёт реальных паттернов сокращает объём проблемных данных в 4.5 раза. Аналогичная логика применима и к парсингу PDF: не все страницы одинаково сложны, и платить за простые - ошибка.
Что именно ломает дешёвые парсеры: три главные аномалии
Дешёвые парсеры работают с текстовым слоем PDF. Они извлекают символы, их координаты и шрифты, но не понимают визуальную структуру. Три типа контента систематически вызывают сбои.
Flattened-таблицы: когда текст есть, а таблицы нет
Возьмём страницу из финансового отчёта с таблицей на 10 строк и 5 колонок. В текстовом слое PDF каждая ячейка - отдельный текстовый блок со своими координатами. Дешёвый парсер читает их в порядке появления в потоке, который редко совпадает с визуальным порядком. Результат: числа и подписи идут сплошным потоком, связи между ячейками потеряны.
Пример вывода PyMuPDF для такой страницы:
Выручка 1 234 567 Себестоимость 890 123 Валовая прибыль 344 444 Операционные расходы 156 789 Чистая прибыль 187 655
Восстановить таблицу из такой каши невозможно. Нужен парсер, который анализирует координаты и строит сетку ячеек - а это уже функциональность Azure Document Intelligence или специализированных моделей. Если вы выбираете OCR-инструмент для сложных документов, обратите внимание на сравнение Nanonets-OCR2-3B, DeepSeek-OCR и OlmOCR-2 - там разобрана точность на таблицах и формулы для открытых моделей.
Рисунки с текстом: пустой bbox - не всегда ошибка
Диаграмма с подписями, встроенная в PDF как растровое изображение, не имеет текстового слоя. PyMuPDF видит объект типа «image» с bounding box, но внутри этого bbox нет текстовых блоков. Парсер молча пропускает страницу или возвращает пустую строку.
Техническая причина: PDF хранит изображения как XObject, а текст - в отдельных текстовых объектах. Если создатель документа вставил диаграмму скриншотом из Excel, текстовые метки «сплавлены» с пикселями. Извлечь их можно только через OCR или vision LLM.
Обнаружить такую страницу просто: находим все bbox изображений, проверяем наличие текста внутри них. Если изображение занимает более 30% страницы и не содержит текста - это кандидат на платный парсинг.
Многоколоночная вёрстка: когда строки перемешиваются
Научная статья в две колонки - классический пример. Дешёвый парсер читает строки слева направо по всей ширине страницы. Строка из левой колонки, затем строка из правой, снова из левой. Текст превращается в бессмыслицу.
Пример из двухколоночной статьи:
Методология исследования основана на previous work in natural language processing. Мы применили трансформерную архитектуру The experimental setup included 8 NVIDIA A100 GPUs. с механизмом внимания на 16 голов. Training took approximately 72 hours.
Первое и третье предложения - из левой колонки. Второе и четвёртое - из правой. Восстановить порядок можно только зная геометрию колонок, которую даёт кластеризация x-координат строк.
Каскад детерминированных проверок: архитектура принятия решений
Каскад - это последовательность из трёх эвристик. Каждая работает быстро и бесплатно. Порядок важен: от самой дешёвой проверки к более ресурсоёмкой. Если страница не проходит хотя бы одну - отправляем в платный парсер.
Схема принятия решений:
- Подсчёт символов на странице и сравнение со средним по документу.
- Обнаружение изображений с пустыми bounding box.
- Кластеризация x-координат строк для детекции колонок.
Все три эвристики реализуются на Python с библиотекой PyMuPDF. Код занимает менее 100 строк и выполняется за миллисекунды на страницу.
Эвристика 1: аномалии количества символов
Страницы с таблицами содержат аномально много цифр и спецсимволов. Алгоритм подсчитывает долю цифр среди всех символов страницы и сравнивает со средним значением по документу. Отклонение более чем на 2 стандартных отклонения - сигнал.
import fitz # PyMuPDF
import numpy as np
def char_anomaly_score(page, doc_avg_digit_ratio, doc_std_digit_ratio):
text = page.get_text()
if len(text) == 0:
return 0.0
digits = sum(1 for c in text if c.isdigit())
digit_ratio = digits / len(text)
if doc_std_digit_ratio == 0:
return 0.0
return abs(digit_ratio - doc_avg_digit_ratio) / doc_std_digit_ratio
# Вычисляем статистику по документу
doc = fitz.open("report.pdf")
ratios = []
for page in doc:
text = page.get_text()
if len(text) > 0:
ratios.append(sum(1 for c in text if c.isdigit()) / len(text))
doc_avg = np.mean(ratios)
doc_std = np.std(ratios)
# Проверяем каждую страницу
for i, page in enumerate(doc):
score = char_anomaly_score(page, doc_avg, doc_std)
if score > 2.0:
print(f"Страница {i+1}: аномалия символов, score={score:.2f}")
Порог 2.0 означает, что доля цифр на странице отклоняется от среднего более чем на 2 сигмы. Для финансовых отчётов это надёжный индикатор таблиц. Для технической документации порог можно снизить до 1.5 - спецификации часто содержат размеры и допуски в цифровом виде.
Эвристика 2: обнаружение пустых bbox изображений
Алгоритм извлекает все изображения со страницы, получает их bounding box и проверяет, есть ли текст внутри этих прямоугольников. Если изображение занимает значительную площадь и не содержит текста - страница требует визуального парсинга.
def has_empty_image_bbox(page, min_image_area_ratio=0.3):
text_blocks = page.get_text("blocks")
images = page.get_images(full=True)
for img in images:
xref = img[0]
# Получаем bbox изображения через обёртку
img_rects = page.get_image_rects(xref)
if not img_rects:
continue
for rect in img_rects:
bbox = rect[0] # fitz.Rect
page_area = page.rect.width * page.rect.height
img_area = bbox.width * bbox.height
if img_area / page_area < min_image_area_ratio:
continue # Слишком маленькое изображение
# Проверяем, есть ли текст внутри bbox
text_inside = False
for block in text_blocks:
block_rect = fitz.Rect(block[:4])
if bbox.intersects(block_rect):
text_inside = True
break
if not text_inside:
return True # Найдено изображение без текста
return False
Параметр min_image_area_ratio=0.3 отсекает мелкие иконки и логотипы. Если изображение занимает менее 30% страницы, оно вряд ли содержит критичные данные. Для документов с большими диаграммами порог можно снизить до 0.2.
Эвристика 3: кластеризация координат строк для детекции колонок
Собираем x-координаты левого края всех текстовых блоков на странице. Применяем кластеризацию KMeans с K=2. Если расстояние между центрами кластеров превышает 100 пунктов - страница многоколоночная.
from sklearn.cluster import KMeans
def detect_columns(page, min_cluster_distance=100):
blocks = page.get_text("blocks")
x_coords = [b[0] for b in blocks if b[6] == 0] # b[0] - x0, b[6] - тип блока (0=текст)
if len(x_coords) < 10:
return False
x_coords = np.array(x_coords).reshape(-1, 1)
kmeans = KMeans(n_clusters=2, n_init=10, random_state=42)
kmeans.fit(x_coords)
centers = sorted(kmeans.cluster_centers_.flatten())
distance = centers[1] - centers[0]
return distance > min_cluster_distance
Порог в 100 пунктов (около 35 мм на стандартном A4) разделяет одноколоночные и многоколоночные макеты. Для документов с широкими полями порог можно увеличить до 150. Метод не требует предварительного знания числа колонок - KMeans сам находит два кластера, если они есть.
Когда подключать платные модели: Azure Document Intelligence и vision LLM
Платная модель вызывается, если сработала хотя бы одна эвристика. Выбор конкретного инструмента зависит от типа аномалии.
Azure Document Intelligence (prebuilt-layout) оптимизирован для таблиц. Он возвращает структуру с номерами строк и колонок, объединёнными ячейками и заголовками. Стоимость - $1.50 за 1000 страниц. Для страниц с таблицами это лучший выбор: качество распознавания структуры выше, чем у vision LLM общего назначения.
Vision LLM (GPT-4V, Claude 3.5 Sonnet) нужны для рисунков с текстом и нестандартных макетов. Они понимают контекст, могут описать диаграмму словами или извлечь текст из скриншота. Стоимость выше - $0.01-0.03 за изображение, но для 10% страниц документа это всё равно дешевле, чем обработка всего PDF.
Правило выбора:
- Сработала эвристика 1 (аномалия символов) → Azure Document Intelligence.
- Сработала эвристика 2 (пустой bbox) → vision LLM.
- Сработала эвристика 3 (многоколоночность) → Azure Document Intelligence, если в колонках таблицы; vision LLM, если текст.
Для сравнения: обработка 500-страничного отчёта целиком через Azure Document Intelligence стоит $0.75. Каскадный подход отправляет в платный парсинг 50 страниц (10%) и платит $0.075. Экономия - 90%. На дистанции в 1000 документов разница составляет $675.
Если вы работаете с большими объёмами документов и думаете о выборе OCR-решения, посмотрите сравнение открытых моделей OCR 2026. Там разобрана точность на таблицах, формулах и многоязычных документах с конкретными цифрами по бенчмаркам.
Ограничения и подводные камни: когда эвристики ошибаются
Каскад не гарантирует 100% точности. Ложные срабатывания и пропуски неизбежны, но их можно минимизировать калибровкой порогов.
Ложные срабатывания эвристики 1: страница с прайс-листом, где много цифр, но нет табличной структуры. Решение - добавить проверку на наличие линий или сетки через анализ векторной графики. Страница с фотографией, где bbox изображения пуст, но данные не нужны. Решение - проверять расширение изображения: JPEG и PNG обычно фотографии, а EMF и WMF - диаграммы из офисных пакетов.
Пропуски: таблица с текстовыми ячейками без цифр (например, расписание) не даст аномалии символов. Одноколоночный документ со сносками, где координаты строк скачут из-за надстрочных индексов. Решение для второго случая - фильтровать блоки по высоте: сноски обычно мельче основного текста.
Рекомендация: начинайте с консервативных порогов (2.5 сигмы для символов, 0.4 для площади изображений, 150 пунктов для колонок). Прогоните каскад на 50-100 документах из вашего корпуса, вручную проверьте расхождения. За две-три итерации вы подберёте пороги, которые дают менее 5% ошибок.
Методология калибровки порогов похожа на ту, что описана в кейсе с анализом 146 тысяч тикетов: учёт реальных паттернов важнее идеальной модели. Ваши документы имеют специфику, и универсальные настройки всегда требуют адаптации.
Практический пример: адаптивный парсинг финансового отчёта
Возьмём квартальный отчёт компании на 45 страниц. Состав: 30 страниц текста с описанием бизнеса, 10 страниц с таблицами финансовых показателей, 5 страниц с диаграммами выручки по сегментам.
Прогоняем через каскад:
- Эвристика 1 (символы): 12 страниц показали аномалию. 10 из них - таблицы, 2 - страницы с перечислением числовых KPI в текстовом виде (ложное срабатывание).
- Эвристика 2 (пустые bbox): 5 страниц с диаграммами обнаружены. Все 5 - корректно.
- Эвристика 3 (колонки): 0 страниц - отчёт одноколоночный.
Итог: 15 страниц из 45 отправлены в платный парсинг (33%). 30 страниц обработаны PyMuPDF локально.
Сравнение затрат:
- Полный парсинг через Azure Document Intelligence: 45 страниц × $0.0015 = $0.0675.
- Каскадный подход: 15 страниц × $0.0015 = $0.0225.
- Экономия: 67%.
Для одного отчёта разница незаметна. Но для ежеквартальной обработки 500 отчётов экономия составляет $22.50 против $33.75 - разница в $11.25 за квартал, или $45 в год. При добавлении vision LLM для диаграмм (5 страниц × $0.01 = $0.05) полная стоимость одного отчёта через каскад - $0.0725, что всё ещё сопоставимо с полным Azure-парсингом, но качество извлечения данных из диаграмм выше.
Ключевой вывод: каскад не всегда даёт драматическую экономию на маленьких документах. Его преимущество раскрывается на масштабе - тысячи страниц, сотни документов, где 70-90% контента не требуют платного парсинга. Для разовых задач проще заплатить за весь документ. Для production-пайплайнов каскад окупается за первую неделю.
Если вы проектируете пайплайн обработки документов и хотите понять, какие инструменты выбрать для разных типов контента, изучите возможности Hugging Face AI Sheets для бескодового извлечения данных из изображений. Там показано, как комбинировать OCR и визуальный анализ в одном интерфейсе без программирования.