diff --git a/bot/special/text_processing.py b/bot/special/text_processing.py new file mode 100644 index 0000000..1bd52c6 --- /dev/null +++ b/bot/special/text_processing.py @@ -0,0 +1,290 @@ +""" +Утилиты для обработки и нормализации текста. +Используется для обнаружения спама и обхода фильтров. + +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)