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

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 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

View File

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