v5.0.0: гибридные токены (Иванов_0001, ООО_Технология_0034), удалены generators/, checksum, random_utils
Deploy drhider / validate (push) Waiting to run
Deploy drhider / validate (push) Waiting to run
This commit is contained in:
@@ -1,69 +0,0 @@
|
|||||||
"""
|
|
||||||
Контрольные суммы для ИНН (10 и 12 знаков) и ОГРН.
|
|
||||||
|
|
||||||
Используются генераторами фиктивных значений для создания
|
|
||||||
синтаксически корректных номеров.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def checksum_inn10(inn: str) -> str:
|
|
||||||
"""Контрольная сумма для 10-значного ИНН (юридические лица).
|
|
||||||
|
|
||||||
Алгоритм:
|
|
||||||
1. Умножаем первые 9 цифр на коэффициенты [2,4,10,3,5,9,4,6,8]
|
|
||||||
2. Суммируем, берём (сумма % 11) % 10
|
|
||||||
|
|
||||||
Args:
|
|
||||||
inn: 9 цифр ИНН (без контрольной суммы)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Одна цифра контрольной суммы
|
|
||||||
"""
|
|
||||||
coeffs = [2, 4, 10, 3, 5, 9, 4, 6, 8]
|
|
||||||
s = sum(int(inn[i]) * coeffs[i] for i in range(9))
|
|
||||||
return str((s % 11) % 10)
|
|
||||||
|
|
||||||
|
|
||||||
def checksum_inn12(inn: str) -> str:
|
|
||||||
"""Контрольные суммы для 12-значного ИНН (физические лица).
|
|
||||||
|
|
||||||
Алгоритм (две контрольные цифры):
|
|
||||||
1. n1: умножаем первые 10 цифр на [7,2,4,10,3,5,9,4,6,8],
|
|
||||||
берём (сумма % 11) % 10
|
|
||||||
2. n2: добавляем n1 к числу, умножаем 11 цифр на
|
|
||||||
[3,7,2,4,10,3,5,9,4,6,8], берём (сумма % 11) % 10
|
|
||||||
|
|
||||||
Args:
|
|
||||||
inn: 10 цифр ИНН (без контрольных сумм)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Две цифры контрольных сумм (n1 + n2)
|
|
||||||
"""
|
|
||||||
c1 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
|
|
||||||
c2 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
|
|
||||||
|
|
||||||
# Первая контрольная цифра
|
|
||||||
s1 = sum(int(inn[i]) * c1[i] for i in range(10))
|
|
||||||
n1 = (s1 % 11) % 10
|
|
||||||
|
|
||||||
# Вторая контрольная цифра (с учётом первой)
|
|
||||||
inn2 = inn + str(n1)
|
|
||||||
s2 = sum(int(inn2[i]) * c2[i] for i in range(11))
|
|
||||||
n2 = (s2 % 11) % 10
|
|
||||||
|
|
||||||
return str(n1) + str(n2)
|
|
||||||
|
|
||||||
|
|
||||||
def checksum_ogrn(ogrn: str) -> str:
|
|
||||||
"""Контрольная сумма для ОГРН (13 знаков).
|
|
||||||
|
|
||||||
Алгоритм: первые 12 цифр как число % 11, затем % 10.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ogrn: 12 цифр ОГРН (без контрольной суммы)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Одна цифра контрольной суммы
|
|
||||||
"""
|
|
||||||
s = int(ogrn) % 11
|
|
||||||
return str(s % 10)
|
|
||||||
@@ -86,3 +86,31 @@ RU_STREETS = [
|
|||||||
|
|
||||||
# Фиктивные домены для генерации email
|
# Фиктивные домены для генерации email
|
||||||
FAKE_DOMAINS = ["example.ru", "mail.test", "company.local", "org.example.ru"]
|
FAKE_DOMAINS = ["example.ru", "mail.test", "company.local", "org.example.ru"]
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# Пулы для гибридных токенов (шаблон + номер)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Существительные для названий компаний (20)
|
||||||
|
COMPANY_NOUNS = [
|
||||||
|
"Технология", "Прогресс", "Гарант", "Стандарт", "Импульс",
|
||||||
|
"Вектор", "Сфера", "Альянс", "Синтез", "Меридиан",
|
||||||
|
"Спектр", "Формат", "Модуль", "Ресурс", "Платформа",
|
||||||
|
"Система", "Комплекс", "Проект", "Решение", "Сервис",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Города для адресов (20)
|
||||||
|
CITY_POOL = [
|
||||||
|
"Москва", "Санкт-Петербург", "Новосибирск", "Екатеринбург",
|
||||||
|
"Казань", "Нижний_Новгород", "Челябинск", "Самара", "Омск",
|
||||||
|
"Ростов-на-Дону", "Уфа", "Красноярск", "Воронеж", "Пермь",
|
||||||
|
"Волгоград", "Краснодар", "Саратов", "Тюмень", "Иркутск", "Хабаровск",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Улицы для адресов (20)
|
||||||
|
STREET_POOL = [
|
||||||
|
"Ленина", "Мира", "Пушкина", "Гагарина", "Советская",
|
||||||
|
"Кирова", "Октябрьская", "Молодёжная", "Садовая", "Центральная",
|
||||||
|
"Школьная", "Лесная", "Полевая", "Строителей", "Набережная",
|
||||||
|
"Парковая", "Солнечная", "Зелёная", "Юбилейная", "Весенняя",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
"""
|
|
||||||
Маппинг типов сущностей на генераторы фиктивных значений.
|
|
||||||
|
|
||||||
ENTITY_GENERATORS используется в TwoPassObfuscator._scan_regex()
|
|
||||||
для автоматического выбора генератора по типу найденной сущности.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .phone import generate_phone
|
|
||||||
from .email import generate_email
|
|
||||||
from .inn import generate_inn10, generate_inn12
|
|
||||||
from .ogrn import generate_ogrn
|
|
||||||
from .kpp import generate_kpp
|
|
||||||
from .bik import generate_bik
|
|
||||||
from .accounts import generate_rs, generate_ks
|
|
||||||
from .passport import generate_passport
|
|
||||||
from .fallback import generate_fallback
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
# Маппинг: entity_type → функция-генератор
|
|
||||||
# Ключи соответствуют ключам в config.ENTITY_PATTERNS
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
ENTITY_GENERATORS = {
|
|
||||||
"phone": generate_phone,
|
|
||||||
"email": generate_email,
|
|
||||||
"inn_ul": generate_inn10, # 10-значный ИНН → юрлица
|
|
||||||
"inn_fl": generate_inn12, # 12-значный ИНН → физлица
|
|
||||||
"ogrn": generate_ogrn,
|
|
||||||
"kpp": generate_kpp,
|
|
||||||
"bik": generate_bik,
|
|
||||||
"rs": generate_rs, # расчётный счёт
|
|
||||||
"ks": generate_ks, # корреспондентский счёт
|
|
||||||
"passport": generate_passport,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fallback-генератор — экспортируется отдельно для использования в scanner/obfuscator
|
|
||||||
FALLBACK_GENERATOR = generate_fallback
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
"""
|
|
||||||
Генераторы фиктивных банковских счетов.
|
|
||||||
|
|
||||||
- generate_rs: расчётный счёт (20 знаков)
|
|
||||||
- generate_ks: корреспондентский счёт (20 знаков)
|
|
||||||
|
|
||||||
Счета начинаются с реальных префиксов: 40702 (расчётный), 30101 (корреспондентский).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from ..random_utils import random_digits
|
|
||||||
|
|
||||||
|
|
||||||
def generate_rs(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный 20-значный расчётный счёт.
|
|
||||||
|
|
||||||
Сохраняет оригинальный префикс (р/с, расчётный счёт и т.д.).
|
|
||||||
Начинается с 40702 (реальный префикс расчётного счёта).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальная строка с расчётным счётом
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «р/с 40702XXXXXXXXXXXXXXX»
|
|
||||||
"""
|
|
||||||
m = re.match(r'(?:р/с|расч[её]тный\s*сч[её]т)\s*', original, re.IGNORECASE)
|
|
||||||
prefix = m.group(0) if m else ""
|
|
||||||
return prefix + "40702" + random_digits(15)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_ks(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный 20-значный корреспондентский счёт.
|
|
||||||
|
|
||||||
Сохраняет оригинальный префикс (к/с, кор/счёт и т.д.).
|
|
||||||
Начинается с 30101 (реальный префикс корр. счёта).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальная строка с корр. счётом
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «к/с 30101XXXXXXXXXXXXXXX»
|
|
||||||
"""
|
|
||||||
m = re.match(
|
|
||||||
r'(?:к/с|кор/сч|корр?[еи]?спондентский\s*сч[её]т)\s*',
|
|
||||||
original,
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
prefix = m.group(0) if m else ""
|
|
||||||
return prefix + "30101" + random_digits(15)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"""
|
|
||||||
Генератор фиктивных почтовых адресов.
|
|
||||||
|
|
||||||
Формат: Город, ул. Улица, д. Номер
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
from ..config import RU_CITIES, RU_STREETS
|
|
||||||
|
|
||||||
|
|
||||||
def generate_address(_: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный почтовый адрес.
|
|
||||||
|
|
||||||
Выбирает случайный город, улицу и номер дома из словарей.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
_: Оригинальный адрес (игнорируется)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «Москва, ул. Ленина, д. 42»
|
|
||||||
"""
|
|
||||||
city = random.choice(RU_CITIES)
|
|
||||||
street = random.choice(RU_STREETS)
|
|
||||||
house = random.randint(1, 200)
|
|
||||||
return f"{city}, ул. {street}, д. {house}"
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""
|
|
||||||
Генератор фиктивного БИК (9 знаков).
|
|
||||||
|
|
||||||
Формат: «БИК » + 9 цифр.
|
|
||||||
Первые 2 цифры — 04 (код РФ).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from ..random_utils import random_digits
|
|
||||||
|
|
||||||
|
|
||||||
def generate_bik(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный 9-значный БИК.
|
|
||||||
|
|
||||||
Сохраняет оригинальный префикс (например, «БИК »).
|
|
||||||
Начинается с 04 (код Российской Федерации).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальная строка с БИК
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «БИК 04XXXXXXX»
|
|
||||||
"""
|
|
||||||
m = re.match(r'БИК\s*', original, re.IGNORECASE)
|
|
||||||
prefix = m.group(0) if m else ""
|
|
||||||
return prefix + "04" + random_digits(7)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"""
|
|
||||||
Генератор фиктивных названий компаний.
|
|
||||||
|
|
||||||
Формат: ООО/ЗАО/АО «СлучайноеСуществительное»
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
|
|
||||||
def generate_company(_: str) -> str:
|
|
||||||
"""Сгенерировать фиктивное название компании.
|
|
||||||
|
|
||||||
Выбирает случайную организационно-правовую форму (ООО, ЗАО, АО)
|
|
||||||
и случайное существительное из словаря.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
_: Оригинальное название (игнорируется)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «ООО «Технология»»
|
|
||||||
"""
|
|
||||||
forms = ["ООО", "ЗАО", "АО"]
|
|
||||||
nouns = [
|
|
||||||
"Технология", "Прогресс", "Гарант", "Стандарт", "Импульс",
|
|
||||||
"Вектор", "Сфера", "Альянс", "Синтез", "Меридиан", "Спектр", "Формат",
|
|
||||||
]
|
|
||||||
return f'{random.choice(forms)} «{random.choice(nouns)}»'
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
"""
|
|
||||||
Генератор фиктивного email-адреса.
|
|
||||||
|
|
||||||
Сохраняет структуру оригинального email: локальная_часть@домен.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
from ..config import FAKE_DOMAINS
|
|
||||||
from ..random_utils import random_letters
|
|
||||||
|
|
||||||
|
|
||||||
def generate_email(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный email с тем же форматом.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальный email (для сохранения структуры, не используется)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Фиктивный email вида случайные_буквы@фиктивный_домен
|
|
||||||
"""
|
|
||||||
domain = random.choice(FAKE_DOMAINS)
|
|
||||||
local = random_letters(random.randint(5, 10))
|
|
||||||
return f"{local}@{domain}"
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
"""
|
|
||||||
Fallback-генератор фиктивных значений для неизвестных типов сущностей.
|
|
||||||
|
|
||||||
Используется когда LLM находит сущность с типом, которого нет
|
|
||||||
в специализированных генераторах (например, "contract_number", "employee_id").
|
|
||||||
|
|
||||||
Принцип: character-class preserving замена.
|
|
||||||
- Цифры → случайные цифры
|
|
||||||
- Буквы → случайные буквы (того же алфавита)
|
|
||||||
- Пробелы/знаки препинания → сохраняются
|
|
||||||
- Длина строки сохраняется
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
|
|
||||||
def generate_fallback(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивное значение, сохраняя структуру оригинала.
|
|
||||||
|
|
||||||
Правила замены:
|
|
||||||
- [0-9] → случайная цифра
|
|
||||||
- [A-Za-z] → случайная буква того же регистра
|
|
||||||
- [А-Яа-я] → случайная кириллическая буква того же регистра
|
|
||||||
- Все остальные символы (пробелы, дефисы, спецсимволы) → без изменений
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальное значение сущности
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Фиктивное значение той же длины и структуры
|
|
||||||
|
|
||||||
Example:
|
|
||||||
"Договор №123/2024" → "Фтщшлпь №847/5921"
|
|
||||||
"EMP-0042" → "XQJ-8193"
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
|
|
||||||
for ch in original:
|
|
||||||
if ch.isdigit():
|
|
||||||
# Цифра → случайная цифра
|
|
||||||
result.append(random.choice(string.digits))
|
|
||||||
|
|
||||||
elif 'A' <= ch <= 'Z':
|
|
||||||
# Заглавная латиница → случайная заглавная латиница
|
|
||||||
result.append(random.choice(string.ascii_uppercase))
|
|
||||||
|
|
||||||
elif 'a' <= ch <= 'z':
|
|
||||||
# Строчная латиница → случайная строчная латиница
|
|
||||||
result.append(random.choice(string.ascii_lowercase))
|
|
||||||
|
|
||||||
elif 'А' <= ch <= 'Я':
|
|
||||||
# Заглавная кириллица → случайная заглавная кириллица
|
|
||||||
result.append(chr(random.randint(0x0410, 0x042F)))
|
|
||||||
|
|
||||||
elif 'а' <= ch <= 'я' or ch == 'ё':
|
|
||||||
# Строчная кириллица → случайная строчная кириллица
|
|
||||||
result.append(chr(random.randint(0x0430, 0x044F)))
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Всё остальное (пробелы, дефисы, слэши, точки) → сохранить
|
|
||||||
result.append(ch)
|
|
||||||
|
|
||||||
return ''.join(result)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
"""
|
|
||||||
Генераторы фиктивных ИНН.
|
|
||||||
|
|
||||||
- generate_inn10: 10-значный ИНН (юридические лица)
|
|
||||||
- generate_inn12: 12-значный ИНН (физические лица)
|
|
||||||
|
|
||||||
Оба сохраняют префикс «ИНН » и генерируют корректную контрольную сумму.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
from ..random_utils import random_digits
|
|
||||||
from ..checksum import checksum_inn10, checksum_inn12
|
|
||||||
|
|
||||||
|
|
||||||
def generate_inn10(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный 10-значный ИНН (для юридических лиц).
|
|
||||||
|
|
||||||
Сохраняет оригинальный префикс (например, «ИНН »).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальная строка с ИНН
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «ИНН XXXXXXXXX-C» с корректной контрольной суммой
|
|
||||||
"""
|
|
||||||
m = re.match(r'ИНН\s*', original, re.IGNORECASE)
|
|
||||||
prefix = m.group(0) if m else ""
|
|
||||||
base = f"{random.randint(1,9)}{random_digits(8)}"
|
|
||||||
return prefix + base + checksum_inn10(base)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_inn12(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный 12-значный ИНН (для физических лиц).
|
|
||||||
|
|
||||||
Сохраняет оригинальный префикс (например, «ИНН »).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальная строка с ИНН
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «ИНН XXXXXXXXXX-CC» с двумя корректными контрольными суммами
|
|
||||||
"""
|
|
||||||
m = re.match(r'ИНН\s*', original, re.IGNORECASE)
|
|
||||||
prefix = m.group(0) if m else ""
|
|
||||||
base = f"{random.randint(1,9)}{random_digits(9)}"
|
|
||||||
return prefix + base + checksum_inn12(base)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"""
|
|
||||||
Генератор фиктивного КПП (9 знаков).
|
|
||||||
|
|
||||||
Формат: «КПП » + 9 цифр.
|
|
||||||
Последние 3 цифры — код причины постановки (01, 43 или 77).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
from ..random_utils import random_digits
|
|
||||||
|
|
||||||
|
|
||||||
def generate_kpp(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный 9-значный КПП.
|
|
||||||
|
|
||||||
Сохраняет оригинальный префикс (например, «КПП »).
|
|
||||||
Последние 3 цифры — один из реальных кодов причины постановки.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальная строка с КПП
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «КПП XXXX-код-XXX»
|
|
||||||
"""
|
|
||||||
m = re.match(r'КПП\s*', original, re.IGNORECASE)
|
|
||||||
prefix = m.group(0) if m else ""
|
|
||||||
return prefix + random_digits(4) + random.choice(["01", "43", "77"]) + random_digits(3)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"""
|
|
||||||
Генератор фиктивного ОГРН (13 знаков).
|
|
||||||
|
|
||||||
Формат: «ОГРН » + 13 цифр с корректной контрольной суммой.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from ..random_utils import random_digits
|
|
||||||
from ..checksum import checksum_ogrn
|
|
||||||
|
|
||||||
|
|
||||||
def generate_ogrn(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный 13-значный ОГРН.
|
|
||||||
|
|
||||||
Сохраняет оригинальный префикс (например, «ОГРН »).
|
|
||||||
Первая цифра всегда 1 (признак юридического лица).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальная строка с ОГРН
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «ОГРН 1XXXXXXXXXXX-C» с корректной контрольной суммой
|
|
||||||
"""
|
|
||||||
m = re.match(r'ОГРН\s*', original, re.IGNORECASE)
|
|
||||||
prefix = m.group(0) if m else ""
|
|
||||||
base = "1" + random_digits(11)
|
|
||||||
return prefix + base + checksum_ogrn(base)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""
|
|
||||||
Генератор фиктивных паспортных данных.
|
|
||||||
|
|
||||||
Формат: «паспорт » / «серия номер » + XX XX XXXXXX
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
from ..random_utils import random_digits
|
|
||||||
|
|
||||||
|
|
||||||
def generate_passport(original: str) -> str:
|
|
||||||
"""Сгенерировать фиктивные паспортные данные.
|
|
||||||
|
|
||||||
Сохраняет оригинальный префикс (паспорт, серия номер и т.д.).
|
|
||||||
Формат: 2 цифры серии + 2 цифры + 6 цифр номера.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: Оригинальная строка с паспортными данными
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «паспорт XX XX XXXXXX»
|
|
||||||
"""
|
|
||||||
m = re.match(r'(?:паспорт|серия\s+номер)[\s:№]*', original, re.IGNORECASE)
|
|
||||||
prefix = m.group(0) if m else ""
|
|
||||||
return prefix + f"{random.randint(10, 99)} {random.randint(10, 99)} {random_digits(6)}"
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""
|
|
||||||
Генератор фиктивных ФИО.
|
|
||||||
|
|
||||||
Формат: Фамилия И.О. (фамилия полностью, имя и отчество — инициалы).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
from ..config import RU_SURNAMES, RU_NAMES, RU_PATRONYMICS
|
|
||||||
|
|
||||||
|
|
||||||
def generate_person(_: str) -> str:
|
|
||||||
"""Сгенерировать фиктивное ФИО в формате «Фамилия И.О.».
|
|
||||||
|
|
||||||
Выбирает случайные фамилию, имя и отчество из словарей.
|
|
||||||
Имя и отчество сокращаются до инициалов.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
_: Оригинальное ФИО (игнорируется)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка вида «Иванов А.И.»
|
|
||||||
"""
|
|
||||||
s = random.choice(RU_SURNAMES)
|
|
||||||
n = random.choice(RU_NAMES)
|
|
||||||
p = random.choice(RU_PATRONYMICS)
|
|
||||||
return f"{s} {n[0]}.{p[0]}."
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
"""
|
|
||||||
Генератор фиктивного телефонного номера.
|
|
||||||
|
|
||||||
Формат: +7 (код) XXX-XX-XX
|
|
||||||
Код выбирается случайно из списка реальных российских кодов.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
from ..random_utils import random_digits
|
|
||||||
|
|
||||||
|
|
||||||
def generate_phone(_: str) -> str:
|
|
||||||
"""Сгенерировать фиктивный российский телефонный номер.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
_: Оригинальный номер (игнорируется, нужен для единого интерфейса)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка в формате +7 (XXX) XXX-XX-XX
|
|
||||||
"""
|
|
||||||
code = random.choice(["495", "499", "812", "383", "343"])
|
|
||||||
return f"+7 ({code}) {random_digits(3)}-{random_digits(2)}-{random_digits(2)}"
|
|
||||||
@@ -44,6 +44,7 @@ class TwoPassObfuscator:
|
|||||||
"""
|
"""
|
||||||
self._mapping: Dict[str, str] = {}
|
self._mapping: Dict[str, str] = {}
|
||||||
self._sorted_keys: List[str] = []
|
self._sorted_keys: List[str] = []
|
||||||
|
self._counters: Dict[str, int] = {} # Глобальные счётчики токенов
|
||||||
self._llm_client = llm_client
|
self._llm_client = llm_client
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -79,11 +80,11 @@ class TwoPassObfuscator:
|
|||||||
|
|
||||||
# Regex-сканирование (быстрое, локальное)
|
# Regex-сканирование (быстрое, локальное)
|
||||||
if text and not text.startswith("[DOC binary"):
|
if text and not text.startswith("[DOC binary"):
|
||||||
scanner.scan_regex(text, self._mapping)
|
scanner.scan_regex(text, self._mapping, self._counters)
|
||||||
|
|
||||||
# LLM-сканирование (получает уже найденное regex'ом чтобы не дублировать)
|
# LLM-сканирование (получает уже найденное regex'ом чтобы не дублировать)
|
||||||
if self._llm_client:
|
if self._llm_client:
|
||||||
scanner.scan_llm_ner(all_texts, self._mapping, self._llm_client)
|
scanner.scan_llm_ner(all_texts, self._mapping, self._llm_client, self._counters)
|
||||||
|
|
||||||
# Предсортировать ключи один раз (по убыванию длины)
|
# Предсортировать ключи один раз (по убыванию длины)
|
||||||
self._sorted_keys = sorted(
|
self._sorted_keys = sorted(
|
||||||
@@ -122,6 +123,7 @@ class TwoPassObfuscator:
|
|||||||
# Очистка состояния (обфускатор может использоваться повторно)
|
# Очистка состояния (обфускатор может использоваться повторно)
|
||||||
self._mapping.clear()
|
self._mapping.clear()
|
||||||
self._sorted_keys.clear()
|
self._sorted_keys.clear()
|
||||||
|
self._counters.clear()
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
"""
|
|
||||||
Утилиты для генерации случайных строк.
|
|
||||||
|
|
||||||
Используются генераторами фиктивных значений для создания
|
|
||||||
случайных цифр и букв в номерах/реквизитах.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
|
|
||||||
def random_digits(n: int) -> str:
|
|
||||||
"""Сгенерировать строку из n случайных цифр.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
n: Количество цифр
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка из n случайных цифр (0-9)
|
|
||||||
"""
|
|
||||||
return ''.join(random.choice(string.digits) for _ in range(n))
|
|
||||||
|
|
||||||
|
|
||||||
def random_letters(n: int) -> str:
|
|
||||||
"""Сгенерировать строку из n случайных строчных латинских букв.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
n: Количество букв
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Строка из n случайных букв (a-z)
|
|
||||||
"""
|
|
||||||
return ''.join(random.choice(string.ascii_lowercase) for _ in range(n))
|
|
||||||
+69
-51
@@ -5,90 +5,118 @@
|
|||||||
1. scan_regex — быстрое regex-обнаружение (телефоны, email, ИНН, счета, компании, ФИО)
|
1. scan_regex — быстрое regex-обнаружение (телефоны, email, ИНН, счета, компании, ФИО)
|
||||||
2. scan_llm_ner — универсальное LLM-обнаружение (любые приватные данные)
|
2. scan_llm_ner — универсальное LLM-обнаружение (любые приватные данные)
|
||||||
|
|
||||||
LLM САМА определяет какие типы сущностей присутствуют в документе.
|
Замена: гибридные токены — читаемый шаблон + уникальный номер.
|
||||||
Никакого фиксированного списка типов.
|
Пример: Иванов_0001, ООО_Технология_0034, ул_Ленина_0015
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import random
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from .config import ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN
|
from .config import (
|
||||||
from .generators import ENTITY_GENERATORS, FALLBACK_GENERATOR
|
ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN,
|
||||||
from .generators.company import generate_company
|
RU_SURNAMES, COMPANY_NOUNS, CITY_POOL, STREET_POOL,
|
||||||
from .generators.person import generate_person
|
)
|
||||||
|
|
||||||
log = logging.getLogger("drhider")
|
log = logging.getLogger("drhider")
|
||||||
|
|
||||||
|
|
||||||
def _get_generator(entity_type: str):
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
"""Получить генератор по типу сущности. Если тип неизвестен — fallback.
|
# Гибридные токены: тип → (пул_шаблонов, префикс_для_счётчика)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TYPE_POOLS = {
|
||||||
|
"person_name": (RU_SURNAMES, "person"),
|
||||||
|
"person": (RU_SURNAMES, "person"),
|
||||||
|
"company": (COMPANY_NOUNS, "company"),
|
||||||
|
"phone": (["+7_000_000"], "phone"),
|
||||||
|
"email": (["email"], "email"),
|
||||||
|
"address": (CITY_POOL, "address"),
|
||||||
|
"inn": (["ИНН"], "inn"),
|
||||||
|
"inn_ul": (["ИНН"], "inn"),
|
||||||
|
"inn_fl": (["ИНН"], "inn"),
|
||||||
|
"kpp": (["КПП"], "kpp"),
|
||||||
|
"ogrn": (["ОГРН"], "ogrn"),
|
||||||
|
"bik": (["БИК"], "bik"),
|
||||||
|
"passport": (["Паспорт"], "passport"),
|
||||||
|
"bank_account": (["Счёт"], "bank"),
|
||||||
|
"rs": (["РС"], "bank"),
|
||||||
|
"ks": (["КС"], "bank"),
|
||||||
|
"contract_number": (["Договор"], "contract"),
|
||||||
|
"other_id": (["ID"], "other"),
|
||||||
|
}
|
||||||
|
_FALLBACK_POOL = (["Данные"], "data")
|
||||||
|
|
||||||
|
|
||||||
|
def _next_token(entity_type: str, counters: Dict[str, int]) -> str:
|
||||||
|
"""Сгенерировать токен замены: случайный шаблон из пула + номер.
|
||||||
|
|
||||||
|
Одна и та же сущность в mapping получает один и тот же токен
|
||||||
|
(mapping dict гарантирует согласованность).
|
||||||
|
Разные сущности одного типа получают РАЗНЫЕ шаблоны из пула.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
entity_type: Строковый идентификатор типа (например, "phone", "person_name")
|
entity_type: Тип сущности (person_name, company, phone...)
|
||||||
|
counters: Глобальные счётчики {prefix_key: count}
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Функция-генератор: (str) -> str
|
Строка вида "Иванов_0001", "ООО_Технология_0034", "[Адрес_0015]"
|
||||||
"""
|
"""
|
||||||
return ENTITY_GENERATORS.get(entity_type, FALLBACK_GENERATOR)
|
pool, prefix_key = TYPE_POOLS.get(entity_type, _FALLBACK_POOL)
|
||||||
|
template = random.choice(pool)
|
||||||
|
key = f"{prefix_key}_{template}"
|
||||||
|
counters[key] = counters.get(key, 0) + 1
|
||||||
|
return f"{template}_{counters[key]:04d}"
|
||||||
|
|
||||||
|
log = logging.getLogger("drhider")
|
||||||
|
|
||||||
|
|
||||||
def scan_regex(text: str, mapping: Dict[str, str]) -> None:
|
def scan_regex(text: str, mapping: Dict[str, str], counters: Dict[str, int]) -> None:
|
||||||
"""Сканировать текст regex-паттернами, заполнить словарь замен.
|
"""Сканировать текст regex-паттернами, заполнить словарь замен.
|
||||||
|
|
||||||
Ищет в тексте:
|
Замена: гибридные токены (шаблон + номер).
|
||||||
- Сущности из ENTITY_PATTERNS (телефоны, email, ИНН, ОГРН, КПП, БИК, счета, паспорта)
|
|
||||||
- Названия компаний (кроме «НУБЕС» — это Исполнитель, не заменяем)
|
|
||||||
- ФИО в формате «Фамилия И.О.»
|
|
||||||
|
|
||||||
Найденные значения добавляются в mapping: оригинал → фиктивное значение.
|
|
||||||
Если сущность уже есть в mapping — не перезаписываем (согласованность).
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Текст документа для сканирования
|
text: Текст документа для сканирования
|
||||||
mapping: Словарь замен (мутабельный, пополняется)
|
mapping: Словарь замен (мутабельный, пополняется)
|
||||||
|
counters: Глобальные счётчики токенов (мутабельный)
|
||||||
"""
|
"""
|
||||||
# ── Сущности по regex-паттернам (телефоны, email, реквизиты) ──
|
# ── Сущности по regex-паттернам ──
|
||||||
for entity_type, pattern in ENTITY_PATTERNS.items():
|
for entity_type, pattern in ENTITY_PATTERNS.items():
|
||||||
for match in re.finditer(pattern, text, re.IGNORECASE | re.MULTILINE):
|
for match in re.finditer(pattern, text, re.IGNORECASE | re.MULTILINE):
|
||||||
original = match.group(0).strip()
|
original = match.group(0).strip()
|
||||||
if not original or original in mapping:
|
if not original or original in mapping:
|
||||||
continue
|
continue
|
||||||
|
mapping[original] = _next_token(entity_type, counters)
|
||||||
# Генератор: специализированный (если есть) или fallback
|
|
||||||
generator = _get_generator(entity_type)
|
|
||||||
mapping[original] = generator(original)
|
|
||||||
|
|
||||||
# ── Названия компаний ──
|
# ── Названия компаний ──
|
||||||
for match in COMPANY_PATTERN.finditer(text):
|
for match in COMPANY_PATTERN.finditer(text):
|
||||||
original = match.group(0).strip()
|
original = match.group(0).strip()
|
||||||
if not original or original in mapping:
|
if not original or original in mapping:
|
||||||
continue
|
continue
|
||||||
|
mapping[original] = _next_token("company", counters)
|
||||||
|
|
||||||
mapping[original] = generate_company(original)
|
# ── ФИО ──
|
||||||
|
|
||||||
# ── ФИО (Фамилия И.О.) ──
|
|
||||||
for match in PERSON_PATTERN.finditer(text):
|
for match in PERSON_PATTERN.finditer(text):
|
||||||
original = match.group(0).strip()
|
original = match.group(0).strip()
|
||||||
if not original or original in mapping:
|
if not original or original in mapping:
|
||||||
continue
|
continue
|
||||||
mapping[original] = generate_person(original)
|
mapping[original] = _next_token("person_name", counters)
|
||||||
|
|
||||||
|
|
||||||
def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str], llm_client) -> None:
|
def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str],
|
||||||
"""LLM NER для обнаружения имён, адресов, паспортных данных.
|
llm_client, counters: Dict[str, int]) -> None:
|
||||||
|
"""Универсальное LLM-обнаружение приватных данных.
|
||||||
|
|
||||||
Отправляет объединённый текст всех файлов в LLM, получает JSON-список
|
LLM САМА определяет типы. Замена — гибридные токены.
|
||||||
найденных сущностей, добавляет их в словарь замен.
|
|
||||||
|
|
||||||
Отправляются первые 3000 символов каждого файла (экономия токенов).
|
|
||||||
Общий размер промпта ограничен 8000 символами.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
all_texts: {filename: text_content} — тексты всех файлов
|
all_texts: {filename: text_content} — тексты всех файлов
|
||||||
mapping: Словарь замен (мутабельный, пополняется)
|
mapping: Словарь замен (мутабельный, пополняется)
|
||||||
llm_client: Объект с методом .complete(prompt) -> str
|
llm_client: Объект с методом .complete(prompt) -> str
|
||||||
|
counters: Глобальные счётчики токенов (мутабельный)
|
||||||
"""
|
"""
|
||||||
# Формируем комбинированный текст: имя файла + первые 3000 символов
|
# Формируем комбинированный текст: имя файла + первые 3000 символов
|
||||||
combined = "\n\n---FILE---\n\n".join(
|
combined = "\n\n---FILE---\n\n".join(
|
||||||
@@ -100,28 +128,21 @@ def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str], llm_client)
|
|||||||
|
|
||||||
prompt = (
|
prompt = (
|
||||||
"You are a PII (Personally Identifiable Information) detector for Russian legal documents.\n\n"
|
"You are a PII (Personally Identifiable Information) detector for Russian legal documents.\n\n"
|
||||||
|
"Find ALL sensitive or private information that is NOT already captured below.\n\n"
|
||||||
"STEP 1 — Identify the service provider:\n"
|
|
||||||
"Read the contract and find which company/person is labeled 'Исполнитель' "
|
|
||||||
"(the party providing services). "
|
|
||||||
"This party's own name is NOT PII — do NOT include it in results.\n\n"
|
|
||||||
|
|
||||||
"STEP 2 — Find all PII that is NOT already captured below.\n\n"
|
|
||||||
|
|
||||||
"Already captured by regex (skip these exact strings):\n"
|
"Already captured by regex (skip these exact strings):\n"
|
||||||
f"{already_found}\n\n"
|
f"{already_found}\n\n"
|
||||||
|
|
||||||
"Return a JSON array. Each item must have:\n"
|
"Return a JSON array. Each item must have:\n"
|
||||||
' - "type": MUST be one of: person_name, company, phone, email, address,\n'
|
' - "type": MUST be one of: person_name, company, phone, email, address,\n'
|
||||||
' inn, kpp, ogrn, bik, passport, contract_number, bank_account, other_id\n'
|
' inn, kpp, ogrn, bik, passport, bank_account, contract_number, other_id\n'
|
||||||
' Use "other_id" only if nothing else fits. If unsure — do NOT include.\n'
|
' Use "other_id" only if nothing else fits. If unsure — do NOT include.\n'
|
||||||
' - "value": the EXACT string from the text (copy verbatim)\n\n'
|
' - "value": the EXACT string from the text (copy verbatim)\n\n'
|
||||||
|
|
||||||
"NOT PII — do NOT include these (legal roles and terms, not private data):\n"
|
"NOT PII — do NOT include these (legal terms, not private data):\n"
|
||||||
"- Roles: Исполнитель, Заказчик, Стороны, Сторона, Покупатель, Поставщик\n"
|
"- Roles: Исполнитель, Заказчик, Стороны, Сторона, Покупатель, Поставщик\n"
|
||||||
"- Titles: Генеральный директор, Директор, Президент, действующего на основании\n"
|
"- Titles: Генеральный директор, Директор, действующего на основании\n"
|
||||||
"- Legal terms: Услуги, Договор, Приложение, Спецификация, НДС, Устава\n"
|
"- Legal terms: Услуги, Договор, Приложение, Спецификация, НДС, Устава\n\n"
|
||||||
"- The 'Исполнитель' company name identified in STEP 1\n\n"
|
|
||||||
|
|
||||||
"Rules:\n"
|
"Rules:\n"
|
||||||
"- Copy value VERBATIM — same spaces, punctuation, case as in text\n"
|
"- Copy value VERBATIM — same spaces, punctuation, case as in text\n"
|
||||||
@@ -152,10 +173,7 @@ def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str], llm_client)
|
|||||||
|
|
||||||
# Тип — любой (LLM сама придумала)
|
# Тип — любой (LLM сама придумала)
|
||||||
ent_type = ent.get("type", "").strip().lower().replace(" ", "_")
|
ent_type = ent.get("type", "").strip().lower().replace(" ", "_")
|
||||||
|
mapping[val] = _next_token(ent_type, counters)
|
||||||
# Генератор: специализированный (если тип совпал) или fallback
|
|
||||||
generator = _get_generator(ent_type)
|
|
||||||
mapping[val] = generator(val)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("LLM NER failed: %s", e)
|
log.warning("LLM NER failed: %s", e)
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ if _sys_path_root not in sys.path:
|
|||||||
sys.path.insert(0, _sys_path_root)
|
sys.path.insert(0, _sys_path_root)
|
||||||
|
|
||||||
# Версия приложения (меняется при изменениях)
|
# Версия приложения (меняется при изменениях)
|
||||||
VERSION = "4.0.2"
|
VERSION = "5.0.0"
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
|
|||||||
Reference in New Issue
Block a user