14 KiB
Архитектура 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 /healthapi_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())
│ Scanner → Extractor → LLM NER → Obfuscator → Replacer → Builder
▼
bytes (ZIP-архив с обезличенными документами + mapping.csv)
Требования к Flask-приложению для Штурвала
1. /health — liveness probe
@health_bp.route("/health")
def health():
return jsonify({"ok": True, "version": "0.0.26"})
Обязательно. Штурвал дёргает /health для readiness/liveness checks. Без этого под не поднимется. Ответ должен быть 200 OK.
2. no_cache на все ответы
@app.after_request
def no_cache(response):
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
Критично для SPA-подобного UI. Без этого nginx/браузер кэшируют страницу → F5 показывает старую версию, кнопки не работают, версия не обновляется.
3. X-Accel-Buffering: no для SSE
return Response(
stream_with_context(generate()),
content_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
)
Критично для SSE-прогресса. Без этого nginx буферизует поток → прогресс не отображается в реальном времени, все события приходят одним блоком в конце.
4. MAX_CONTENT_LENGTH
app.config["MAX_CONTENT_LENGTH"] = 200 * 1024 * 1024 # 200 MB
Без этого Flask использует дефолтный лимит (~1MB) и отвергает docx/pdf.
5. GeneratorExit в SSE-генераторе
try:
yield f"event: start\n..."
except GeneratorExit:
return # клиент отключился — не продолжаем обработку
Критично. При закрытии вкладки браузер рвёт SSE-соединение. Без GeneratorExit генератор продолжает слать запросы в LLM, тратя токены впустую.
6. stream_with_context
from flask import stream_with_context
return Response(stream_with_context(generate()), ...)
Обязательно для SSE. Без этого Flask держит приложение заблокированным на время SSE-соединения, другие запросы не обрабатываются.
7. sys.path для импорта пакета drhider
_sys_path_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root)
Штурвал запускает из site/, пакет drhider/ на уровень выше. Без этого импорт не работает.
8. In-memory session storage
Сессии хранятся в site/session.py как Dict[str, dict]. Не персистентны — теряются при рестарте пода. Очистка по таймауту (2 часа).
9. LLM-клиент через переменные окружения
LLM_KEY # API ключ
LLM_URL # эндпоинт (по умолчанию https://api.aillm.ru/v1/chat/completions)
LLM_MODEL # модель (по умолчанию gpt-oss-120b)
10. BOM в CSV
buf.write('\ufeff'.encode('utf-8') + csv_str.encode('utf-8'))
BOM (\ufeff) нужен для корректного открытия CSV в Excel (чтобы русские буквы не ломались).
│
▼
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-архив
### Имена скачиваемых файлов
Формат: `drhider_YYYY-MM-DD_HH-MM-SS.zip` и `mapping_YYYY-MM-DD_HH-MM-SS.csv`
Время — московское (UTC+3). Генерируется на сервере в момент запроса:
```python
# api_bp.py — download() / csv_download()
ts = (datetime.now() + timedelta(hours=3)).strftime("%Y-%m-%d_%H-%M-%S")
download_name=f"drhider_{ts}.zip" # → Content-Disposition
download_name=f"mapping_{ts}.csv" # → Content-Disposition
Фронтенд (JS) читает имя из заголовка Content-Disposition, не хардкодит.
Принципы
- Модули — чистые функции. Где возможно — без классов, без состояния.
- Единственный класс — TwoPassObfuscator. Только он управляет состоянием (mapping, sorted_keys).
- Flask — тонкая прослойка. Только принимает запрос, вызывает
obfuscate_files(), отдаёт ответ. - Без БД. Всё в памяти. Никаких внешних зависимостей кроме LLM API.
- ⛔ Ноль внешних зависимостей на фронтенде. Никаких CDN, внешних шрифтов, иконок, JS-библиотек. ВСЁ в коде. Страница грузится за 1 запрос.
- ⛔ ВСЯ обработка данных — на бэкенде. Фронтенд только отображает и скачивает. Никакой JS-логики для ZIP, парсинга, обфускации. Один запрос → один ответ.
- ⛔ Пофайловая обработка — на бэкенде. Файлы обрабатываются строго последовательно (по одному), без параллельных LLM-запросов. Прогресс — бэкенд сообщает статус каждого файла.