Files
LGBot/src/bad_words.py
Евгений Храмов 4aba68d242 Исправление с границами слов
Исправление команды карма с юзернеймом
2025-10-19 19:50:32 +03:00

326 lines
13 KiB
Python
Raw 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.

# Система управления бранными словами
# Список слов хранится в JSON файле для возможности управления через команды
import json
import os
import logging
import re
logger = logging.getLogger(__name__)
# Путь к файлу с бранными словами
BAD_WORDS_FILE = os.path.join(os.path.dirname(__file__), 'data', 'bad_words.json')
# Кэш для загруженных слов
_bad_words_cache = None
_exceptions_cache = None
_whole_word_patterns_cache = None
_contains_patterns_cache = None
def load_bad_words():
"""
Загружает список бранных слов из JSON файла.
Поддерживает как новый формат (patterns), так и старый (bad_words).
Returns:
tuple: (список бранных слов, список исключений)
"""
global _bad_words_cache, _exceptions_cache, _whole_word_patterns_cache, _contains_patterns_cache
try:
with open(BAD_WORDS_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
# Проверяем, какой формат используется
if 'patterns' in data:
# Новый формат с паттернами
patterns = data['patterns']
_whole_word_patterns_cache = patterns.get('whole_word', [])
_contains_patterns_cache = patterns.get('contains', [])
_exceptions_cache = data.get('exceptions', [])
# Для обратной совместимости объединяем в один список
_bad_words_cache = _whole_word_patterns_cache + _contains_patterns_cache
logger.info(
f"Загружено паттернов: {len(_whole_word_patterns_cache)} whole_word, "
f"{len(_contains_patterns_cache)} contains, {len(_exceptions_cache)} исключений"
)
else:
# Старый формат для обратной совместимости
_bad_words_cache = data.get('bad_words', [])
_exceptions_cache = data.get('exceptions', [])
_whole_word_patterns_cache = []
_contains_patterns_cache = _bad_words_cache.copy()
logger.info(f"Загружено {len(_bad_words_cache)} бранных слов (старый формат) и {len(_exceptions_cache)} исключений")
return _bad_words_cache, _exceptions_cache
except FileNotFoundError:
logger.error(f"Файл {BAD_WORDS_FILE} не найден")
return [], []
except json.JSONDecodeError as e:
logger.error(f"Ошибка чтения JSON: {e}")
return [], []
def save_bad_words(bad_words: list, exceptions: list):
"""
Сохраняет список бранных слов в JSON файл.
Args:
bad_words: Список бранных слов
exceptions: Список исключений
Returns:
bool: True если успешно, иначе False
"""
global _bad_words_cache, _exceptions_cache
try:
# Создаем директорию, если её нет
os.makedirs(os.path.dirname(BAD_WORDS_FILE), exist_ok=True)
data = {
'bad_words': sorted(list(set(bad_words))), # Убираем дубликаты и сортируем
'exceptions': sorted(list(set(exceptions)))
}
with open(BAD_WORDS_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# Обновляем кэш
_bad_words_cache = data['bad_words']
_exceptions_cache = data['exceptions']
logger.info(f"Сохранено {len(_bad_words_cache)} бранных слов и {len(_exceptions_cache)} исключений")
return True
except Exception as e:
logger.error(f"Ошибка сохранения: {e}")
return False
def get_bad_words():
"""Возвращает список бранных слов (с кэшированием)"""
global _bad_words_cache
if _bad_words_cache is None:
load_bad_words()
return _bad_words_cache or []
def get_exceptions():
"""Возвращает список исключений (с кэшированием)"""
global _exceptions_cache
if _exceptions_cache is None:
load_bad_words()
return _exceptions_cache or []
def reload_words():
"""Перезагружает списки из файла (сбрасывает кэш)"""
global _bad_words_cache, _exceptions_cache
_bad_words_cache = None
_exceptions_cache = None
return load_bad_words()
def normalize_text(text: str) -> str:
"""
Нормализует текст для обхода обфускации.
Убирает:
- Звездочки, точки, подчеркивания между буквами (х*й, х.у.й, х_у_й → хуй)
- Повторяющиеся символы (хууууууй → хуй)
- ОДИНОЧНЫЕ пробелы между ОДИНОЧНЫМИ буквами (х у й → хуй, но "не бу" остаётся "не бу")
Args:
text: Исходный текст
Returns:
Нормализованный текст
"""
if not text:
return text
# Приводим к нижнему регистру
normalized = text.lower()
# Циклически убираем обфускацию, пока что-то меняется
max_iterations = 10
for _ in range(max_iterations):
before = normalized
# Убираем звёздочки, точки, дефисы, подчёркивания между буквами
# х*й, х.у.й, х_у_й → хуй
normalized = re.sub(r'([а-яё])[\*\.\-_]+([а-яё])', r'\1\2', normalized)
# Убираем ОДИНОЧНЫЕ пробелы между ОДИНОЧНЫМИ буквами (обфускация)
# "х у й" → "хуй", но "не бу" → "не бу" (не склеиваем обычные слова)
# Паттерн: одиночная буква + пробелы + одиночная буква
normalized = re.sub(r'\b([а-яё])\s+([а-яё])\b', r'\1\2', normalized)
if before == normalized:
break
# Убираем повторяющиеся буквы (более 2 подряд)
# хууууууй → хуй, пииииздец → пиздец
normalized = re.sub(r'([а-яё])\1{2,}', r'\1', normalized)
return normalized
# Предзагружаем слова при импорте модуля в кэш
load_bad_words()
def contains_bad_word(text: str) -> bool:
"""
Проверяет, содержит ли текст бранные слова.
Использует:
- Нормализацию текста для обхода обфускации
- Проверку границ слов для whole_word паттернов
- Проверку подстрок для contains паттернов
- Список исключений
Args:
text: Текст для проверки
Returns:
True, если найдено бранное слово, иначе False
"""
if not text:
return False
# Нормализуем текст (обход обфускации)
normalized_text = normalize_text(text)
# Получаем паттерны и исключения
global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
# Если кэш не загружен, загружаем
if _whole_word_patterns_cache is None:
load_bad_words()
whole_word_patterns = _whole_word_patterns_cache or []
contains_patterns = _contains_patterns_cache or []
exceptions = _exceptions_cache or []
# 1. Проверяем whole_word паттерны (только целые слова)
for pattern in whole_word_patterns:
# Используем границы слов \b для поиска только целых слов
regex = r'\b' + re.escape(pattern) + r'\b'
if re.search(regex, normalized_text, re.IGNORECASE):
# Проверяем, не входит ли в исключения
is_exception = False
for exception in exceptions:
if exception in normalized_text and pattern in exception:
is_exception = True
break
if not is_exception:
return True
# 2. Проверяем contains паттерны (любое вхождение)
for pattern in contains_patterns:
if pattern in normalized_text:
# Проверяем все вхождения паттерна
start = 0
while True:
pos = normalized_text.find(pattern, start)
if pos == -1:
break
# Проверяем, не входит ли в исключение
is_exception = False
for exception in exceptions:
if pattern in exception:
exc_start = normalized_text.find(exception, max(0, pos - len(exception)))
if exc_start != -1:
exc_end = exc_start + len(exception)
if exc_start <= pos < exc_end:
is_exception = True
break
if not is_exception:
return True
start = pos + 1
return False
def get_bad_words_from_text(text: str) -> list:
"""
Возвращает список найденных бранных слов в тексте.
Использует:
- Нормализацию текста для обхода обфускации
- Проверку границ слов для whole_word паттернов
- Проверку подстрок для contains паттернов
- Список исключений
Args:
text: Текст для проверки
Returns:
Список найденных бранных слов
"""
if not text:
return []
# Нормализуем текст (обход обфускации)
normalized_text = normalize_text(text)
found_words = []
# Получаем паттерны и исключения
global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
# Если кэш не загружен, загружаем
if _whole_word_patterns_cache is None:
load_bad_words()
whole_word_patterns = _whole_word_patterns_cache or []
contains_patterns = _contains_patterns_cache or []
exceptions = _exceptions_cache or []
# 1. Проверяем whole_word паттерны (только целые слова)
for pattern in whole_word_patterns:
# Используем границы слов \b для поиска только целых слов
regex = r'\b' + re.escape(pattern) + r'\b'
if re.search(regex, normalized_text, re.IGNORECASE):
# Проверяем, не входит ли в исключения
is_exception = False
for exception in exceptions:
if exception in normalized_text and pattern in exception:
is_exception = True
break
if not is_exception and pattern not in found_words:
found_words.append(pattern)
# 2. Проверяем contains паттерны (любое вхождение)
for pattern in contains_patterns:
if pattern in normalized_text:
# Проверяем все вхождения паттерна
start = 0
word_is_valid = False
while True:
pos = normalized_text.find(pattern, start)
if pos == -1:
break
# Проверяем, не входит ли в исключение
is_exception = False
for exception in exceptions:
if pattern in exception:
exc_start = normalized_text.find(exception, max(0, pos - len(exception)))
if exc_start != -1:
exc_end = exc_start + len(exception)
if exc_start <= pos < exc_end:
is_exception = True
break
if not is_exception:
word_is_valid = True
break
start = pos + 1
# Добавляем слово только если оно действительно найдено (не в исключении)
if word_is_valid and pattern not in found_words:
found_words.append(pattern)
return found_words