Files
drhider/docs/ARCHITECTURE.md
T

9.7 KiB
Raw Blame History

Архитектура DrHider

Общая схема

Пользователь (браузер)
    │
    ▼
https://drhider.pythonk8s.dev.nubes.ru/
    │
    ▼
Штурвал (Managed Flask на pythonk8s)
    │
    ▼
site/app.py  ──→  Flask (create_app)
    │
    ├── GET  /          → main_bp    → templates/index.html
    ├── GET  /health    → health_bp  → {"ok": true}
    └── POST /api/drhider → api_bp   → drhider.obfuscate_files()
                                           │
                                           ▼
                                    TwoPassObfuscator
                                           │
                              ┌────────────┼────────────┐
                              │            │            │
                         extractor     scanner      replacer
                         (текст)     (regex+LLM)   (замена)
                              │            │            │
                              └────────────┼────────────┘
                                           │
                                      builder
                                      (ZIP+CSV)
                                           │
                                           ▼
                                    application/zip

Поддерживаемые форматы файлов

Формат Обработка Детали
.docx → Markdown Заголовки (H1H3), жирный, курсив, таблицы (python-docx)
.pdf → Markdown Текст + таблицы (pdfplumber), без форматирования
.txt и прочие текстовые → как есть Декодируется UTF-8, ошибки заменяются
.zip → распаковка Файлы внутри обрабатываются рекурсивно. Защита от ZIP-бомб: ≤500 файлов, ratio ≤100:1, ≤500 MB
.doc (бинарный) не поддерживается Штурвал не даёт установить LibreOffice. Если понадобится — просить админов платформы

Ограничение на размер загрузки: 200 MB (MAX_CONTENT_LENGTH).


Двухпроходная обфускация

Проход 1: сбор сущностей

Файлы (.docx, .pdf, .txt, .zip)
    │
    ▼
extractor.expand_zips()          ← распаковать ZIP
    │
    ▼
extractor.convert_pdfs_to_docx() ← PDF → DOCX
    │
    ▼
extractor.extract_text()         ← извлечь текст из каждого файла
    │
    ├──→ scanner.scan_regex()    ← regex: телефоны, email, ИНН, ОГРН, КПП,
    │                               БИК, счета, паспорта, компании, ФИО
    │
    └──→ scanner.scan_llm_ner()  ← LLM: имена, адреса, паспорта
                                      (только если llm_client передан)
    │
    ▼
mapping = {оригинал → фиктивное}  ← глобальный словарь замен

Проход 2: замена

mapping + sorted_keys (по убыванию длины)
    │
    ▼
Для каждого файла:
    │
    ├── .docx → replacer.replace_in_docx()   ← склеить runs → заменить → сохранить
    ├── .doc  → без изменений (бинарный)
    └── .txt/.pdf → replacer.replace_in_text() ← простая строковая замена
    │
    ▼
builder.build_mapping_csv()       ← mapping.csv
builder.build_zip()               ← ZIP со всеми файлами + mapping.csv

Модули

drhider/config.py

Константы уровня всего пакета. НЕ содержит логики — только данные.

  • ENTITY_PATTERNS — 10 regex-паттернов
  • COMPANY_PATTERN, PERSON_PATTERN — скомпилированные regex
  • Словари: RU_SURNAMES, RU_NAMES, RU_PATRONYMICS, RU_CITIES, RU_STREETS, FAKE_DOMAINS

drhider/checksum.py

Чистые функции для расчёта контрольных сумм. Без побочных эффектов.

  • checksum_inn10(inn) → 1 цифра
  • checksum_inn12(inn) → 2 цифры
  • checksum_ogrn(ogrn) → 1 цифра

drhider/random_utils.py

Утилиты генерации случайных строк.

  • random_digits(n) → строка из n цифр
  • random_letters(n) → строка из n строчных латинских букв

drhider/generators/

Каждый генератор — независимая функция с сигнатурой generate_xxx(original: str) -> str. Параметр original нужен для извлечения префикса (например, «ИНН » из «ИНН 1234567890»). Генераторы НЕ общаются друг с другом — только импортируют из config, checksum, random_utils.

__init__.py содержит ENTITY_GENERATORS — словарь, связывающий entity_type из ENTITY_PATTERNS с функцией-генератором. Используется в scanner.scan_regex().

drhider/llm_client.py

HTTP-клиент к OpenAI-совместимому API. Читает переменные окружения. Интерфейс: .complete(prompt: str) -> str.

drhider/extractor.py

Три независимые функции (не класс):

  • extract_text(fname, content, ctype)(text, docx_document_or_None)
  • expand_zips(files) → распакованный список
  • convert_pdfs_to_docx(files) → PDF заменены на DOCX

drhider/scanner.py

Две функции (не класс), мутируют переданный mapping: Dict[str, str]:

  • scan_regex(text, mapping) — regex-поиск
  • scan_llm_ner(all_texts, mapping, llm_client) — LLM NER

drhider/replacer.py

Три чистые функции:

  • apply_replacements(text, mapping, sorted_keys) → строка с заменами
  • replace_in_docx(doc, mapping, sorted_keys) → bytes (изменённый DOCX)
  • replace_in_text(text, fname, mapping, sorted_keys) → bytes

drhider/builder.py

Две функции сборки результата:

  • build_zip(files, mapping_csv) → bytes (ZIP-архив)
  • build_mapping_csv(mapping) → str (CSV)

drhider/obfuscator.py

TwoPassObfuscator — оркестратор. Единственный класс, который знает о顺序е вызовов. Метод .obfuscate(files) вызывает модули в правильном порядке. obfuscate_files() — удобная функция-обёртка.

site/app.py

Точка входа для Штурвала. create_app() создаёт Flask, регистрирует blueprint'ы. app = create_app() — глобальный экземпляр.

site/routes/

Blueprint'ы — каждый в своём файле:

  • main_bp — только GET /
  • health_bp — только GET /health
  • api_bp — только POST /api/drhider

__init__.py содержит register_routes(app) — единая точка регистрации.


Поток данных

HTTP POST /api/drhider (multipart/form-data)
    │
    ▼
api_bp.drhider()
    │  request.files.getlist("files")
    │  → [(filename, bytes, mimetype), ...]
    ▼
obfuscate_files(files, llm_client=LLMClient())
    │
    ▼
TwoPassObfuscator.obfuscate(files)
    │
    ├── expand_zips → convert_pdfs_to_docx → extract_text
    ├── scan_regex + scan_llm_ner → mapping
    ├── replace_in_docx / replace_in_text → обфусцированные файлы
    └── build_mapping_csv + build_zip → (zip_bytes, csv_str)
    │
    ▼
send_file(BytesIO(zip_data), mimetype="application/zip")
    │
    ▼
HTTP 200 + ZIP-архив

Принципы

  1. Модули — чистые функции. Где возможно — без классов, без состояния.
  2. Единственный класс — TwoPassObfuscator. Только он управляет состоянием (mapping, sorted_keys).
  3. Flask — тонкая прослойка. Только принимает запрос, вызывает obfuscate_files(), отдаёт ответ.
  4. Без БД. Всё в памяти. Никаких внешних зависимостей кроме LLM API.
  5. Ноль внешних зависимостей на фронтенде. Никаких CDN, внешних шрифтов, иконок, JS-библиотек. ВСЁ в коде. Страница грузится за 1 запрос.
  6. ВСЯ обработка данных — на бэкенде. Фронтенд только отображает и скачивает. Никакой JS-логики для ZIP, парсинга, обфускации. Один запрос → один ответ.
  7. Пофайловая обработка — на бэкенде. Файлы обрабатываются строго последовательно (по одному), без параллельных LLM-запросов. Прогресс — бэкенд сообщает статус каждого файла.