208 lines
9.7 KiB
Markdown
208 lines
9.7 KiB
Markdown
# Архитектура 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 | Заголовки (H1–H3), **жирный**, *курсив*, таблицы (`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-запросов. Прогресс — бэкенд сообщает статус каждого файла.
|