Детектор мата для RU / EN / транслита. Без зависимостей.
Profanity detection for Russian, English and translit. Zero dependencies, pure Python.
NFKC · ё→е · leet / homoglyph · сепараторы · повторы 3+→2 · делеция / дубль · Aho-Corasick O(n+z) · allowlist
- Features
- Установка
- Быстрый старт
- Маскировки
- Архитектура
- Производительность
- Сравнение с альтернативами
- API Reference
- Allowlist
- Разработка и публикация
- Лицензия
| Возможность | Что делает |
|---|---|
| 3 языка | ru / en / translit (pizdec, hui, blyad) + all (по умолчанию) |
| Нормализация | NFKC (fullwidth → ascii, лигатуры), lower, ё→е, снятие диакритики |
| Leet / homoglyph | RU: 0→о, 3→з, 4→а, @→а, $→с, a→а, e→е, o→о, p→р · EN: 0→o, 1→l, !→i, ` |
| Сепараторы | 40+ символов: # * . _ - | / · • — + ZWS (, , , и др.) — вырезаются перед поиском |
| Повторы | бляяяя → бляя, fuuuuuck → fuuck — схлопывание 3+ → 2 |
| Делеция / дубль | пздец (пропуск), хууй (дубль) — варианты генерируются при сборке автомата |
| Границы слова | Проверка word-expand + allowlist — мандарин / assassin не триггерят |
| Aho-Corasick | O(n+z) поиск, ленивая сборка автоматов, thread-safe search |
| Zero deps | Только stdlib, Python 3.8+ |
| Транслит | Двойная проверка: lower + EN-leet нормализация |
pip install detected-profanityТребования: Python 3.8+, без зависимостей.
Из исходников:
git clone https://github.com/detected-profanity/detected-profanity
cd detected-profanity
pip install -e .
# с dev-зависимостями
pip install -e ".[dev]"Проверка:
python -c "from detected_profanity import contains_profanity; print(contains_profanity('привет'))"
# False
python -m detected_profanity "пошёл на хуй"
# profanity: YES matches=['хуй']from detected_profanity import ProfanityDetector, contains_profanity, detect, censor, normalize_text
# по умолчанию — все языки (ru + en + translit)
contains_profanity("Привет, как дела?") # False
contains_profanity("охуеть, это пиздец") # True
detect("охуеть, это пиздец") # ["охуеть", "пиздец"]
censor("ну ты мудак") # "ну ты *****"
censor("пошёл нахуй", repl="#") # "пошёл #####"
# нормализация раскрывает маскировку
normalize_text("бл@ть, 0хуеть") # "блать, охуеть" (@→а, 0→о)
contains_profanity("бл@ть") # True
# фильтр по языку
det_ru = ProfanityDetector(lang="ru")
det_ru.contains_profanity("hello fuck") # False — только RU
det_ru.detect("блядь и shit") # ["блядь"]
det_en = ProfanityDetector(lang="en")
det_en.contains_profanity("fuck") # True
det_en.contains_profanity("хуй") # False
# несколько языков
det = ProfanityDetector(languages=["ru", "en"])
det.contains_profanity("хуй") # True
det.contains_profanity("fuck") # True
# транслит
ProfanityDetector().contains_profanity("pizdec kak holodno") # True
ProfanityDetector(lang="translit").detect("ebat ti pidor") # ["ebat", "pidor"]
# без нормализации / без транслита
ProfanityDetector(use_normalization=False).contains_profanity("бл@ть") # False
ProfanityDetector(lang="ru", include_translit=False).contains_profanity("pizdec") # False
# allowlist — ложных срабатываний нет
contains_profanity("мандарин и assassin") # False
contains_profanity("художник") # False# one-shot
python -m detected_profanity "охуеть, это пиздец"
# profanity: YES matches=['охуеть', 'пиздец']
python -m detected_profanity --censor "ну ты мудак"
# ну ты *****
# разные языки
python -m detected_profanity --lang ru "fuck" # profanity: NO
python -m detected_profanity --lang en "fuck" # profanity: YES
# JSON + stdin (для пайпов и CI)
echo "pizdec" | python -m detected_profanity --json
# {"text": "pizdec\n", "has_profanity": true, "matches": ["pizdec"]}
python -m detected_profanity --censor --repl "#" --json --lang all "fuck you, блядь"
# REPL
python cli.py
# › привет, как дела? → not detected
# › на#хуя ты это сделал → detected
# › /exit — выход--help:
usage: detected_profanity [-h] [--lang {ru,en,translit,all}] [--censor] [--repl REPL]
[--json] [--no-normalize] [--no-translit] [-v] [text ...]
text Текст для проверки (если не указан — читается stdin)
--lang Язык лексикона (default: all)
--censor Вывести цензурированную версию
--repl REPL Символ-заменитель (default: "*")
--json JSON-вывод
--no-normalize Отключить NFKC/leet/homoglyph
--no-translit Не включать транслит
-v, --version
exit-коды: 0 = чисто, 1 = найден мат, 2 = ошибка ввода
Консольная команда (после pip install): detected-profanity "текст" — алиас к python -m detected_profanity.
Каждый пример ниже — contains_profanity(...) is True. Проверено на датасете 60 фраз (см. Производительность).
| # | Тип | Ввод | Нормализован | Матчится как | Комментарий |
|---|---|---|---|---|---|
| 1 | Сепаратор # |
на#хуя |
нахуя |
нахуя |
# ∈ SEPARATORS, вырезается |
| 2 | Сепаратор * |
х*й |
хй |
хй |
каноническая форма для х*й |
| 3 | Leet 0→о |
0хуеть |
охуеть |
охуеть |
LEET_MAP: 0→о |
| 4 | Leet @→а |
бл@ть |
блать |
блать |
@→а, форма блать в словаре |
| 5 | Сепараторы .. |
п..здец |
пздец |
пздец |
. — сепаратор, и — делеция |
| 6 | Пробелы | х у й |
хуй |
хуй |
пробел — сепаратор, поиск в stripped |
| 7 | Точки | б.л.я |
бля |
бля |
каждая . — сепаратор |
| 8 | Повторы 3+→2 |
бляяяя |
бляя |
бля |
collapse_repeats: (.)\1{2,}→\1\1 |
| 9 | Делеция | пздец |
пздец |
пздец |
_gen_variants удаляет 1 букву при len≥4 |
| 10 | Дефисы | х-у-й |
хуй |
хуй |
- — сепаратор |
| 11 | Homoglyph a→а |
блaть (лат. a) |
блать |
блать |
HOMOGLYPH_MAP: a→а |
| 12 | Fullwidth | blyad |
blyad → бляд |
бляд |
NFKC fullwidth → ascii → homoglyph |
| 13 | Смешанная | п#здец |
пздец |
пздец |
# + делеция |
| 14 | Регистр | ХУЙ / ХуЙ |
хуй |
хуй |
lower до маппинга |
| # | Тип | Ввод | Нормализован | Матчится как |
|---|---|---|---|---|
| 1 | Сепаратор * |
f*ck |
fck |
fuck (вариант делеции u) |
| 2 | Сепаратор * |
sh*t |
sht |
shit |
| 3 | Leet $→s |
a$$hole |
asshole |
asshole |
| 4 | Leet !→i |
b!tch |
bitch |
bitch |
| 5 | Leet @→a |
f@ck |
fack |
fuck (через stripped) |
| 6 | Пробелы | f u c k |
fuck |
fuck |
| 7 | Точки | b.i.t.c.h |
bitch |
bitch |
| 8 | Повторы | fuuuuuck |
fuuck |
fuck (вариант дубля) |
| 9 | Делеция | fck |
fck |
fuck |
| 10 | Leet 5→s |
5hit |
shit |
shit |
| Ввод | Матчится как | Исходник |
|---|---|---|
pizdec |
pizdec |
пиздец |
hui / huy / khuy |
hui |
хуй |
ebat / yebat |
ebat |
ебать |
blyad / blya |
blyad |
блядь |
pidor / pidaras |
pidor |
пидор |
suka |
suka |
сука |
Карты:
detected_profanity/normalizer.py—LEET_MAP,HOMOGLYPH_MAP,SKELETON_MAP,SEPARATORS(40+ символов включая ZWS,,,).
┌──────────────────────────────────────────────────┐
raw text │ ProfanityDetector │
"бл@ть, 0хуеть!"│ │
│ │ ┌──────────────┐ ┌──────────────────┐ │
▼ │ │ RU branch │ │ EN / translit │ │
┌──────────┐ │ │ normalize_ │ │ _en_normalize │ │
│ NFKC │────┼──►│ text (NFKC+ │ │ (NFKC+lower+ │ │
│ normalize│ │ │ lower+ё→е+ │ │ EN leet) │ │
└────┬─────┘ │ │ skeleton) │ │ │ │
│ │ └──────┬───────┘ └────────┬─────────┘ │
▼ │ │ │ │
┌──────────┐ │ ▼ ▼ │
│ lower │ │ collapse_repeats (.)\1{2,} → \1\1 │
└────┬─────┘ │ "бляяяя" → "бляя" / "fuuuuuck" → "fuuck" │
│ │ │ │ │
▼ │ ▼ ▼ │
┌──────────┐ │ ┌─────────────┐ ┌─────────────┐ │
│ ё → е │ │ │ collapsed │ │ collapsed │ │
└────┬─────┘ │ │ "блать, │ │ "fuck, │ │
│ │ │ охуеть" │ │ shit" │ │
▼ │ └──────┬──────┘ └──────┬──────┘ │
┌──────────────┐│ │ ┌───────────────────┘ │
│ skeleton map ││ │ │ _strip_with_map(SEPARATORS) │
│ 0→о,@→а,a→а ││ ┌───▼──▼───┐ │
└──────┬───────┘│ │ stripped │ без сепараторов + cmap │
│ │ │ "блать │ "fuckshit" + [pos map] │
▼ │ │ охуеть" │ │
┌──────────────┐│ └────┬─────┘ │
│ collapsed ││ │ │
│ 3+ → 2 ││ ┌─────▼──────┐ │
└──────┬───────┘│ │ Aho-Corasick│ O(n+z) scan │
│ │ │ build: O(N)│ N = Σ len(patterns) │
▼ │ │ search: O(n+z)│ n=len(text), z=matches │
┌──────────────┐│ └─────┬──────┘ │
│ stripped ││ │ collapsed + stripped │
│ + cmap ││ │ (два прохода) │
└──────┬───────┘│ ▼ │
│ │ ┌──────────┐ word-expand + allowlist │
▼ │ │ filter │ is_allowed(word) │
┌────────┐ │ └────┬─────┘ len 2/3 → strict │
│ Aho AC │ │ │ │
└────┬───┘ │ ▼ │
│ │ contains → bool / detect → list / censor │
▼ │ │
┌─────────┐ │ │
│ filter │ └──────────────────────────────────────────────────┘
└─────────┘
| # | RU ветка | EN / translit ветка | Сложность |
|---|---|---|---|
| 1 | unicodedata.normalize('NFKC', text) |
то же | O(n) |
| 2 | lower() + ё→е |
lower() + _EN_LEET_MAP |
O(n) |
| 3 | SKELETON_MAP (0→о, @→а, a→а …) |
_EN_LEET_MAP (0→o, @→a, !→i …) |
O(n) |
| 4 | collapse_repeats |
то же | O(n) |
| 5 | stripped + cmap (без SEPARATORS) |
то же | O(n) |
| 6 | _gen_variants при сборке (делеция len≥4, дубль len≥3) |
то же | O(P·L) один раз |
| 7 | AhoCorasick.search(collapsed) + search(stripped) |
то же | O(n+z) |
| 8 | _expand_word + is_allowed фильтрация |
то же | O(z·w) |
Паттерны расширяются на этапе сборки, поиск остаётся линейным. Автоматы ленивые (_get_ru_ac / _get_en_ac / _get_tr_ac) — строятся при первом вызове. search thread-safe (только чтение).
| Операция | Сложность | Примечание |
|---|---|---|
AhoCorasick.build |
O(N), N = Σ len(pattern) |
BFS по трие, один раз |
AhoCorasick.search |
O(n+z) |
n=len(text), z=совпадений |
AhoCorasick.search_iter |
O(n+z) streaming |
ленивый генератор |
normalize_text |
O(n) |
NFKC + lower + map |
contains_profanity |
O(n+z) |
нормализация + 2× Aho |
| Память | O(N) trie |
~500 паттернов с вариантами |
Линейный скан против O(n·m) наивного перебора — критично на длинных текстах и больших словарях.
Датасет: 60 фраз — 30 RU (10 clean + 10 open + 10 masked) + 30 EN (10 clean + 10 open + 10 masked). Запуск: python benchmarks/benchmark.py.
Benchmark: detected-profanity vs alternatives (60 фраз: 30 RU + 30 EN)
| Library | Status | Acc | Prec | Rec | F1 | Time(s) | ms/txt | TP | TN | FP | FN |
|---|---|---|---|---|---|---|---|---|---|---|---|
| detected-profanity | ok | 100.0% | 100.0% | 100.0% | 100.0% | 0.0316 | 0.53 | 40 | 20 | 0 | 0 |
| better-profanity | ok | 60.0% | 100.0% | 40.0% | 57.1% | 0.1354 | 2.26 | 16 | 20 | 0 | 24 |
| profanity-check | ok | 58.3% | 100.0% | 37.5% | 54.5% | 0.2000 | 3.33 | 15 | 20 | 0 | 25 |
| censure | skipped | — | — | — | — | — | — | — | — | — | — |
Детализация (только ok):
| Library | RU (30) | EN (30) | Clean (20) | Open (20) | Masked (20) |
|---|---|---|---|---|---|
| detected-profanity | 100.0% | 100.0% | 100.0% | 100.0% | 100.0% |
| better-profanity | 33.3% | 86.7% | 100.0% | 50.0% | 30.0% |
| profanity-check | 33.3% | 83.3% | 100.0% | 50.0% | 25.0% |
Итог:
- Best accuracy / F1 / recall / masked —
detected-profanity(100% на всех срезах) - Fastest —
detected-profanity(0.53 ms/текст, в 4–6× быстрее альтернатив) - Альтернативы проваливают RU (33.3%) и маскировки (25–30%)
Воспроизвести:
python benchmarks/benchmark.py --json benchmarks/results.json --png benchmarks/benchmark.png
python benchmarks/benchmark.py --no-plot
python benchmarks/benchmark.py --list-datasetОсновано на benchmarks/results.json (60 фраз, см. выше).
| Критерий | detected-profanity | better-profanity |
profanity-check |
censure |
|---|---|---|---|---|
| RU | Да (150+ форм, 40+ корней) | Нет (EN only) | Нет | Нет |
| Транслит | Да (100+ форм) | Нет | Нет | Нет |
| Leet / homoglyph | Да | Частично | Нет | Нет |
| Сепараторы (40+) | Да | Нет | Нет | Нет |
| Повторы 3+→2 | Да | Нет | Нет | Нет |
| Делеция / дубль | Да | Нет | Нет | Нет |
| Allowlist | Да | Нет | Нет | Нет |
| Accuracy (60) | 100% | 60.0% | 58.3% | — |
| Masked (20) | 100% | 30.0% | 25.0% | — |
| RU accuracy (30) | 100% | 33.3% | 33.3% | — |
| Алгоритм | Aho-Corasick O(n+z) | Regex / list scan | ML (sklearn TF-IDF) | Regex |
| Зависимости | zero (stdlib) | zero | scikit-learn (~100 MB) |
zero |
| Скорость | 0.53 ms/txt | 2.26 ms/txt | 3.33 ms/txt | — |
| Censor | Да (repl) |
Да | Нет | Да |
| Лицензия | MIT | MIT | MIT | MIT |
Вердикт: единственная из проверенных библиотек, проходящая RU + маскировки без регресса на EN. Единственный zero-deps вариант с O(n+z) и <1 ms на текст.
ProfanityDetector(
lang: str | None = None, # "ru" | "en" | "translit" | "all" | None (all)
languages: str | list[str] | None = None, # альтернатива lang: ["ru", "en"]
*,
include_translit: bool = True, # включать транслит при ru/all
use_normalization: bool = True, # NFKC+leet+homoglyph (False → только lower)
)| Метод | Сигнатура | Описание |
|---|---|---|
contains_profanity |
(text: str) -> bool |
Есть ли мат |
detect |
(text: str) -> list[str] |
Список канонических форм |
censor |
(text: str, repl="*") -> str |
Цензурированная копия |
Бросает TypeError если text не str. Пустая строка → False / [].
| Символ | Тип | Описание |
|---|---|---|
contains_profanity(text) |
-> bool |
Шорткат через дефолтный детектор |
detect(text) |
-> list[str] |
Шорткат |
censor(text, repl="*") |
-> str |
Шорткат |
normalize_text(text) |
-> str |
NFKC + NFKD strip + lower + ё→е + SKELETON_MAP |
collapse_repeats(text) |
-> str |
aaa→aa (3+ → 2) |
__version__ |
str |
Версия пакета |
| Константа | Содержимое |
|---|---|
LEET_MAP |
0→о, 3→з, 4→а, 5→s, @→а, $→с, €→е |
HOMOGLYPH_MAP |
a→а, e→е, o→о, p→р, c→с, x→х, y→у, H→Н … |
SKELETON_MAP |
LEET_MAP ∪ HOMOGLYPH_MAP |
SEPARATORS |
set из 40+ сепараторов (включая ZWS , , , ) |
from detected_profanity.matcher import AhoCorasick, Match, find_matches
ac = AhoCorasick(["хуй", "пиздец", "fuck"])
ac.search("охуеть, fuck") # [(5, "хуй"), (12, "fuck")] — (end_idx, pattern)
ac.search_iter("длинный текст") # генератор
ac.find_matches("охуеть") # [Match(pattern="хуй", start=1, end=3)]
find_matches("text", ["bad", "word"])
len(ac) # число паттернов
"хуй" in ac # True| Метод | Сложность |
|---|---|
add_pattern(p) |
O(k), k=len(p) |
build() |
O(N) BFS, idempotent |
search(text) |
O(n+z) |
search_iter(text) |
O(n+z) streaming |
find_matches(text) |
O(n+z) → list[Match] |
| Символ | Описание |
|---|---|
ALL_RU_ROOTS (40+) |
Базовые корни |
RU_FORMS (150+) |
Кириллические формы |
EN_FORMS (100+) |
Английские формы |
TRANSLIT_FORMS (100+) |
Транслит-формы |
ALLOWLIST |
Белый список (set[str]) |
is_allowed(token) |
Проверка allowlist (нормализация + префикс len≥4) |
get_patterns(lang, include_translit) |
Скомпилированные re паттерны с \b |
Подстроки профлексики встречаются в нормальных словах — детектор фильтрует их до проверки паттернов.
| Токен | Почему в allowlist | Без фильтра |
|---|---|---|
мандарин, мандат, команда |
содержат манд |
FP на манда |
художник, художественный |
содержат ху |
FP на хуй |
бляшка, блок, бланк, облако |
содержат бля / бл |
FP на блядь |
страхование, подстраховать |
содержат страх |
FP |
assassin, assistant, classic, pass |
содержат ass |
FP на ass |
cocktail, cockpit, peacock |
содержат cock |
FP на cock |
shiitake |
содержит shit |
FP на shit |
Проверка — is_allowed(token): lower + ё→е + strip пунктуации, точное совпадение или префикс len≥4 (мандариновый → мандарин). Плюс word-expand в детекторе: совпадение отбрасывается, если слово-носитель целиком в allowlist.
Кастомизация — передайте свой allowlist через форк lexicon.py или фильтрацию результата detect().
git clone https://github.com/detected-profanity/detected-profanity
cd detected-profanity
pip install -e ".[dev]"
pytest -q # все тесты
pytest tests/test_detector.py -v # только детектор
python -m ruff check . # линтер
python -m mypy detected_profanity # типы (strict)Бенчмарк:
python benchmarks/benchmark.py
python benchmarks/benchmark.py --list-dataset
python benchmarks/benchmark.py --no-plot
cat benchmarks/results.json | python -m json.tool | head -n 60
open benchmarks/benchmark.pngСтруктура:
detected_profanity/
__init__.py — публичный API, __version__
detector.py — ProfanityDetector + Aho-Corasick wiring
normalizer.py — NFKC, LEET_MAP, HOMOGLYPH_MAP, SEPARATORS, collapse_repeats
matcher.py — AhoCorasick, Match, find_matches (pure Python, O(n+z))
lexicon.py — ALL_RU_ROOTS, RU/EN/TRANSLIT_FORMS, ALLOWLIST, is_allowed
__main__.py — python -m detected_profanity (argparse)
cli.py — интерактивный REPL
benchmarks/
benchmark.py — сравнение с better-profanity / profanity-check / censure
dataset.py — 60 фраз (RU/EN clean/profane/masked)
results.json — последний прогон
benchmark.png — график
tests/
test_detector.py / test_normalizer.py / test_matcher.py / test_cli.py
Требования к PR:
python -m ruff check .— 0 warningspython -m mypy detected_profanity— strict, безanypytest -q— зелёный- PR <200 строк
# версия — в pyproject.toml и detected_profanity/__init__.py (__version__)
# 1. bump версии, changelog, git tag
# 2. сборка
python -m build # hatchling → dist/*.whl + *.tar.gz
twine check dist/*
# 3. проверка в TestPyPI (опционально)
twine upload --repository testpypi dist/*
# 4. релиз
twine upload dist/*
git tag v1.0.0 && git push --tagspyproject.toml: build-system = hatchling, requires-python >=3.8, dependencies = [].
MIT — см. LICENSE.
Для модерации чатов, комментариев и UGC без ML и внешних API.
