# Система управления бранными словами # Список слов хранится в 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() # Убираем распространенные символы обфускации между буквами # Заменяем последовательности: буква + [*._ ]+ + буква на буква+буква normalized = re.sub(r'([а-яё])[\*\.\-_\s]+([а-яё])', r'\1\2', normalized) # Убираем повторяющиеся буквы (более 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