forked from Muzifs/LGBot
Улучшение обнаружения мата
This commit is contained in:
183
src/bad_words.py
183
src/bad_words.py
@@ -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
|
@@ -1,47 +1,96 @@
|
|||||||
{
|
{
|
||||||
"bad_words": [
|
"patterns": {
|
||||||
"хуй", "хуе", "хуи", "хую", "хуя", "хер",
|
"whole_word": [
|
||||||
"пизд", "пизж", "пезд",
|
"гей",
|
||||||
"ебал", "ебан", "ебат", "ебу", "ебош", "ебля", "ебет",
|
"гомик",
|
||||||
"бля", "блядь", "блять",
|
"гомос",
|
||||||
"сука", "суки", "сучк", "сучар",
|
"даун",
|
||||||
"мудак", "мудил", "муди",
|
"дебил",
|
||||||
"гандон",
|
"гандон",
|
||||||
"даун",
|
"мразь",
|
||||||
"дебил",
|
"мраз",
|
||||||
"долбоеб", "долбаеб",
|
"козел",
|
||||||
"уебан", "уебок",
|
"козл",
|
||||||
"хуесос",
|
"урод",
|
||||||
"пидор", "пидар", "педик", "педр",
|
"урода",
|
||||||
"гей", "гомик", "гомос",
|
"тварь",
|
||||||
"шлюх", "шалав",
|
"твар",
|
||||||
"еблан",
|
"падла",
|
||||||
"говн",
|
"гнида",
|
||||||
"срать", "сраль", "серун",
|
"гнид"
|
||||||
"дрочи", "дроч",
|
],
|
||||||
"жоп", "жёп",
|
"contains": [
|
||||||
"залуп",
|
"хуй",
|
||||||
"мразь", "мраз",
|
"хуе",
|
||||||
"козел", "козл",
|
"хуи",
|
||||||
"урод", "урода",
|
"хую",
|
||||||
"ублюдо", "ублюд",
|
"хуя",
|
||||||
"тварь", "твар",
|
"хер",
|
||||||
"падла",
|
"пизд",
|
||||||
"сволочь", "сволоч",
|
"пизж",
|
||||||
"гнида", "гнид",
|
"пезд",
|
||||||
"выблядо",
|
"ебал",
|
||||||
"хуета", "хуйн",
|
"ебан",
|
||||||
"охуе", "охуи", "охуя",
|
"ебат",
|
||||||
"нахуй", "нахер",
|
"ебу",
|
||||||
"похуй", "похер",
|
"ебош",
|
||||||
"захуя",
|
"ебля",
|
||||||
"ахуе",
|
"ебет",
|
||||||
"впизду",
|
"бля",
|
||||||
"попизд"
|
"блядь",
|
||||||
],
|
"блять",
|
||||||
|
"сука",
|
||||||
|
"суки",
|
||||||
|
"сучк",
|
||||||
|
"сучар",
|
||||||
|
"мудак",
|
||||||
|
"мудил",
|
||||||
|
"муди",
|
||||||
|
"долбоеб",
|
||||||
|
"долбаеб",
|
||||||
|
"уебан",
|
||||||
|
"уебок",
|
||||||
|
"хуесос",
|
||||||
|
"пидор",
|
||||||
|
"пидар",
|
||||||
|
"педик",
|
||||||
|
"педр",
|
||||||
|
"шлюх",
|
||||||
|
"шалав",
|
||||||
|
"еблан",
|
||||||
|
"говн",
|
||||||
|
"срать",
|
||||||
|
"сраль",
|
||||||
|
"серун",
|
||||||
|
"дрочи",
|
||||||
|
"дроч",
|
||||||
|
"жоп",
|
||||||
|
"жёп",
|
||||||
|
"залуп",
|
||||||
|
"ублюдо",
|
||||||
|
"ублюд",
|
||||||
|
"сволочь",
|
||||||
|
"сволоч",
|
||||||
|
"выблядо",
|
||||||
|
"хуета",
|
||||||
|
"хуйн",
|
||||||
|
"охуе",
|
||||||
|
"охуи",
|
||||||
|
"охуя",
|
||||||
|
"нахуй",
|
||||||
|
"нахер",
|
||||||
|
"похуй",
|
||||||
|
"похер",
|
||||||
|
"захуя",
|
||||||
|
"ахуе",
|
||||||
|
"впизду",
|
||||||
|
"попизд"
|
||||||
|
]
|
||||||
|
},
|
||||||
"exceptions": [
|
"exceptions": [
|
||||||
"республика",
|
"республика",
|
||||||
"документ",
|
"документ",
|
||||||
"документы"
|
"документы"
|
||||||
]
|
],
|
||||||
|
"_comment": "whole_word - только целые слова (не часть другого слова), contains - любое вхождение подстроки"
|
||||||
}
|
}
|
Reference in New Issue
Block a user