Утилита нормализация текста
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