Глубокий парсинг PDF для RAG: два уровня извлечения текста без потери качества | AiManual
AiManual Logo Ai / Manual.
10 Июн 2026 Гайд

Глубокий парсинг PDF для RAG: как извлекать текст из многостраничных документов без потери качества

Руководство по двухуровневому парсингу PDF для RAG: текстовый слой + layout-анализ. Реальные кейсы ошибок, сравнение инструментов и пошаговый пайплайн на июнь 2

Реклама
vec_recv1

Когда RAG ломается из-за кривого парсинга — диагноз и вскрытие

RAG-системы — как тот самый друг, который умно рассуждает, но путает Шекспира с каким-то блогером. И виноват не LLM, а то, что на вход пришло. Подайте модели абзац из середины двухколоночного PDF — и она честно прочитает "продажа алкоголя запрещена" как "алкоголя запрещена продажа". Не потому, что модель глупая, а потому что колонки не разделили, и текст склеился в кашу.

Я пересмотрел десятки RAG-пайплайнов на собеседованиях и в реальных проектах. В 80% случаев проблема не в выборе LLM или эмбеддера — проблема в парсинге. Берут PyMuPDF, выгребают весь текст без разбора, режут по 512 токенов — и удивляются, почему ответы нерелевантны.

Типичная ошибка: считать PDF одной кучкой текста. На самом деле PDF — это контейнер с координатами каждого символа, линиями, изображениями, шрифтами. Игнорировать разметку — всё равно что отдавать нейросети несортированный набор букв вместо документа.

Два слоя, которые нужно разделять

Любой PDF (кроме простых текстовых) содержит два слоя:

  • Текстовый слой — сам текст, его шрифты, кодировки, порядок следования (иногда неправильный).
  • Визуальный слой — позиционирование элементов: колонки, таблицы, колонтитулы, изображения, подложки.

Обычные текстовые экстракторы (PyMuPDF, pdfplumber) работают с первым слоем. Они дергают строки из внутренней структуры PDF, но не понимают, где заканчивается колонка и начинается следующая. Для простых документов с одним столбцом это норм, но попробуйте скормить научную статью в две колонки — получите винегрет.

Решение — layout-анализ: мы сначала распознаём визуальную структуру (колонки, заголовки, таблицы, подписи), а потом извлекаем текст в правильном порядке. Именно так работает RAG-Anything и современный Docling.

Инструментальная карта на середину 2026 года

Инструмент Версия (актуальная на июнь 2026) Сильные стороны Слабые места
PyMuPDF (fitz) 1.26.2 Скорость, извлечение метаданных, работа с встроенными шрифтами Не понимает layout, склеивает колонки
pdfplumber 0.12.0 Таблицы, точное позиционирование, отличная работа с границами Медленный на больших файлах, не умеет распознавать колонки
Tesseract 5 5.5.0 (LSTM) OCR для сканов, бесплатно, поддерживает 100+ языков Чувствителен к качеству изображения, не понимает layout сам по себе
Docling 2.8.0 Семантический layout-анализ, понимание структуры, встроенный AI-анализатор Требует GPU для лучшей работы, сложен в настройке
Unstructured.io 0.16 (library) / API Гибкость, поддержка множества форматов, хитрая логика разделения Медленный, для серьезной точности требуется GPU или API-ключ
Kreuzberg (Rust) 4.1.0 Скорость (Rust), легковесность, хорошо для простых документов Плохо с layout и таблицами (см. наш обзор Kreuzberg v4)

Выбор инструмента зависит от типа документов. Если у вас однородные счета с чёткой структурой — хватит pdfplumber. Если научные статьи, контракты, отчёты — без layout-анализа (Docling, Unstructured) не обойтись.

Практика: как выглядит правильный пайплайн

Разберём на реальном примере — многостраничный отчёт с таблицами, колонтитулами и сносками. Я покажу код на Python, который можно запустить на ноутбуке (16 ГБ RAM, CPU). Полный стек описан в статье RAG на ноутбуке.

1 Подготовка и загрузка документа

Используем PyMuPDF для быстрого извлечения текстового слоя и метаданных, а pdfplumber — для таблиц. Но прежде нужно понять структуру страницы.

import fitz  # PyMuPDF
import pdfplumber
from PIL import Image
import io

doc = fitz.open("report.pdf")
# Получаем количество страниц, метаданные
print(f"Страниц: {doc.page_count}, автор: {doc.metadata.get('author')}")

2 Layout-анализ: определяем колонки и блоки

Вручную писать детектор колонок — гиблое дело. Проще использовать Docling или pdf2image + Tesseract с опцией layout. Но если нужно быстрое решение — анализируем координаты текстовых блоков из PyMuPDF.

def get_text_blocks(page):
    blocks = page.get_text("dict")["blocks"]
    text_blocks = []
    for b in blocks:
        if b["type"] == 0:  # текстовый блок
            text_blocks.append({
                "x0": b["bbox"][0],
                "y0": b["bbox"][1],
                "x1": b["bbox"][2],
                "y1": b["bbox"][3],
                "text": "".join([ span["text"] for line in b["lines"] for span in line["spans"]])
            })
    return text_blocks

# Простая эвристика: если на странице есть блоки, чьи x-координаты сильно смещены — две колонки
blocks = get_text_blocks(page)
x_positions = [ (b["x0"]+b["x1"])/2 for b in blocks ]
if max(x_positions) - min(x_positions) > page.rect.width * 0.4:
    print("Похоже на two-column layout")

На практике лучше сразу использовать Docling. Он работает на базе AI-модели, которая натренирована на миллионах PDF и умеет выделять колонки, заголовки, сноски. В нашем гайде по Docling разобраны стратегии для больших документов.

3 Извлечение структурированных элементов: таблицы, сноски, подписи

Таблицы — главная головная боль. pdfplumber с ними справляется, но только если границы ячеек чётко прорисованы. Если таблица без линий — нужен либо OCR с детекцией таблиц, либо библиотека вроде Camelot или Table-Transformer.

with pdfplumber.open("report.pdf") as pdf:
    for page_num, page in enumerate(pdf.pages):
        tables = page.extract_tables()
        if tables:
            print(f"Страница {page_num+1}: найдено {len(tables)} таблиц")
            for table in tables:
                # table — список строк, каждая строка — список ячеек
                for row in table:
                    print(row)

Совет: после извлечения таблицы не сохраняйте её как плоский текст. Лучше перевести в Markdown-таблицу или JSON. Тогда LLM сможет интерпретировать связь между столбцами. Плохая идея — склеить все ячейки через пробел.

4 OCR для сканов и плохих PDF

Если документ — скан (нет текстового слоя), без OCR не обойтись. Используем Tesseract с опцией psm 3 (автоматическое определение страницы) или, если нужно распознать колонки, psm 4.

tesseract scan.png output -l eng+rus --psm 4

Но Tesseract сам по себе не даёт структуры. Поэтому лучше сначала извлечь страницу как изображение (PyMuPDF может рендерить страницы), затем обработать через layout-анализатор (например, DocTR или Surya OCR от VikParuchuri), который выдаёт блоки текста с координатами. Эти блоки уже можно сортировать по колонкам.

5 Сборка финального документа и чанкинг

После извлечения всех элементов (текст в порядке чтения, таблицы как Markdown, подписи к изображениям) собираем единый документ. Важно сохранить иерархию: заголовки H1, H2, секции. Это поможет при чанкинге. Мы используем семантический чанкинг: соединяем родственные абзацы, не разрывая таблицы или списки.

# Пример: собираем страницу как Markdown-документ
def page_to_markdown(page, blocks, tables):
    md = []
    for block in blocks:
        if block['type'] == 'title':
            md.append(f"## {block['text']}")
        elif block['type'] == 'paragraph':
            md.append(block['text'])
        # ... ещё блоки
    for table in tables:
        md.append(table_to_markdown(table))
    return "\n\n".join(md)

Подробности чанкинга для RAG — в статье Почему RAG работает на демо, но ломается в проде. Если не следить за разрывом контекста — модель ответит только частью информации.

Как НЕ надо: три примера того, что убивает качество

💡
Я собрал статистику за два года: больше 40% ошибок в ответах RAG связаны с тем, что парсер неверно интерпретировал структуру PDF. Вот топ-3 грабли.

Ошибка 1: Порядок текста при двух колонках

Типичная ситуация: PyMuPDF выдёргивает текст построчно слева направо. Если колонок две, он сначала прочитает первую строку левой колонки, потом первую строку правой — и текст смешивается. В итоге LLM получает кашу.

Правильный порядок:                                                                              
[Левая колонка] Lorem ipsum dolor sit amet           [Правая колонка] Consectetur adipiscing elit.

Что получаем:                                                                                    
Lorem ipsum dolor sit amet Consectetur adipiscing elit.                                         
(и дальше в том же духе)

Решение: после извлечения блоков с координатами, сортируем их по x, затем по y, группируем по колонкам. Ещё лучше — использовать библиотеку с детекцией колонок, такую как Unstructured.io или Docling.

Ошибка 2: Сноски и колонтитулы

Колонтитулы (типа “Страница 3 из 10”) частенько внедряются в середину текста. Многие парсеры их пропускают, но если попадает в чанк — модель путает порядок страниц. Особенно опасно для многостраничных документов.

Решение: на основе анализа координат — блоки, которые повторяются на каждой странице в одной и той же позиции, можно исключить. Реализовать несложно: сравнить текст на всех страницах с одинаковыми y-координатами.

# Простой фильтр колонтитулов
from collections import Counter

def get_footer_candidates(pages_data):
    # pages_data — список страниц, каждая содержит список блоков с координатами и текстом
    footer_texts = []
    for page in pages_data:
        for block in page:
            if block['y0'] > page_height * 0.85:  # нижняя часть страницы
                footer_texts.append(block['text'])
    # Если один и тот же текст встречается на многих страницах — это колонтитул
    counts = Counter(footer_texts)
    return [text for text, count in counts.items() if count > len(pages_data) * 0.5]

Ошибка 3: Таблицы без границ

pdfplumber не видит таблицу, если нет чётких линий. В итоге текст из таблицы извлекается как обычный текст, но теряется структура — модель не может понять, что это таблица, и отвечает неверно. Выход — использовать детектор таблиц на базе компьютерного зрения (например, Table Transformer) или OCR с последующим анализом выравнивания.

Выбор подхода в зависимости от типа документа

Тип документа Рекомендуемый инструмент Почему
Простые одноколоночные тексты (письма, статьи) PyMuPDF + pdfplumber для таблиц Быстро, дёшево, без зависимостей
Многоколоночные (научные журналы, отчёты) Docling / Unstructured Layout-анализ нужен обязательно
Сканы (без текстового слоя) OCR (Surya / Tesseract) + layout-анализатор Сначала изображение, потом структура
Документы со сложными таблицами Camelot / Table Transformer + pdfplumber Комбинация даёт лучшее распознавание

Собираем всё вместе: простой, но эффективный пайплайн на Python

Ниже — готовый код, который можно взять за основу. Он делает layout-анализ через pdfplumber + PyMuPDF, детектирует таблицы, чистит колонтитулы и отдаёт чанки.

import pdfplumber
import fitz
from collections import defaultdict

class SmartPDFParser:
    def __init__(self, path):
        self.path = path
        self.doc = fitz.open(path)
        self.pages_content = []

    def parse(self):
        for page_num in range(len(self.doc)):
            page = self.doc[page_num]
            # Получаем текстовые блоки с координатами
            blocks = page.get_text("dict")["blocks"]
            # Фильтруем колонтитулы (упрощённо)
            page_height = page.rect.height
            blocks = [b for b in blocks if not self._is_header_footer(b, page_height)]
            # Сортируем по вертикали, потом по горизонтали
            blocks.sort(key=lambda b: (b["bbox"][1], b["bbox"][0]))
            # Собираем текст
            text = "\n".join([self._block_text(b) for b in blocks])
            # Если есть таблицы на странице — добавляем их в формате Markdown
            with pdfplumber.open(self.path) as pdf:
                page_plumber = pdf.pages[page_num]
                tables = page_plumber.extract_tables()
                for table in tables:
                    md_table = self._table_to_markdown(table)
                    text += "\n\n" + md_table
            self.pages_content.append(text)
        return "\n\n---\n\n".join(self.pages_content)

    def _is_header_footer(self, block, page_height):
        y0, y1 = block["bbox"][1], block["bbox"][3]
        top_margin = page_height * 0.05
        bottom_margin = page_height * 0.95
        return y1 < top_margin or y0 > bottom_margin

    def _block_text(self, block):
        text = ""
        for line in block["lines"]:
            for span in line["spans"]:
                text += span["text"]
            text += "\n"
        return text.strip()

    def _table_to_markdown(self, table):
        if not table:
            return ""
        md_rows = []
        for i, row in enumerate(table):
            row_str = "|" + "|".join([cell or "" for cell in row]) + "|"
            md_rows.append(row_str)
            if i == 0:
                separator = "|" + "|".join(["---"] * len(row)) + "|"
                md_rows.append(separator)
        return "\n".join(md_rows)

Важный нюанс: этот парсер не идеален. Он не умеет распознавать колонки — для настоящего продакшена используйте Docling или Unstructured. Но как стартовая точка для простых документов — вполне.

Когда один парсер — зло: как сделать адаптивную систему

Если у вас разнородные документы, не пытайтесь выудить всё одним инструментом. Лучше сделайте классификатор: быстрый анализ первых страниц (количество колонок, наличие таблиц, текстовый слой или скан) и на основе этого выбирайте стратегию. В статье Как выбрать метод RAG мы разбираем именно такой подход — от Regex до Vision моделей.

Неочевидный совет: не парсите PDF, если можно получить исходный формат

Звучит как ересь для DevOps’а, но иногда лучше отказаться от парсинга PDF в пользу другого формата. Если документы создаются в вашей организации — настаивайте на экспорте в Markdown, HTML или DOCX. Confluence2md — отличный пример: не нужно мучиться с PDF, если есть нормальные исходники. Но если PDF — единственный доступный формат, используйте многоуровневый подход, описанный выше.

И ещё: никогда не верьте, что PyMuPDF выдал идеально. Всегда проверяйте на реальных данных. Прогоняйте пайплайн на тестовой выборке, считайте метрики (например, совпадение с оригиналом по ключевым фразам). Без валидации вы просто гадаете.

Подписаться на канал