Утилита нормализация текста
This commit is contained in:
290
bot/special/text_processing.py
Normal file
290
bot/special/text_processing.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user