Утилита нормализация текста

This commit is contained in:
2026-02-23 14:36:44 +07:00
parent dfd537b96a
commit b3ab19f94f

View 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("")
"привет"
>> 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)