From 370a869aef4335fd00dad744c9ee754d658eba24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 12 Jul 2026 14:30:12 +0400 Subject: [PATCH] =?UTF-8?q?v5.0.0:=20=D0=B3=D0=B8=D0=B1=D1=80=D0=B8=D0=B4?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D1=8B=20(?= =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=D0=BE=D0=B2=5F0001,=20=D0=9E=D0=9E?= =?UTF-8?q?=D0=9E=5F=D0=A2=D0=B5=D1=85=D0=BD=D0=BE=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D1=8F=5F0034),=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?generators/,=20checksum,=20random=5Futils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drhider/checksum.py | 69 ------------------- drhider/config.py | 28 ++++++++ drhider/generators/__init__.py | 37 ---------- drhider/generators/accounts.py | 49 -------------- drhider/generators/address.py | 25 ------- drhider/generators/bik.py | 26 ------- drhider/generators/company.py | 27 -------- drhider/generators/email.py | 23 ------- drhider/generators/fallback.py | 64 ------------------ drhider/generators/inn.py | 47 ------------- drhider/generators/kpp.py | 27 -------- drhider/generators/ogrn.py | 27 -------- drhider/generators/passport.py | 26 ------- drhider/generators/person.py | 26 ------- drhider/generators/phone.py | 22 ------ drhider/obfuscator.py | 6 +- drhider/random_utils.py | 33 --------- drhider/scanner.py | 120 +++++++++++++++++++-------------- site/app.py | 2 +- 19 files changed, 102 insertions(+), 582 deletions(-) delete mode 100644 drhider/checksum.py delete mode 100644 drhider/generators/__init__.py delete mode 100644 drhider/generators/accounts.py delete mode 100644 drhider/generators/address.py delete mode 100644 drhider/generators/bik.py delete mode 100644 drhider/generators/company.py delete mode 100644 drhider/generators/email.py delete mode 100644 drhider/generators/fallback.py delete mode 100644 drhider/generators/inn.py delete mode 100644 drhider/generators/kpp.py delete mode 100644 drhider/generators/ogrn.py delete mode 100644 drhider/generators/passport.py delete mode 100644 drhider/generators/person.py delete mode 100644 drhider/generators/phone.py delete mode 100644 drhider/random_utils.py diff --git a/drhider/checksum.py b/drhider/checksum.py deleted file mode 100644 index 733ef0f..0000000 --- a/drhider/checksum.py +++ /dev/null @@ -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) diff --git a/drhider/config.py b/drhider/config.py index 79c2c70..80451f7 100644 --- a/drhider/config.py +++ b/drhider/config.py @@ -86,3 +86,31 @@ RU_STREETS = [ # Фиктивные домены для генерации email FAKE_DOMAINS = ["example.ru", "mail.test", "company.local", "org.example.ru"] + +# ═══════════════════════════════════════════════════════════════════════════ +# Пулы для гибридных токенов (шаблон + номер) +# ═══════════════════════════════════════════════════════════════════════════ + +# Существительные для названий компаний (20) +COMPANY_NOUNS = [ + "Технология", "Прогресс", "Гарант", "Стандарт", "Импульс", + "Вектор", "Сфера", "Альянс", "Синтез", "Меридиан", + "Спектр", "Формат", "Модуль", "Ресурс", "Платформа", + "Система", "Комплекс", "Проект", "Решение", "Сервис", +] + +# Города для адресов (20) +CITY_POOL = [ + "Москва", "Санкт-Петербург", "Новосибирск", "Екатеринбург", + "Казань", "Нижний_Новгород", "Челябинск", "Самара", "Омск", + "Ростов-на-Дону", "Уфа", "Красноярск", "Воронеж", "Пермь", + "Волгоград", "Краснодар", "Саратов", "Тюмень", "Иркутск", "Хабаровск", +] + +# Улицы для адресов (20) +STREET_POOL = [ + "Ленина", "Мира", "Пушкина", "Гагарина", "Советская", + "Кирова", "Октябрьская", "Молодёжная", "Садовая", "Центральная", + "Школьная", "Лесная", "Полевая", "Строителей", "Набережная", + "Парковая", "Солнечная", "Зелёная", "Юбилейная", "Весенняя", +] diff --git a/drhider/generators/__init__.py b/drhider/generators/__init__.py deleted file mode 100644 index 042d3cd..0000000 --- a/drhider/generators/__init__.py +++ /dev/null @@ -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 diff --git a/drhider/generators/accounts.py b/drhider/generators/accounts.py deleted file mode 100644 index 70259ba..0000000 --- a/drhider/generators/accounts.py +++ /dev/null @@ -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) diff --git a/drhider/generators/address.py b/drhider/generators/address.py deleted file mode 100644 index ecdc733..0000000 --- a/drhider/generators/address.py +++ /dev/null @@ -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}" diff --git a/drhider/generators/bik.py b/drhider/generators/bik.py deleted file mode 100644 index 3af256d..0000000 --- a/drhider/generators/bik.py +++ /dev/null @@ -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) diff --git a/drhider/generators/company.py b/drhider/generators/company.py deleted file mode 100644 index a4bc310..0000000 --- a/drhider/generators/company.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Генератор фиктивных названий компаний. - -Формат: ООО/ЗАО/АО «СлучайноеСуществительное» -""" - -import random - - -def generate_company(_: str) -> str: - """Сгенерировать фиктивное название компании. - - Выбирает случайную организационно-правовую форму (ООО, ЗАО, АО) - и случайное существительное из словаря. - - Args: - _: Оригинальное название (игнорируется) - - Returns: - Строка вида «ООО «Технология»» - """ - forms = ["ООО", "ЗАО", "АО"] - nouns = [ - "Технология", "Прогресс", "Гарант", "Стандарт", "Импульс", - "Вектор", "Сфера", "Альянс", "Синтез", "Меридиан", "Спектр", "Формат", - ] - return f'{random.choice(forms)} «{random.choice(nouns)}»' diff --git a/drhider/generators/email.py b/drhider/generators/email.py deleted file mode 100644 index fd40eb7..0000000 --- a/drhider/generators/email.py +++ /dev/null @@ -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}" diff --git a/drhider/generators/fallback.py b/drhider/generators/fallback.py deleted file mode 100644 index 955dc6c..0000000 --- a/drhider/generators/fallback.py +++ /dev/null @@ -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) diff --git a/drhider/generators/inn.py b/drhider/generators/inn.py deleted file mode 100644 index c82aed5..0000000 --- a/drhider/generators/inn.py +++ /dev/null @@ -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) diff --git a/drhider/generators/kpp.py b/drhider/generators/kpp.py deleted file mode 100644 index 8d93ae4..0000000 --- a/drhider/generators/kpp.py +++ /dev/null @@ -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) diff --git a/drhider/generators/ogrn.py b/drhider/generators/ogrn.py deleted file mode 100644 index 51121be..0000000 --- a/drhider/generators/ogrn.py +++ /dev/null @@ -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) diff --git a/drhider/generators/passport.py b/drhider/generators/passport.py deleted file mode 100644 index 8179fb2..0000000 --- a/drhider/generators/passport.py +++ /dev/null @@ -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)}" diff --git a/drhider/generators/person.py b/drhider/generators/person.py deleted file mode 100644 index fa6b66d..0000000 --- a/drhider/generators/person.py +++ /dev/null @@ -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]}." diff --git a/drhider/generators/phone.py b/drhider/generators/phone.py deleted file mode 100644 index 5e273ec..0000000 --- a/drhider/generators/phone.py +++ /dev/null @@ -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)}" diff --git a/drhider/obfuscator.py b/drhider/obfuscator.py index 1a6cb6d..77410c7 100644 --- a/drhider/obfuscator.py +++ b/drhider/obfuscator.py @@ -44,6 +44,7 @@ class TwoPassObfuscator: """ self._mapping: Dict[str, str] = {} self._sorted_keys: List[str] = [] + self._counters: Dict[str, int] = {} # Глобальные счётчики токенов self._llm_client = llm_client # ═══════════════════════════════════════════════════════════════════ @@ -79,11 +80,11 @@ class TwoPassObfuscator: # Regex-сканирование (быстрое, локальное) if text and not text.startswith("[DOC binary"): - scanner.scan_regex(text, self._mapping) + scanner.scan_regex(text, self._mapping, self._counters) # LLM-сканирование (получает уже найденное regex'ом чтобы не дублировать) 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( @@ -122,6 +123,7 @@ class TwoPassObfuscator: # Очистка состояния (обфускатор может использоваться повторно) self._mapping.clear() self._sorted_keys.clear() + self._counters.clear() # ═══════════════════════════════════════════════════════════════════════ diff --git a/drhider/random_utils.py b/drhider/random_utils.py deleted file mode 100644 index ad87bfb..0000000 --- a/drhider/random_utils.py +++ /dev/null @@ -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)) diff --git a/drhider/scanner.py b/drhider/scanner.py index a2cce3b..615ecdc 100644 --- a/drhider/scanner.py +++ b/drhider/scanner.py @@ -5,90 +5,118 @@ 1. scan_regex — быстрое regex-обнаружение (телефоны, email, ИНН, счета, компании, ФИО) 2. scan_llm_ner — универсальное LLM-обнаружение (любые приватные данные) -LLM САМА определяет какие типы сущностей присутствуют в документе. -Никакого фиксированного списка типов. +Замена: гибридные токены — читаемый шаблон + уникальный номер. +Пример: Иванов_0001, ООО_Технология_0034, ул_Ленина_0015 """ import re import json +import random import logging from typing import Dict -from .config import ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN -from .generators import ENTITY_GENERATORS, FALLBACK_GENERATOR -from .generators.company import generate_company -from .generators.person import generate_person +from .config import ( + ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN, + RU_SURNAMES, COMPANY_NOUNS, CITY_POOL, STREET_POOL, +) 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: - entity_type: Строковый идентификатор типа (например, "phone", "person_name") + entity_type: Тип сущности (person_name, company, phone...) + counters: Глобальные счётчики {prefix_key: count} 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-паттернами, заполнить словарь замен. - Ищет в тексте: - - Сущности из ENTITY_PATTERNS (телефоны, email, ИНН, ОГРН, КПП, БИК, счета, паспорта) - - Названия компаний (кроме «НУБЕС» — это Исполнитель, не заменяем) - - ФИО в формате «Фамилия И.О.» - - Найденные значения добавляются в mapping: оригинал → фиктивное значение. - Если сущность уже есть в mapping — не перезаписываем (согласованность). + Замена: гибридные токены (шаблон + номер). Args: text: Текст документа для сканирования mapping: Словарь замен (мутабельный, пополняется) + counters: Глобальные счётчики токенов (мутабельный) """ - # ── Сущности по regex-паттернам (телефоны, email, реквизиты) ── + # ── Сущности по regex-паттернам ── for entity_type, pattern in ENTITY_PATTERNS.items(): for match in re.finditer(pattern, text, re.IGNORECASE | re.MULTILINE): original = match.group(0).strip() if not original or original in mapping: continue - - # Генератор: специализированный (если есть) или fallback - generator = _get_generator(entity_type) - mapping[original] = generator(original) + mapping[original] = _next_token(entity_type, counters) # ── Названия компаний ── for match in COMPANY_PATTERN.finditer(text): original = match.group(0).strip() if not original or original in mapping: continue + mapping[original] = _next_token("company", counters) - mapping[original] = generate_company(original) - - # ── ФИО (Фамилия И.О.) ── + # ── ФИО ── for match in PERSON_PATTERN.finditer(text): original = match.group(0).strip() if not original or original in mapping: 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: - """LLM NER для обнаружения имён, адресов, паспортных данных. +def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str], + llm_client, counters: Dict[str, int]) -> None: + """Универсальное LLM-обнаружение приватных данных. - Отправляет объединённый текст всех файлов в LLM, получает JSON-список - найденных сущностей, добавляет их в словарь замен. - - Отправляются первые 3000 символов каждого файла (экономия токенов). - Общий размер промпта ограничен 8000 символами. + LLM САМА определяет типы. Замена — гибридные токены. Args: all_texts: {filename: text_content} — тексты всех файлов mapping: Словарь замен (мутабельный, пополняется) llm_client: Объект с методом .complete(prompt) -> str + counters: Глобальные счётчики токенов (мутабельный) """ # Формируем комбинированный текст: имя файла + первые 3000 символов 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 = ( "You are a PII (Personally Identifiable Information) detector for Russian legal documents.\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" + "Find ALL sensitive or private information that is NOT already captured below.\n\n" "Already captured by regex (skip these exact strings):\n" f"{already_found}\n\n" "Return a JSON array. Each item must have:\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' ' - "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" - "- Titles: Генеральный директор, Директор, Президент, действующего на основании\n" - "- Legal terms: Услуги, Договор, Приложение, Спецификация, НДС, Устава\n" - "- The 'Исполнитель' company name identified in STEP 1\n\n" + "- Titles: Генеральный директор, Директор, действующего на основании\n" + "- Legal terms: Услуги, Договор, Приложение, Спецификация, НДС, Устава\n\n" "Rules:\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 сама придумала) ent_type = ent.get("type", "").strip().lower().replace(" ", "_") - - # Генератор: специализированный (если тип совпал) или fallback - generator = _get_generator(ent_type) - mapping[val] = generator(val) + mapping[val] = _next_token(ent_type, counters) except Exception as e: log.warning("LLM NER failed: %s", e) diff --git a/site/app.py b/site/app.py index 8bda095..f1a635f 100644 --- a/site/app.py +++ b/site/app.py @@ -20,7 +20,7 @@ if _sys_path_root not in sys.path: sys.path.insert(0, _sys_path_root) # Версия приложения (меняется при изменениях) -VERSION = "4.0.2" +VERSION = "5.0.0" def create_app():