""" Утилиты для обработки и нормализации текста. Используется для обнаружения спама и обхода фильтров. Pipeline обработки текста: 1. unicode_to_ascii() - замена Unicode-символов 2. normalize_text() - латиница → кириллица, удаление диакритики 3. clean_separators() - удаление разделителей ("г е й" → "гей") 4. get_lemma() - получение нормальной формы слова """ import re import unicodedata from typing import Set, List from pymorphy3 import MorphAnalyzer from configs.mapping import UNICODE_MAP, LATIN_TO_CYRILLIC, CYRILLIC_NORMALIZE __all__ = ( "unicode_to_ascii", "normalize_text", "clean_separators", "process_text", "get_lemma", "get_inflected_forms", "morph", "extract_words" ) # Глобальный экземпляр морфоанализатора (инициализируется один раз) morph = MorphAnalyzer() def unicode_to_ascii(text: str) -> str: """ Преобразует Unicode-символы в ASCII/кириллические аналоги. Args: text: Текст с Unicode-символами Returns: str: Текст с нормализованными символами Examples: >> unicode_to_ascii("privet") "привет" >> unicode_to_ascii("κупиτь") "купить" >> unicode_to_ascii("𝐡𝐞𝐥𝐥𝐨") "нелло" """ return ''.join(UNICODE_MAP.get(char, char) for char in text) def normalize_text(text: str) -> str: """ Нормализует текст для обхода фильтров: 1. Удаляет диакритические знаки (é → e, ė → e) 2. Заменяет латинские буквы на кириллические 3. Заменяет похожие кириллические буквы (укр/бел) на русские Args: text: Исходный текст Returns: str: Нормализованный текст Examples: >> normalize_text("prívét") "привет" >> normalize_text("hеllo") # h - кириллическая "нелло" >> normalize_text("Київ") # і → и "Киев" """ # Шаг 1: Удаляем диакритические знаки (акценты) # NFD разбивает символ на базовый + диакритику text = unicodedata.normalize('NFD', text) # Mn = Mark, Nonspacing (диакритические знаки) text = ''.join(char for char in text if unicodedata.category(char) != 'Mn') # Возвращаем в NFC (композитная форма) text = unicodedata.normalize('NFC', text) # Шаг 2: Заменяем латинские → кириллица и нормализуем кириллицу result: List[str] = [] for char in text: # Сначала латиница → кириллица if char in LATIN_TO_CYRILLIC: result.append(LATIN_TO_CYRILLIC[char]) # Потом нормализуем кириллицу (укр/бел → рус) elif char in CYRILLIC_NORMALIZE: result.append(CYRILLIC_NORMALIZE[char]) else: result.append(char) return ''.join(result) def clean_separators(text: str) -> str: """ Удаляет разделители между буквами для обнаружения обхода через пробелы/символы. Args: text: Исходный текст Returns: str: Текст без разделителей между буквами Examples: >> clean_separators("г е й") "гей" >> clean_separators("г.е.й") "гей" >> clean_separators("г*е*й") "гей" >> clean_separators("к у п и т ь") "купить" >> clean_separators("нормальный текст тут") "нормальный текст тут" """ # Удаляем все НЕ буквенно-цифровые символы, кроме пробелов cleaned: str = re.sub(r'[^\w\s]', '', text, flags=re.UNICODE) # Убираем множественные пробелы cleaned = re.sub(r'\s+', ' ', cleaned) # Убираем пробелы между отдельными буквами # "г е й" → "гей", но "нормальный текст" остаётся words = cleaned.split() result: List[str] = [] temp_chars: List[str] = [] for word in words: if len(word) == 1: # Одиночный символ - копим temp_chars.append(word) else: # Полное слово - сначала сбрасываем накопленные символы if temp_chars: result.append(''.join(temp_chars)) temp_chars = [] result.append(word) # Не забываем остаток if temp_chars: result.append(''.join(temp_chars)) return ' '.join(result) def process_text(text: str, remove_spaces: bool = False) -> str: """ Полный пайплайн обработки текста для спам-фильтра. Args: text: Исходный текст remove_spaces: Удалить все пробелы (для проверки part-слов) Returns: str: Обработанный текст в нижнем регистре Examples: >> process_text("Κупи*τь сейчас!") "купить сейчас" >> process_text("г е й", remove_spaces=True) "гей" """ # Приводим к нижнему регистру text = text.casefold() # Шаг 1: Unicode → ASCII/кириллица text = unicode_to_ascii(text) # Шаг 2: Нормализация (латиница → кириллица, диакритика) text = normalize_text(text) # Шаг 3: Удаление разделителей text = clean_separators(text) # Опционально: удаляем все пробелы (для part-проверки) if remove_spaces: text = re.sub(r'\s+', '', text) return text def get_lemma(word: str) -> str: """ Получает нормальную форму слова (лемму). Args: word: Слово для анализа Returns: str: Лемма (нормальная форма) Examples: >> get_lemma("купил") "купить" >> get_lemma("карты") "карта" >> get_lemma("хочется") "хотеться" """ try: parsed = morph.parse(word)[0] return parsed.normal_form except (IndexError, Exception): return word def get_inflected_forms(base_word: str, limit: int = 50) -> Set[str]: """ Получает все словоформы слова через морфологический анализ. Args: base_word: Исходное слово limit: Максимальное количество форм (для экономии памяти) Returns: Set[str]: Набор всех словоформ (падежи, числа и т.д.) Examples: >> get_inflected_forms("купить") {'купить', 'куплю', 'купишь', 'купит', ...} >> get_inflected_forms("карта") {'карта', 'карты', 'карте', 'карту', ...} """ try: parsed = morph.parse(base_word)[0] forms: Set[str] = set() for form in parsed.lexeme: if len(forms) >= limit: break forms.add(form.normal_form) forms.add(form.word) return forms except Exception: return {base_word} def extract_words(text: str) -> List[str]: """ Извлекает слова из текста (только буквы). Args: text: Текст для обработки Returns: List[str]: Список слов Examples: >> extract_words("Привет, как дела?") ['Привет', 'как', 'дела'] """ return re.findall(r'\b\w+\b', text, flags=re.UNICODE) def calculate_similarity(text1: str, text2: str) -> float: """ Вычисляет схожесть двух текстов (простая метрика). Args: text1: Первый текст text2: Второй текст Returns: float: Коэффициент схожести (0.0 - 1.0) Examples: >> calculate_similarity("привет", "привет") 1.0 >> calculate_similarity("купить", "продать") 0.0 """ processed1 = process_text(text1) processed2 = process_text(text2) if processed1 == processed2: return 1.0 # Levenshtein distance (простой вариант) len1, len2 = len(processed1), len(processed2) if len1 == 0 or len2 == 0: return 0.0 # Считаем совпадающие символы matches = sum(1 for a, b in zip(processed1, processed2) if a == b) return matches / max(len1, len2)