From 1595acb4bb8ade306cb1bf45705a171004a4e0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=A5=D1=80?= =?UTF-8?q?=D0=B0=D0=BC=D0=BE=D0=B2?= Date: Sun, 19 Oct 2025 13:08:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BE=D0=B1=D0=BD=D0=B0=D1=80=D1=83=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BC=D0=B0=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bad_words.py | 183 +++++++++++++++++++++++++++++++--------- src/data/bad_words.json | 131 +++++++++++++++++++--------- 2 files changed, 235 insertions(+), 79 deletions(-) diff --git a/src/bad_words.py b/src/bad_words.py index cc59c56..cf54445 100644 --- a/src/bad_words.py +++ b/src/bad_words.py @@ -4,6 +4,7 @@ import json import os import logging +import re logger = logging.getLogger(__name__) @@ -13,22 +14,47 @@ 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 + 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) - _bad_words_cache = data.get('bad_words', []) - _exceptions_cache = data.get('exceptions', []) - logger.info(f"Загружено {len(_bad_words_cache)} бранных слов и {len(_exceptions_cache)} исключений") + + # Проверяем, какой формат используется + 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} не найден") @@ -93,6 +119,37 @@ def reload_words(): _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() @@ -100,6 +157,12 @@ def contains_bad_word(text: str) -> bool: """ Проверяет, содержит ли текст бранные слова. + Использует: + - Нормализацию текста для обхода обфускации + - Проверку границ слов для whole_word паттернов + - Проверку подстрок для contains паттернов + - Список исключений + Args: text: Текст для проверки @@ -109,40 +172,56 @@ def contains_bad_word(text: str) -> bool: if not text: return False - # Приводим к нижнему регистру для проверки - text_lower = text.lower() + # Нормализуем текст (обход обфускации) + normalized_text = normalize_text(text) - # Получаем актуальные списки из кэша - bad_words = get_bad_words() - exceptions = get_exceptions() + # Получаем паттерны и исключения + global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache - # Проверяем бранные слова - for bad_word in bad_words: - if bad_word in text_lower: - # Проверяем, не является ли это слово частью исключения - # Ищем все вхождения плохого слова + # Если кэш не загружен, загружаем + 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 = text_lower.find(bad_word, start) + pos = normalized_text.find(pattern, start) if pos == -1: break - # Проверяем, входит ли это вхождение в какое-либо исключение + # Проверяем, не входит ли в исключение is_exception = False for exception in exceptions: - # Проверяем, находится ли плохое слово внутри слова-исключения - # и содержится ли это слово-исключение в тексте в этой позиции - if bad_word in exception: - # Ищем позицию исключения, которое могло бы содержать это плохое слово - exc_start = text_lower.find(exception, max(0, pos - len(exception))) + 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 @@ -154,6 +233,12 @@ def get_bad_words_from_text(text: str) -> list: """ Возвращает список найденных бранных слов в тексте. + Использует: + - Нормализацию текста для обхода обфускации + - Проверку границ слов для whole_word паттернов + - Проверку подстрок для contains паттернов + - Список исключений + Args: text: Текст для проверки @@ -163,36 +248,58 @@ def get_bad_words_from_text(text: str) -> list: if not text: return [] - text_lower = text.lower() + # Нормализуем текст (обход обфускации) + normalized_text = normalize_text(text) found_words = [] - # Получаем актуальные списки из кэша - bad_words = get_bad_words() - exceptions = get_exceptions() + # Получаем паттерны и исключения + global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache - # Ищем бранные слова - for bad_word in bad_words: - if bad_word in text_lower: - # Проверяем, не является ли это слово частью исключения + # Если кэш не загружен, загружаем + 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 = text_lower.find(bad_word, start) + pos = normalized_text.find(pattern, start) if pos == -1: break - # Проверяем, входит ли это вхождение в какое-либо исключение + # Проверяем, не входит ли в исключение is_exception = False for exception in exceptions: - if bad_word in exception: - exc_start = text_lower.find(exception, max(0, pos - len(exception))) + 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 @@ -200,7 +307,7 @@ def get_bad_words_from_text(text: str) -> list: start = pos + 1 # Добавляем слово только если оно действительно найдено (не в исключении) - if word_is_valid: - found_words.append(bad_word) + if word_is_valid and pattern not in found_words: + found_words.append(pattern) return found_words \ No newline at end of file diff --git a/src/data/bad_words.json b/src/data/bad_words.json index 1071886..eecac7f 100644 --- a/src/data/bad_words.json +++ b/src/data/bad_words.json @@ -1,47 +1,96 @@ { - "bad_words": [ - "хуй", "хуе", "хуи", "хую", "хуя", "хер", - "пизд", "пизж", "пезд", - "ебал", "ебан", "ебат", "ебу", "ебош", "ебля", "ебет", - "бля", "блядь", "блять", - "сука", "суки", "сучк", "сучар", - "мудак", "мудил", "муди", - "гандон", - "даун", - "дебил", - "долбоеб", "долбаеб", - "уебан", "уебок", - "хуесос", - "пидор", "пидар", "педик", "педр", - "гей", "гомик", "гомос", - "шлюх", "шалав", - "еблан", - "говн", - "срать", "сраль", "серун", - "дрочи", "дроч", - "жоп", "жёп", - "залуп", - "мразь", "мраз", - "козел", "козл", - "урод", "урода", - "ублюдо", "ублюд", - "тварь", "твар", - "падла", - "сволочь", "сволоч", - "гнида", "гнид", - "выблядо", - "хуета", "хуйн", - "охуе", "охуи", "охуя", - "нахуй", "нахер", - "похуй", "похер", - "захуя", - "ахуе", - "впизду", - "попизд" - ], + "patterns": { + "whole_word": [ + "гей", + "гомик", + "гомос", + "даун", + "дебил", + "гандон", + "мразь", + "мраз", + "козел", + "козл", + "урод", + "урода", + "тварь", + "твар", + "падла", + "гнида", + "гнид" + ], + "contains": [ + "хуй", + "хуе", + "хуи", + "хую", + "хуя", + "хер", + "пизд", + "пизж", + "пезд", + "ебал", + "ебан", + "ебат", + "ебу", + "ебош", + "ебля", + "ебет", + "бля", + "блядь", + "блять", + "сука", + "суки", + "сучк", + "сучар", + "мудак", + "мудил", + "муди", + "долбоеб", + "долбаеб", + "уебан", + "уебок", + "хуесос", + "пидор", + "пидар", + "педик", + "педр", + "шлюх", + "шалав", + "еблан", + "говн", + "срать", + "сраль", + "серун", + "дрочи", + "дроч", + "жоп", + "жёп", + "залуп", + "ублюдо", + "ублюд", + "сволочь", + "сволоч", + "выблядо", + "хуета", + "хуйн", + "охуе", + "охуи", + "охуя", + "нахуй", + "нахер", + "похуй", + "похер", + "захуя", + "ахуе", + "впизду", + "попизд" + ] + }, "exceptions": [ "республика", "документ", "документы" - ] + ], + "_comment": "whole_word - только целые слова (не часть другого слова), contains - любое вхождение подстроки" } \ No newline at end of file