Когда PyMuPDF не видит таблицу: анализ PDF-файлов для RAG с помощью Azure Layout
Enterprise Document Intelligence [Том 1 #5bis] – Те же реляционные таблицы. Встроенные ячейки таблиц. OCR для отсканированных страниц и изображений. Подписи и заголовки без регулярных выражений.
Делиться
Эта статья является дополнением к серии статей «Enterprise Document Intelligence», посвященной созданию корпоративной системы RAG из четырех компонентов. В статье 5 (анализ документов) парсер был создан с использованием PyMuPDF (fitz). В этой статье сохраняется та же цель и те же реляционные таблицы, но движок заменен на Azure Layout (модель prebuilt-layout ), более функциональный пакет, который восстанавливает то, что не может воспроизвести fitz. Именно с этого пробела мы и начнем.

PyMuPDF (fitz) — быстрый, бесплатный и точный инструмент для работы с чистым текстом. Однако он также дает сбои в трех местах, и в каждом из них незаметно возникают проблемы с корпоративным RAG.
Таблица на странице 14 контракта. Фитц считывает ячейки по одной и объединяет их. Столбцовая структура исчезает. В блок попадает «Плата за продление 500, плата за настройку 200». Вашей модели предлагается угадать, какое число соответствует какой плате.
Отсканированная поправка вклеена в конец документа. Fitz считывает исходные страницы и возвращает пустые строки на отсканированных. Пользователь не получает ответа по поводу поправки, потому что парсер её так и не прочитал.
Рисунок с текстом внутри. Диаграмма с подписями осей. Подписанная печать. Скриншот электронной таблицы. Fitz возвращает ограничивающий прямоугольник изображения. Текст внутри исчез.
Azure Document Intelligence считывает все три типа данных. Это проприетарный облачный сервис Microsoft Azure, регулируемый Условиями использования онлайн-сервисов Microsoft. prebuilt-layout возвращает ячейки таблицы (строки, столбцы, заголовки), текст, распознанный с помощью OCR, для каждой страницы (в исходном или отсканированном виде), рисунки с текстом внутри них и роли абзацев ( title , sectionHeading , figureCaption , tableCaption ). Один вызов. Те же реляционные таблицы, что и у fitz, половина из них обогащена.
Конвейер обработки данных не интересует, какой именно механизм создал словарь. Извлечение, генерация, аннотирование — считываются строки. Они никогда не считывают PDF-файл.

1. Где Фитц слепой
Четыре случая. В каждом из них Fitz выдает ошибку, а Azure работает.
1.1. Таблицы: fitz возвращает плоские слова, Azure возвращает ячейки.
В таблице контрактов есть строки и столбцы. Заголовок «Плата за продление» находится в первом столбце, значение 500 — во втором. Fitz считывает страницу сверху вниз и выводит по одной строке на каждый текстовый сегмент. Четыре ячейки строки возвращаются в виде четырех отдельных слов. Иногда ячейки из строки ниже перемешиваются, если координаты Y близки. Процессор обработки данных видит «суп» из слов. Структура строк и столбцов, которая делает таблицу таблицей, исчезает.
prebuilt-layout Azure распознает каждую таблицу как структурированный объект. result.tables — это список таблиц, каждая из которых содержит cells индексированные по (row_index, column_index) . Строка заголовка помечена ( cell.kind == "columnHeader" ). Содержимое ячейки — это текст ячейки, точно так, как его ввел автор. Мы преобразуем таблицу в строки Markdown, чтобы она находилась внутри line_df , как и любое другое содержимое. Строка из четырех ячеек «Плата за продление | 500 | Плата за настройку | 200» становится одной строкой line_df с этим текстом Markdown. Строка заголовка получает разделитель | --- | --- | ... | , чтобы последующая модель могла считывать структуру обратно.
1.2. Изображения: fitz возвращает ограничивающий прямоугольник, Azure возвращает текст.
Во многих PDF-файлах содержатся рисунки с текстом внутри. Архитектурные схемы с подписями в прямоугольниках. Диаграммы с делениями осей и легендами. Подписанные печати. Встроенные скриншоты электронных таблиц. Fitz возвращает каждое изображение в виде прямоугольника и необработанных байтов. Текст внутри невидим для парсера.
Технология распознавания текста Azure (OCR) работает на каждой странице, включая пиксели внутри областей рисунка. Для каждого рисунка мы собираем все слова Azure, чей ограничивающий прямоугольник находится внутри области рисунка, и объединяем их в ocr_text . «Multi-Head Attention Concat Linear h» теперь находится в image_df.ocr_text для рисунка на странице 4 статьи об Attention. Поиск может сопоставить вопрос о «многоголовом внимании», даже если ответ представляет собой текст внутри рисунка.

1.3. Отсканированные страницы: fitz ничего не возвращает, Azure возвращает OCR.
В конце 30-страничного оригинального контракта вставляется 10-страничная отсканированная поправка. Fitz считывает оригинальные страницы и возвращает пустые строки для отсканированных. Парсер не замечает этого. Последующий конвейер обработки незаметно обрабатывает 75% документа. Пользователь даже не подозревает, что 25% отсутствуют.
Azure выполняет распознавание текста (OCR) на каждой странице независимо от источника. Исходные и отсканированные страницы возвращаются по одному и тому же пути result.pages[i].lines с одинаковой структурой. Столбец parsing_method в line_df позволяет последующему коду определить, какой движок создал какие строки. Словарь parsing_summary содержит поле n_pages , которое соответствует фактическому количеству страниц документа, а не только страницам с исходным текстом.

1.4. Заголовки и подписи: fitz использует регулярные выражения, Azure имеет явное указание ролей.
Fitz обнаруживает подписи к рисункам/таблицам с помощью регулярных выражений в начале каждой строки ( ^Figure d+b , ^Table d+b ). Он работает, когда подписи выглядят как «Рисунок 2», и пропускает остальное («Рис. 2», многострочные переносы). Также есть ложные срабатывания: предложение в основном тексте, начинающееся с «Рисунок 2», распознается как подпись, если это упоминание.

В поле paragraphs Azure есть метки ролей: каждый абзац в результате содержит тег, например, "figureCaption" , "tableCaption" , "title" или "sectionHeading" , который указывает на тип блока без использования регулярных выражений. "figureCaption" и "tableCaption" напрямую заполняют object_registry . Теги "title" и "sectionHeading" перестраивают оглавление. Тег — это модель макета Azure, определяющая функцию блока; у fitz нет эквивалента. Ключ соединения (object_type, object_id) по-прежнему извлекается с помощью того же регулярного выражения из текста подписи, поэтому cross_ref_df выполняет обратное соединение тем же способом.
Оглавление — более интересный случай. build_toc_df в Fitz считывает собственные закладки ( doc.get_toc() ). Если в PDF-файле нет собственных закладок, Fitz возвращает пустое оглавление. Это типичный корпоративный случай: экспорт в Word, отсканированные документы, PDF-файлы из генераторов форм. Azure восстанавливает оглавление на основе ролей абзацев. Каждый абзац с "title" становится записью первого уровня, каждый абзац с пометкой "sectionHeading" — второго уровня. Иерархия определяется порядком их появления. Это не идеальное решение, но оно позволяет получить пригодное для использования оглавление там, где Fitz не смог бы его создать.
2. Тот же контракт, более полные данные.
Одна функция. Те же таблицы, что и в parse_pdf , той же структуры. Один вызов Azure, используемый каждым конструктором. Этот вызов невелик: укажите SDK на документ с одним model_id , prebuilt-layout . (Другая предварительно созданная модель, prebuilt-read , предназначена только для распознавания текста; модель layout — это та, которая также возвращает таблицы, роли абзацев и порядок чтения.)
from azure.ai.documentintelligence import DocumentIntelligenceClient from azure.ai.documentintelligence.models import AnalyzeDocumentRequest from azure.core.credentials import AzureKeyCredential client = DocumentIntelligenceClient(endpoint, AzureKeyCredential(key)) # "Layout" = the prebuilt-layout model (NOT prebuilt-read, which is OCR only) with open("contract.pdf", "rb") as f: poller = client.begin_analyze_document( "prebuilt-layout", AnalyzeDocumentRequest(bytes_source=f.read()), ) result = poller.result() # tables, paragraph roles, OCR, reading order
parse_pdf_azure_layout — это аналог parse_pdf в Azure: та же структура вызова, тот же словарь таблиц, поэтому каждый последующий блок считывает его, не зная, какой движок использовался. Стоит взглянуть на тело запроса, поскольку оно соответствует структуре всех движков этой серии: один вызов, затем один небольшой построитель для каждой таблицы, и повторное использование независимых от движка построителей для таблиц, которым нужен только line_df .
def parse_pdf_azure_layout(pdf_path): result = analyze_pdf(pdf_path) # one call, prebuilt-layout line_df = azure_layout_pdf_to_line_df(pdf_path, result=result) image_df = build_image_df_azure_layout(result) # + ocr_text toc_df = build_toc_df_azure_layout(result) # paragraph roles object_registry = build_object_registry_azure_layout(result) # role tags page_df = build_page_df(line_df) # reused fitz builder (line_df only) cross_ref_df = build_cross_ref_df(line_df) # reused fitz builder (line_df only) return {"line_df": line_df, "image_df": image_df, "toc_df": toc_df, "object_registry": object_registry, "page_df": page_df, "cross_ref_df": cross_ref_df, "span_df": pd.DataFrame(), "parsing_summary": parsing_summary}
Если читать сверху вниз: один analyze_pdf выполняет вызов Azure один раз, затем один небольшой построитель для каждой таблицы считывает этот общий result , и две таблицы, которым нужен только line_df , page_df и cross_ref_df , создаются теми же самыми построителями fitz, которые использует собственный парсер. Словарь в конце — это контракт, который возвращает каждый движок.

parse_pdf , с различиями по строкам по сравнению с fitz – Изображение предоставлено автором.3. Что выигрывает каждый стол
3.1. line_df получает строки ячеек таблицы, распознавание текста на изображениях и метки выделения.
Таблица «График платежей» из 4 столбцов превращается в 6 строк в line_df : строку заголовка, разделитель Markdown и четыре строки данных.

line_df ; структура столбцов переносится внутрь текста Markdown – Изображение предоставлено автором. Мы храним ячейки внутри line_df вместо добавления отдельной table_cells_df . Одна таблица для каждого последующего блока, который нужно прочитать; строки абзацев и строки таблицы выглядят одинаково при выводе. Цена: запросы к каждой ячейке требуют этапа анализа Markdown. Для вопросов RAG это приемлемо. Извлекатель сопоставляет ключевые слова с текстом строки. LLM считывает Markdown напрямую.
Текст, распознанный с помощью OCR, также попадает в line_df в виде дополнительных строк. В результатах Azure result.pages[i].lines уже содержатся линии, попадающие в области рисунка, поэтому построитель линий автоматически их подхватывает. Отметки выделения (флажки) становятся односимвольными линиями: [x] для выбранного, [ ] для невыбранного. Формы с полями, отмеченными флажками, становятся доступными для запросов.
3.2. В image_df добавлен столбец ocr_text
Та же строка, новый столбец. Для каждого обнаруженного изображения мы перечисляем все слова Azure, ограничивающая рамка которых перекрывает область изображения как минимум на 50%, и объединяем их в ocr_text .

Тот же столбец в image_df созданном с помощью fitz, пуст. Парсер fitz не распознает изображения. Когда parsing_method == "fitz" , столбец ocr_text присутствует для проверки четности формы, но остается пустым. Код, проверяющий ocr_text != "" работает одинаково независимо от того, получена ли строка из fitz или Azure.
3.3. toc_df восстанавливается из ролей абзацев.
Если PDF-файл содержит встроенные закладки, функция fitz build_toc_df работает точно и без проблем: она считывает то, что написал автор. Если же закладок нет (как в большинстве корпоративных документов), fitz возвращает пустой toc_df , и на последующих этапах теряется структура разделов.
Конструктор Azure обрабатывает result.paragraphs , фильтрует по роли в {"title", "sectionHeading"} и формирует оглавление. Уровень 1 = заголовок, уровень 2 = заголовок раздела. Иерархия определяется порядком появления абзацев в документе. Те же столбцы start_page , end_page , start_y и breadcrumb , что и в оглавлении fitz. Проход обратного поиска, вычисляющий end_page ( start_page следующего элемента или предка, или total_pages для последнего раздела), идентичен проходу fitz; единственное различие заключается в источнике строк.
Реконструкция не идеальна. Azure не может различать уровни подразделов, кроме sectionHeading . Получаемая иерархия имеет максимум две вложенности. Для большинства корпоративных запросов этого достаточно: фрагмент с пометкой «Расписание платежей» позволяет LLM связать свой ответ с нужным разделом даже без полного пути Статья 14 > Расписание платежей.
3.4. object_registry получает определение роли подписи
Fitz обнаруживает подписи с помощью регулярных выражений, привязанных к началу строки: ^Figure d+b , ^Table d+b . Существует два режима ошибок. Ложные отрицательные результаты возникают, когда формат подписи отличается ( Fig. 2. вместо Figure 2 или многострочный перенос, который выносит число за пределы первой строки). Ложные срабатывания возникают, когда предложение в основном тексте начинается со слов «Рисунок 2 показывает…».
Azure обходит проблему с регулярными выражениями. В полях paragraphs явно указываются теги "figureCaption" и "tableCaption" . Мы считываем роль напрямую. Ключ объединения (object_type, object_id) в cross_ref_df по-прежнему извлекается из текста подписи с помощью того же регулярного выражения, которое использует построитель fitz, поэтому объединение работает одинаково с любым из этих механизмов. Преимущество заключается в возможности повторного использования: Azure обнаруживает подписи, которые пропускает fitz. Стоимость остается той же (один вызов Azure, результат используется в разных построителях).
3.5. parsing_summary получает статистику, специфичную для Azure.
В словарь для синтеза документов добавлены три новых поля:
-
n_tables_detected: количество таблиц, обнаруженных Azure (ноль для документа, содержащего только текст, ненулевое количество для контракта с таблицами). -
n_figures: количество фигур, идентифицированных моделью компоновки. -
n_selection_marks: количество флажков (заполненных или пустых), обнаруженных Azure на всех страницах.
Эти три параметра упрощают маршрутизацию документа. 30-страничный документ с n_tables_detected = 18 выглядит как договор, и структура таблиц имеет значение. Документ с n_selection_marks = 0 , вероятно, не является формой. Документ с n_figures = 0 — это только текст; нет смысла запускать распознавание текста на изображениях.
3.6. page_df и cross_ref_df: без изменений
Две таблицы сохраняют свою форму. page_df и cross_ref_df создаются исключительно на основе line_df , поэтому механизм, создавший line_df не имеет значения. Одна реализация, два механизма, никакого смещения.
В Azure переменная span_df пуста. Модель компоновки не поддерживает типографику подстрок (жирный или курсив для каждого слова). Если вам нужны span-элементы для определения заголовков или выделения терминов, используйте fitz для этого документа. Эти два механизма дополняют друг друга.
4. Столбец parsing_method: происхождение для адаптивного анализа.
В каждой таблице parse_pdf_azure_layout содержащей данные по каждой строке parsing_method == "azure_layout" . В каждой таблице parse_pdf (той, что с движком Fitz) parsing_method == "fitz" . Один и тот же столбец, одно и то же имя, оба движка. Проблема в последующих цепях.

parsing_method – Изображение предоставлено автором Вот что использует адаптивный синтаксический анализ (статья 10). По умолчанию используется fitz. Страницы, не прошедшие предварительную проверку (обнаружена область таблицы без извлеченных строк, страница с большим количеством изображений и редким текстом, слой OCR низкого качества), повторно анализируются Azure. Повторно проанализированные строки заменяют или добавляются к исходным строкам line_df . Столбец parsing_method сохраняет итоговый результат.
Три варианта дальнейшего использования, которые обеспечивает эта колонка:
- Дедупликация : если одна и та же страница прошла оба прохода, сохраняйте строки Azure поверх строк Fitz (
df.sort_values("parsing_method").drop_duplicates(["page_num", "line_num"], keep="first")если"azure_layout" < "fitz"лексикографически, или используйте явное сопоставление приоритетов). - Аудит : проверка вопроса, который попадает в строку с
parsing_method == "azure_layout", обходится дороже (требовалась поддержка Azure). Это можно учесть при взвешивании достоверности ответа. - Учет затрат :
(line_df.parsing_method == "azure_layout").any()за страницу показывает, какие страницы прошли через Azure и как выставлять счет за время обработки.
5. Стоимость и задержка
Azure не бесплатна. Три цифры имеют значение.
Задержка : обработка одной страницы с помощью prebuilt-layout занимает от 2 до 4 секунд. Документ из 30 страниц обрабатывается за 60–120 секунд. Fitz обрабатывает тот же документ менее чем за секунду. Когда пользователь ожидает ответа на запрос, сначала обработайте его с помощью Fitz. Передавайте запрос в Azure только для тех страниц, которые Fitz обрабатывает некорректно.
Финансовые затраты : Azure взимает плату за страницу. Стоимость уровня prebuilt-layout сегодня составляет около 10 долларов США за 1000 страниц. Контракт на 30 страниц стоит примерно 0,30 доллара США. Обработка 1000 таких контрактов в день обходится в 300 долларов США в день, если каждая страница проходит через Azure. Ограничение использования Azure только для тех страниц, которым это необходимо, снижает эти затраты в 10 раз и более.
Ограничения : лимит размера PDF-файла на один запрос составляет 500 МБ или 2000 страниц, в зависимости от того, что наступит раньше. Более крупные документы необходимо разбивать на части. Бесплатный тариф (F0) позволяет обрабатывать 500 страниц в месяц и подходит для разработки. Для производства обычно требуется тариф S0.
Порядок величин остается стабильным: fitz бесплатен, Azure стоит примерно цент за страницу. Точные цены на тарифные планы меняются в зависимости от региона и времени: рассматривайте приведенные выше цифры как калибровку, а не как договор. Article 10 выбирает, какой движок будет использоваться.
6. Когда звонить в какой раз
По умолчанию используется fitz. При обнаружении недостаточности сигнала fitz следует передать запрос в Azure.
Три сигнала, которые стоит подключить:
- На странице есть область таблицы, но fitz извлек мало или совсем не извлек структур, похожих на строки. Вычислите на
line_df: кластеризуйте линии по координате y, ищите последовательности коротких равномерно расположенных линий (признак ячеек). Если метаданные страницы указывают на «таблица обнаружена» (из функции fitzpage.find_tables()), но структура линий не похожа на таблицу, переходите к следующему шагу. - Страница содержит много изображений и мало текста. Файл
image_dfзанимает более 80% площади страницы, аline_dfсодержит менее 10 строк. Это может быть отсканированная страница без слоя распознавания текста или страница, представляющая собой одну большую диаграмму с текстом внутри. В любом случае требуется Azure. - Низкий показатель качества распознавания текста: если
page.get_text("text")в fitz возвращает искаженный текст (высокое соотношение символов замены Unicode, низкое соотношение слов в словаре), повторное распознавание текста выполняется с помощью Azure. Показательtext_quality_scoreвычисляется вpre_parse_signalsи считывается диспетчером.
Четвертый сигнал проще. Если в документе отсутствует собственное оглавление ( fitz.toc_df.empty ), а для генерации требуется контекст раздела, запустите документ один раз через Azure, чтобы получить восстановленное оглавление. Стоимость одна за документ, а не за запрос.
Статья 10 создает полный диспетчер. Столбец parsing_method позволяет каждому последующему этапу определить, какой движок работал в какой строке.
7. Заключение
Два движка, один контракт: одни и те же реляционные таблицы на выходе, один и тот же код на выходе независимо от того, какой из них работает.

Парсер не возвращает текст; он возвращает модель документа. Azure делает эту модель более полной (таблицы на уровне ячеек, OCR внутри рисунков, подписи, помеченные по роли, оглавление, восстановленное без закладок) за 2–4 секунды и примерно 0,01 доллара США за страницу. Fitz ничего не стоит и работает за миллисекунды. Правило маршрутизации простое: Fitz по умолчанию, Azure, когда сигнал от вышестоящего источника говорит, что Fitz недостаточно. Статья 10 связывает диспетчер.
8. Источники и дополнительная литература
В prebuilt-layout parse_pdf_azure_layout лежит документация Microsoft, основанная на исследованиях по извлечению таблиц на уровне ячеек (Smock et al. 2022), а также слой ролей абзацев, который преобразует визуальные области в структурные роли. Docling (статья 5ter) — это аналог той же каскадной модели с открытым исходным кодом; он обеспечивает тот же контракт таблиц на локальном оборудовании, что полезно, когда документы не могут покинуть здание.
В том же направлении, что и в статье:
- Microsoft, Azure AI Document Intelligence. Модель макета. Официальная документация по
prebuilt-layout, модели, лежащей в основеparse_pdf_azure_layout. Вывод таблиц на уровне ячеек, роли абзацев и охват OCR берут свое начало здесь. - Смок, Песала, Абрахам, PubTables-1M / Table Transformer (TATR), CVPR 2022 (arXiv:2110.00061). Исследование, лежащее в основе извлечения данных из таблиц на уровне ячеек, которое поставляется с Azure; полезно для понимания того, что делает
azure_layoutпод капотом».
Другой ракурс, другой контекст:
- Ауэр и др., Технический отчет Docling, IBM Research 2024 (arXiv:2408.09869). Локальный аналог каскадной компоновки Azure с открытым исходным кодом. Тот же контракт таблицы; обмен стоимости облачных вычислений на локальные. Правильный выбор, когда конфиденциальность блокирует загрузку в облако, требуемую Azure.
Ранее в серии:
- Document Intelligence: введение в серию. Четыре основных компонента; это углубленное изучение более совершенного механизма анализа документов.
- Базовая конфигурация Enterprise RAG: от PDF-файла до выделенного ответа. Конвейер обработки таблиц, заполняемых Azure Layout.
- Эмбеддинги — это не магия: предсказуемые режимы сбоев при поиске RAG. Как поиск сопоставляет текст ячеек и распознавание текста на изображениях, которые восстанавливает этот механизм.
- Переранжировщики тоже не волшебны: когда кросс-кодировочный слой оправдывает затраты. Переоценка фрагментов, построенных из этих строк.
- RAG — это не машинное обучение, и инструментарий машинного обучения решает не ту задачу. Почему синтаксический анализ — это инженерная работа, а не обучение модели.
- От регулярных выражений до моделей компьютерного зрения: какой метод RAG подходит для какой задачи? Какой метод синтаксического анализа подходит для какого документа?
- 10 распространенных ошибок RAG, которые мы постоянно наблюдаем на производстве. Ошибки, которых призваны избежать эти четыре кирпичика.
- Помимо функции extract_text: два слоя PDF-файла, определяющие качество RAG. Первая половина блока анализа: характер документа, сигналы и резюме.
- Прекратите возвращать плоский текст из PDF-файла: реляционная структура, необходимая RAG (ссылка будет позже). Вторая половина блока парсинга: реляционные таблицы, которые считывает каждый последующий блок.
Кежан Ши Посмотреть все Кежан Ши
Источник: towardsdatascience.com
Похожие записи
- Действительно ли нам нужны гигантские, шумные, пожирающие воду центры обработки данных, разрушающие наши сообщества? В условиях нынешней экономики? | Первая собака на Луне
- Лёд и гравитация. Технологии длительного хранения энергии
- Штат Флорида подал в суд на OpenAI, заявив, что Сэм Альтман продемонстрировал «полное игнорирование риска для человеческой жизни»
Оцените материал:
Похожие записи
Малые данные, большие карты: обучение геопространственных моделей машинного обучения при ограниченном объеме выборки.
05.06.2026
Найдено первое свидетельство о боевых слонах в Альпах
15.02.2026
