Улучшение системы фильтрации матов

Добавлено:
- разделение словаря на базовый (в репозитории) и кастомный (добавления модераторов)
- поддержка английских бранных слов и их обфускации
- тесты для проверки ловли матов и ложных срабатываний
- автоматическая категоризация добавленных модераторами слов

Изменено:
- переработан механизм проверки текста на маты
- базовый словарь объединён с серверным и переведён в новый формат
- короткие слова теперь проверяются как начало слова, а не пропускаются

Удалено:
- повреждённая запись exception урод из базового словаря
This commit is contained in:
xpamych committed 2026-06-20 16:53:20 +03:00
1 parent b38c24f775
commit cfc52247a0
4 files changed
+942 -518

No files matched your search

+3
View File
@@ -28,6 +28,9 @@ bot.log
# Databases
users.db
# Custom moderator wordlist (generated at runtime)
src/data/bad_words_custom.json
# OS
.DS_Store
Thumbs.db
+611 -451
View File
File diff suppressed because it is too large. Load diff
+132 -67
View File
@@ -1,96 +1,148 @@
{
"patterns": {
"whole_word": [
"гандон",
"гей",
"гнид",
"гнида",
"гомик",
"гомос",
"даун",
"дебил",
"гандон",
"мразь",
"мраз",
"козел",
"козл",
"урод",
"урода",
"тварь",
"твар",
"мраз",
"мразь",
"падла",
"гнида",
"гнид"
"твар",
"тварь",
"урод",
"урода"
],
"word_start": [
"хуй",
"хуе",
"хуи",
"хую",
"хуя",
"хер",
"бля",
"пизд",
"пизж",
"пезд",
"ебал",
"ебан",
"ебат",
"ебош",
"ебля",
"ебет",
"ебля",
"ебош",
"ебут",
"ебуч",
"жоп",
"жёп"
"жёп",
"пезд",
"пизд",
"пизж",
"хер",
"хуе",
"хуи",
"хуй",
"хую",
"хуя"
],
"contains": [
"ахуе",
"блядь",
"блять",
"сука",
"суки",
"сучк",
"сучар",
"мудак",
"мудил",
"муди",
"долбоеб",
"долбаеб",
"уебан",
"уебок",
"хуесос",
"пидор",
"пидар",
"педик",
"педр",
"шлюх",
"шалав",
"еблан",
"говн",
"срать",
"сраль",
"серун",
"дрочи",
"дроч",
"залуп",
"ублюдо",
"ублюд",
"сволочь",
"сволоч",
"впизду",
"выблядо",
"хуета",
"хуйн",
"говн",
"долбаеб",
"долбоеб",
"дроч",
"дрочи",
"еблан",
"залуп",
"захуя",
"звиздюк",
"мудак",
"муди",
"мудил",
"нахер",
"нахуй",
"охуе",
"охуи",
"охуя",
"нахуй",
"нахер",
"похуй",
"педик",
"педр",
"пидар",
"пидор",
"попизд",
"похер",
"захуя",
"ахуе",
"впизду",
"попизд"
"похуй",
"сволоч",
"сволочь",
"серун",
"сирик",
"срака",
"сраль",
"срать",
"ссался",
"сука",
"суки",
"сучар",
"сучк",
"сытса",
"ублюд",
"ублюдо",
"уебан",
"уебок",
"хуесос",
"хуета",
"хуйн",
"шалав",
"шлюх"
],
"en": [
"asshole",
"assholes",
"bitch",
"bitches",
"bitchy",
"bliad",
"bliat",
"blya",
"blyat",
"ebal",
"ebany",
"ebat",
"fck",
"fuck",
"fucked",
"fucker",
"fucking",
"fucks",
"fuk",
"fuuk",
"gandon",
"gandona",
"hui",
"huilo",
"huy",
"mudak",
"mudila",
"nahui",
"pedik",
"pidor",
"pizda",
"pizdec",
"pizdets",
"pizdetsa",
"pizdu",
"pohui",
"shit",
"shitting",
"shitty",
"suchka",
"suka",
"suki",
"xyu",
"xyй"
]
},
"exceptions": [
"bloatware",
"аблятив",
"абляут",
"абляционный",
@@ -121,6 +173,7 @@
"гребу",
"гребут",
"даунгрейд",
"даунгрейт",
"догребать",
"документ",
"документы",
@@ -129,6 +182,7 @@
"дубляж",
"дубляжем",
"дубляжом",
"забляковать",
"завихрение",
"завихренние",
"заглублявший",
@@ -140,6 +194,7 @@
"зарыблять",
"застрахуйся",
"злоупотреблявший",
"злоупотреблял",
"злоупотреблять",
"злоупотребляющий",
"истреблявший",
@@ -165,6 +220,7 @@
"нагребать",
"наскребать",
"нахлебаться",
"небалансным",
"небудь",
"обляпавший",
"обляпался",
@@ -173,6 +229,8 @@
"обляпаться",
"обляпывать",
"обляпываться",
"облёк",
"облёкся",
"обособлявший",
"обособлявшийся",
"обособляйся",
@@ -229,12 +287,16 @@
"пособлявший",
"пособлять",
"пособляющий",
"потребление",
"потреблявший",
"потребляемый",
"потребляет",
"потребляла",
"потреблялась",
"потреблять",
"потребляться",
"потребляют",
"потребляющий",
"потребление",
"потребует",
"потребуется",
"похлебать",
@@ -281,6 +343,7 @@
"сиагейты",
"скоблящий",
"скоблящийся",
"скорблящий",
"скребу",
"скребут",
"слюбляться",
@@ -290,14 +353,15 @@
"стебать",
"стебля",
"схлебать",
"твар",
"требование",
"требовать",
"требует",
"требуем",
"требует",
"требуется",
"требуха",
"требую",
"требуют",
"требуха",
"углублявший",
"углублявшийся",
"углублять",
@@ -310,15 +374,15 @@
"уподобляться",
"уподобляющий",
"уподобляющийся",
"употребление",
"употреблявший",
"употреблявшийся",
"употребляемый",
"употреблять",
"употребляться",
"употребляю",
"употребляющий",
"употребляющийся",
"употребление",
"употребляю",
"усугублявший",
"усугублявшийся",
"усугубляемый",
@@ -328,9 +392,10 @@
"усугубляющийся",
"ущерблять",
"ущербляться",
"херметичность",
"хлебать",
"чебурнет",
"шоудаун",
"эчпочмак"
],
"_comment": "whole_word - только целые слова с границами \\b, word_start - только начало слова, contains - любое вхождение (мин. 4 буквы в коде)"
]
}
+196
View File
@@ -0,0 +1,196 @@
# Тесты для системы обнаружения бранных слов
# Запуск: python -m unittest tests.test_bad_words
import sys
import os
import unittest
# Добавляем src в путь для импорта
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from bad_words import contains_bad_word, get_bad_words_from_text, normalize_text
class TestNormalizeText(unittest.TestCase):
"""Тесты нормализации текста"""
def test_lowercase(self):
self.assertEqual(normalize_text('ХЕР'), 'хер')
def test_remove_yo(self):
self.assertEqual(normalize_text('хЁр'), 'хер')
def test_remove_obfuscation_ru(self):
self.assertEqual(normalize_text('х*е*р'), 'хер')
self.assertEqual(normalize_text('х.е.р'), 'хер')
self.assertEqual(normalize_text('х_е_р'), 'хер')
self.assertEqual(normalize_text('х-е-р'), 'хер')
def test_collapse_spaces_ru(self):
self.assertEqual(normalize_text('х е р'), 'хер')
self.assertEqual(normalize_text('х е р'), 'хер')
def test_collapse_repeated_letters_ru(self):
self.assertEqual(normalize_text('хххеееррр'), 'хер')
self.assertEqual(normalize_text('хууууй'), 'хуй')
class TestContainsBadWordPositive(unittest.TestCase):
"""Позитивные тесты: должны ловиться"""
def test_russian_short_word_start(self):
"""Короткие слова, которые являются началом слова"""
self.assertTrue(contains_bad_word('хер'))
self.assertTrue(contains_bad_word('херня'))
self.assertTrue(contains_bad_word('нахер'))
self.assertTrue(contains_bad_word('похер'))
self.assertTrue(contains_bad_word('херовый'))
self.assertTrue(contains_bad_word('бля'))
self.assertTrue(contains_bad_word('блядь'))
self.assertTrue(contains_bad_word('хуй'))
self.assertTrue(contains_bad_word('хуйло'))
self.assertTrue(contains_bad_word('жопа'))
def test_russian_contains(self):
"""Слова, содержащие бранные подстроки"""
self.assertTrue(contains_bad_word('сука'))
self.assertTrue(contains_bad_word('пиздец'))
self.assertTrue(contains_bad_word('ебать'))
self.assertTrue(contains_bad_word('говно'))
self.assertTrue(contains_bad_word('дрочить'))
self.assertTrue(contains_bad_word('пидор'))
self.assertTrue(contains_bad_word('мудак'))
self.assertTrue(contains_bad_word('шлюха'))
self.assertTrue(contains_bad_word('ублюдок'))
def test_russian_whole_word(self):
"""Точные слова-оскорбления"""
self.assertTrue(contains_bad_word('даун'))
self.assertTrue(contains_bad_word('дебил'))
self.assertTrue(contains_bad_word('урод'))
self.assertTrue(contains_bad_word('мразь'))
def test_english_bad_words(self):
"""Английские бранные слова"""
self.assertTrue(contains_bad_word('fuck'))
self.assertTrue(contains_bad_word('fucking'))
self.assertTrue(contains_bad_word('fucker'))
self.assertTrue(contains_bad_word('fuck you'))
self.assertTrue(contains_bad_word('shit'))
self.assertTrue(contains_bad_word('bitch'))
self.assertTrue(contains_bad_word('asshole'))
def test_obfuscated_english(self):
"""Обфусцированные английские слова"""
self.assertTrue(contains_bad_word('f*ck'))
self.assertTrue(contains_bad_word('f.u.c.k'))
self.assertTrue(contains_bad_word('f u c k'))
self.assertTrue(contains_bad_word('fuuuck'))
self.assertTrue(contains_bad_word('f-u-c-k'))
def test_transliterated_russian(self):
"""Русские маты, написанные латиницей"""
self.assertTrue(contains_bad_word('xyu'))
self.assertTrue(contains_bad_word('hui'))
self.assertTrue(contains_bad_word('xyй'))
self.assertTrue(contains_bad_word('pizdec'))
def test_mixed_text(self):
"""Бранные слова внутри обычного текста"""
self.assertTrue(contains_bad_word('Это просто херня какая-то'))
self.assertTrue(contains_bad_word('Ты что, дебил?'))
self.assertTrue(contains_bad_word('Он сказал fuck off'))
class TestContainsBadWordNegative(unittest.TestCase):
"""Негативные тесты: НЕ должны ловиться"""
def test_russian_exceptions_from_list(self):
"""Слова из списка исключений"""
self.assertFalse(contains_bad_word('аблятив'))
self.assertFalse(contains_bad_word('абляция'))
self.assertFalse(contains_bad_word('влюблять'))
self.assertFalse(contains_bad_word('гребля'))
self.assertFalse(contains_bad_word('даунгрейд'))
self.assertFalse(contains_bad_word('гейтвей'))
self.assertFalse(contains_bad_word('гейм'))
def test_normal_words_with_bad_substrings(self):
"""Обычные слова, содержащие бранные корни"""
self.assertFalse(contains_bad_word('херметичность'))
self.assertFalse(contains_bad_word('скорблящий'))
self.assertFalse(contains_bad_word('забляковать'))
self.assertFalse(contains_bad_word('обляпать'))
self.assertFalse(contains_bad_word('находится')) # содержит "хуй"? нет
self.assertFalse(contains_bad_word('класс')) # содержит "асс" - не мат
def test_english_normal_words(self):
"""Обычные английские слова"""
self.assertFalse(contains_bad_word('hello'))
self.assertFalse(contains_bad_word('assume'))
self.assertFalse(contains_bad_word('passion'))
self.assertFalse(contains_bad_word('class'))
self.assertFalse(contains_bad_word('association'))
self.assertFalse(contains_bad_word('document'))
def test_empty_and_short(self):
"""Пустой текст и короткие безобидные строки"""
self.assertFalse(contains_bad_word(''))
self.assertFalse(contains_bad_word('а'))
self.assertFalse(contains_bad_word('привет'))
class TestGetBadWordsFromText(unittest.TestCase):
"""Тесты функции получения списка найденных слов"""
def test_single_word(self):
found = get_bad_words_from_text('херня')
self.assertIn('хер', found)
def test_multiple_words(self):
found = get_bad_words_from_text('херня и fuck')
self.assertIn('хер', found)
self.assertIn('fuck', found)
def test_no_bad_words(self):
found = get_bad_words_from_text('абляция и документ')
self.assertEqual(found, [])
class TestEdgeCases(unittest.TestCase):
"""Дополнительные проверки на ложные срабатывания и сложные обходы"""
def test_english_word_boundaries(self):
"""Обычные английские слова не должны ловиться"""
self.assertFalse(contains_bad_word('class'))
self.assertFalse(contains_bad_word('passion'))
self.assertFalse(contains_bad_word('assignment'))
self.assertFalse(contains_bad_word('shifty')) # содержит shift, не shit
def test_russian_word_boundaries(self):
"""Обычные русские слова с бранными корнями"""
self.assertFalse(contains_bad_word('херметичность'))
self.assertFalse(contains_bad_word('скорблящий'))
self.assertFalse(contains_bad_word('забляковать'))
self.assertFalse(contains_bad_word('обляпать'))
def test_mixed_scripts(self):
"""Смешанные латиница и кириллица"""
self.assertTrue(contains_bad_word('xyй'))
self.assertTrue(contains_bad_word('ху й'))
def test_case_insensitive(self):
"""Регистронезависимость"""
self.assertTrue(contains_bad_word('FUCK'))
self.assertTrue(contains_bad_word('ХерНя'))
self.assertTrue(contains_bad_word('БЛЯ'))
def test_with_punctuation(self):
"""Бранные слова с пунктуацией"""
self.assertTrue(contains_bad_word('херня!'))
self.assertTrue(contains_bad_word('fuck!!!'))
self.assertTrue(contains_bad_word('ты, блядь, кто?'))
if __name__ == '__main__':
unittest.main()