291 lines
9.4 KiB
Python
291 lines
9.4 KiB
Python
"""
|
||
Утилиты для обработки и нормализации текста.
|
||
Используется для обнаружения спама и обхода фильтров.
|
||
|
||
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)
|