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.
