Улучшение обнаружения мата

This commit is contained in:
2025-10-19 13:08:26 +03:00
parent 44a8b54ddc
commit 1595acb4bb
2 changed files with 235 additions and 79 deletions

View File

@@ -4,6 +4,7 @@
import json import json
import os import os
import logging import logging
import re
logger = logging.getLogger(__name__) 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 _bad_words_cache = None
_exceptions_cache = None _exceptions_cache = None
_whole_word_patterns_cache = None
_contains_patterns_cache = None
def load_bad_words(): def load_bad_words():
""" """
Загружает список бранных слов из JSON файла. Загружает список бранных слов из JSON файла.
Поддерживает как новый формат (patterns), так и старый (bad_words).
Returns: Returns:
tuple: (список бранных слов, список исключений) tuple: (список бранных слов, список исключений)
""" """
global _bad_words_cache, _exceptions_cache global _bad_words_cache, _exceptions_cache, _whole_word_patterns_cache, _contains_patterns_cache
try: try:
with open(BAD_WORDS_FILE, 'r', encoding='utf-8') as f: with open(BAD_WORDS_FILE, 'r', encoding='utf-8') as f:
data = json.load(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 return _bad_words_cache, _exceptions_cache
except FileNotFoundError: except FileNotFoundError:
logger.error(f"Файл {BAD_WORDS_FILE} не найден") logger.error(f"Файл {BAD_WORDS_FILE} не найден")
@@ -93,6 +119,37 @@ def reload_words():
_exceptions_cache = None _exceptions_cache = None
return load_bad_words() 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() load_bad_words()
@@ -100,6 +157,12 @@ def contains_bad_word(text: str) -> bool:
""" """
Проверяет, содержит ли текст бранные слова. Проверяет, содержит ли текст бранные слова.
Использует:
- Нормализацию текста для обхода обфускации
- Проверку границ слов для whole_word паттернов
- Проверку подстрок для contains паттернов
- Список исключений
Args: Args:
text: Текст для проверки text: Текст для проверки
@@ -109,40 +172,56 @@ def contains_bad_word(text: str) -> bool:
if not text: if not text:
return False return False
# Приводим к нижнему регистру для проверки # Нормализуем текст (обход обфускации)
text_lower = text.lower() normalized_text = normalize_text(text)
# Получаем актуальные списки из кэша # Получаем паттерны и исключения
bad_words = get_bad_words() global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
exceptions = get_exceptions()
# Проверяем бранные слова # Если кэш не загружен, загружаем
for bad_word in bad_words: if _whole_word_patterns_cache is None:
if bad_word in text_lower: 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 start = 0
while True: while True:
pos = text_lower.find(bad_word, start) pos = normalized_text.find(pattern, start)
if pos == -1: if pos == -1:
break break
# Проверяем, входит ли это вхождение в какое-либо исключение # Проверяем, не входит ли в исключение
is_exception = False is_exception = False
for exception in exceptions: for exception in exceptions:
# Проверяем, находится ли плохое слово внутри слова-исключения if pattern in exception:
# и содержится ли это слово-исключение в тексте в этой позиции exc_start = normalized_text.find(exception, max(0, pos - len(exception)))
if bad_word in exception:
# Ищем позицию исключения, которое могло бы содержать это плохое слово
exc_start = text_lower.find(exception, max(0, pos - len(exception)))
if exc_start != -1: if exc_start != -1:
exc_end = exc_start + len(exception) exc_end = exc_start + len(exception)
# Если плохое слово находится внутри исключения
if exc_start <= pos < exc_end: if exc_start <= pos < exc_end:
is_exception = True is_exception = True
break break
# Если это не исключение, значит найдено плохое слово
if not is_exception: if not is_exception:
return True return True
@@ -154,6 +233,12 @@ def get_bad_words_from_text(text: str) -> list:
""" """
Возвращает список найденных бранных слов в тексте. Возвращает список найденных бранных слов в тексте.
Использует:
- Нормализацию текста для обхода обфускации
- Проверку границ слов для whole_word паттернов
- Проверку подстрок для contains паттернов
- Список исключений
Args: Args:
text: Текст для проверки text: Текст для проверки
@@ -163,36 +248,58 @@ def get_bad_words_from_text(text: str) -> list:
if not text: if not text:
return [] return []
text_lower = text.lower() # Нормализуем текст (обход обфускации)
normalized_text = normalize_text(text)
found_words = [] found_words = []
# Получаем актуальные списки из кэша # Получаем паттерны и исключения
bad_words = get_bad_words() global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
exceptions = get_exceptions()
# Ищем бранные слова # Если кэш не загружен, загружаем
for bad_word in bad_words: if _whole_word_patterns_cache is None:
if bad_word in text_lower: 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 start = 0
word_is_valid = False word_is_valid = False
while True: while True:
pos = text_lower.find(bad_word, start) pos = normalized_text.find(pattern, start)
if pos == -1: if pos == -1:
break break
# Проверяем, входит ли это вхождение в какое-либо исключение # Проверяем, не входит ли в исключение
is_exception = False is_exception = False
for exception in exceptions: for exception in exceptions:
if bad_word in exception: if pattern in exception:
exc_start = text_lower.find(exception, max(0, pos - len(exception))) exc_start = normalized_text.find(exception, max(0, pos - len(exception)))
if exc_start != -1: if exc_start != -1:
exc_end = exc_start + len(exception) exc_end = exc_start + len(exception)
if exc_start <= pos < exc_end: if exc_start <= pos < exc_end:
is_exception = True is_exception = True
break break
# Если найдено хотя бы одно вхождение, которое не является исключением
if not is_exception: if not is_exception:
word_is_valid = True word_is_valid = True
break break
@@ -200,7 +307,7 @@ def get_bad_words_from_text(text: str) -> list:
start = pos + 1 start = pos + 1
# Добавляем слово только если оно действительно найдено (не в исключении) # Добавляем слово только если оно действительно найдено (не в исключении)
if word_is_valid: if word_is_valid and pattern not in found_words:
found_words.append(bad_word) found_words.append(pattern)
return found_words return found_words

View File

@@ -1,47 +1,96 @@
{ {
"bad_words": [ "patterns": {
"хуй", "хуе", "хуи", "хую", "хуя", "хер", "whole_word": [
"пизд", "пизж", "пезд", "гей",
"ебал", "ебан", "ебат", "ебу", "ебош", "ебля", "ебет", "гомик",
"бля", "блядь", "блять", "гомос",
"сука", "суки", "сучк", "сучар", "даун",
"мудак", "мудил", "муди", "дебил",
"гандон", "гандон",
"даун", "мразь",
"дебил", "мраз",
"долбоеб", "долбаеб", "козел",
"уебан", "уебок", "козл",
"хуесос", "урод",
"пидор", "пидар", "педик", "педр", "урода",
"гей", "гомик", "гомос", "тварь",
"шлюх", "шалав", "твар",
"еблан", "падла",
"говн", "гнида",
"срать", "сраль", "серун", "гнид"
"дрочи", "дроч", ],
"жоп", "жёп", "contains": [
"залуп", "хуй",
"мразь", "мраз", "хуе",
"козел", "козл", "хуи",
"урод", "урода", "хую",
"ублюдо", "ублюд", "хуя",
"тварь", "твар", "хер",
"падла", "пизд",
"сволочь", "сволоч", "пизж",
"гнида", "гнид", "пезд",
"выблядо", "ебал",
"хуета", "хуйн", "ебан",
"охуе", "охуи", "охуя", "ебат",
"нахуй", "нахер", "ебу",
"похуй", "похер", "ебош",
"захуя", "ебля",
"ахуе", "ебет",
"впизду", "бля",
"попизд" "блядь",
], "блять",
"сука",
"суки",
"сучк",
"сучар",
"мудак",
"мудил",
"муди",
"долбоеб",
"долбаеб",
"уебан",
"уебок",
"хуесос",
"пидор",
"пидар",
"педик",
"педр",
"шлюх",
"шалав",
"еблан",
"говн",
"срать",
"сраль",
"серун",
"дрочи",
"дроч",
"жоп",
"жёп",
"залуп",
"ублюдо",
"ублюд",
"сволочь",
"сволоч",
"выблядо",
"хуета",
"хуйн",
"охуе",
"охуи",
"охуя",
"нахуй",
"нахер",
"похуй",
"похер",
"захуя",
"ахуе",
"впизду",
"попизд"
]
},
"exceptions": [ "exceptions": [
"республика", "республика",
"документ", "документ",
"документы" "документы"
] ],
"_comment": "whole_word - только целые слова (не часть другого слова), contains - любое вхождение подстроки"
} }