L3 Открытый урок · Стажёр

L3.5. Обработка документов

Извлечение текста из PDF, HTML и сканов, нормализация, метаданные в SQLite, параметризованный SQL и отчёт о качестве загрузки.

Объём
5 уроков · 12 часов
Компетенции
DATA-01·A, DATA-02·F, PROG-09·F, MM-02·F

Качество ответа начинается с качества текста

Если из PDF извлеклось «Гаран тия 24 ме сяца» или таблица превратилась в кашу, никакая модель это не исправит. Ingestion — первый и самый недооценённый слой: большинство странных ответов AI-систем объясняется тем, что в базу попал испорченный текст.

Формат Чем извлекать Типичные проблемы
TXT, Markdown pathlib кодировка (cp1251 vs utf-8)
HTML парсер HTML (например, BeautifulSoup) меню, футеры, скрипты в тексте
DOCX python-docx или конвертер таблицы, сноски
PDF с текстовым слоем pypdf; для структуры — Docling колонки, переносы, колонтитулы на каждой странице
Скан (PDF-картинка, фото) OCR (Tesseract) или vision-модель ошибки распознавания, таблицы

Docling и Tesseract — FAMILIAR-инструменты курса: понимать назначение и уметь выбрать. Начинайте с простого (pypdf), переходите к тяжёлому, когда eval показал проблему.

Нормализация

import re
import unicodedata

def normalize(text: str) -> str:
    text = unicodedata.normalize("NFC", text)            # «й» одним символом, а не «и» + знак
    text = text.replace("­", "")                    # мягкие переносы из PDF
    text = re.sub(r"(\w)-\n(\w)", r"\1\2", text)         # перенос слова через строку
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"\n{3,}", "\n\n", text)               # абзацы сохраняем
    return text.strip()

Нормализация не должна уничтожать структуру: заголовки и абзацы понадобятся при нарезке на фрагменты (L4.6).

Метаданные — в SQL

У каждого документа есть свойства: источник, дата, категория, язык, хеш содержимого, кто имеет доступ. Им место в базе данных, а не в имени файла. На L3 — SQLite (встроен в Python), на L4 — PostgreSQL.

import hashlib
import sqlite3

db = sqlite3.connect("corpus.db")
db.execute("""
CREATE TABLE IF NOT EXISTS documents (
    doc_id     TEXT PRIMARY KEY,
    path       TEXT NOT NULL,
    category   TEXT,
    updated_at TEXT,           -- ISO-дата
    sha256     TEXT NOT NULL,  -- изменился ли файл с прошлой загрузки
    n_chars    INTEGER,
    status     TEXT NOT NULL   -- ok | empty | parse_error
)""")

def upsert(doc_id: str, path: str, text: str, category: str, updated_at: str, status: str) -> None:
    digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
    db.execute(
        "INSERT INTO documents VALUES (?, ?, ?, ?, ?, ?, ?) "
        "ON CONFLICT(doc_id) DO UPDATE SET path=excluded.path, category=excluded.category, "
        "updated_at=excluded.updated_at, sha256=excluded.sha256, n_chars=excluded.n_chars, status=excluded.status",
        (doc_id, path, category, updated_at, digest, len(text), status),
    )
    db.commit()

Всегда передавайте значения параметрами (?), а не склеивайте строку SQL: иначе имя файла x'); DROP TABLE documents;-- станет командой.

Хеш нужен, чтобы при повторной загрузке не обрабатывать (и не оплачивать эмбеддинги) неизменённые документы.

SQL для аналитики корпуса. CTE (WITH) делает запрос читаемым, оконная функция считает «внутри группы»:

-- три самых свежих документа в каждой категории
WITH ranked AS (
  SELECT doc_id, category, updated_at,
         ROW_NUMBER() OVER (PARTITION BY category ORDER BY updated_at DESC) AS rn
  FROM documents WHERE status = 'ok'
)
SELECT category, doc_id, updated_at FROM ranked WHERE rn <= 3 ORDER BY category, rn;

Отчёт о качестве загрузки

Загрузчик обязан сказать, что не получилось: сколько файлов пустые, сколько упали при разборе, какие подозрительно короткие. Молчаливый пропуск файла — это ответ «не знаю» через месяц, причину которого никто не найдёт.

Практикум: загрузчик корпуса

ingest.py: папка с документами (MD, TXT, PDF, HTML — набор выдаёт преподаватель, в нём есть пустой файл, битый PDF и файл в cp1251) → нормализованный текст в data/clean/ + метаданные в SQLite. Повторный запуск не обрабатывает неизменённые файлы. В конце — отчёт: всего, ok, пустых, ошибок разбора с причиной; запрос «3 самых свежих в каждой категории». Тесты: нормализация переносов, идемпотентность повторного запуска, параметризованный SQL.

Диагностика уровня

С какого уровня начать?

Оставьте контакты — свяжемся, зададим несколько вопросов об опыте и целях и подскажем уровень входа: с нуля, с середины программы или через Fast Track для разработчиков.

Или напишите: hello@superint.ru