Files
PrimoGuardBot/bot/special/text_processing.py

291 lines
9.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Утилиты для обработки и нормализации текста.
Используется для обнаружения спама и обхода фильтров.
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("")
"привет"
>> 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)