Detected Profanity
Детектор мата для 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
- Разработка и публикация
- Лицензия
Features
| Возможность | Что делает |
|---|---|
| 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=['хуй']
Быстрый старт
Python API
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
CLI
# 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 фраз (см. Производительность).
RU — 14 примеров
| # | Тип | Ввод | Нормализован | Матчится как | Комментарий |
|---|---|---|---|---|---|
| 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 до маппинга |
EN — 10 примеров
| # | Тип | Ввод | Нормализован | Матчится как |
|---|---|---|---|---|
| 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 на текст.
API Reference
ProfanityDetector
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 / [].
Функциональный API (синглтон all)
| Символ | Тип | Описание |
|---|---|---|
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 |
Версия пакета |
normalizer — карты
| Константа | Содержимое |
|---|---|
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 , , , ) |
matcher.AhoCorasick
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] |
lexicon
| Символ | Описание |
|---|---|
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
Подстроки профлексики встречаются в нормальных словах — детектор фильтрует их до проверки паттернов.
| Токен | Почему в 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 --tags
pyproject.toml: build-system = hatchling, requires-python >=3.8, dependencies = [].
Лицензия
MIT — см. LICENSE.
Для модерации чатов, комментариев и UGC без ML и внешних API.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file detected_profanity-0.1.0.tar.gz.
File metadata
- Download URL: detected_profanity-0.1.0.tar.gz
- Upload date:
- Size: 63.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e738d9b1311d68b5ea66c2a3e609833dc8a638e3ca239d089f436585ccec245
|
|
| MD5 |
6a88452449f0a294feb070cce45a3a9f
|
|
| BLAKE2b-256 |
4216431236b65585d6e6adbe22915e7157c3d5cd0fae6bab9f50c5586345cdad
|
File details
Details for the file detected_profanity-0.1.0-py3-none-any.whl.
File metadata
- Download URL: detected_profanity-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4c78512d859c77f19e0e4f81ca44fcbdb0d3747b6d35d84603b2da9983889a4b
|
|
| MD5 |
14e9f00fba53ab0c16595e7f285ca520
|
|
| BLAKE2b-256 |
2d531d14cd893aeac11018da8dd22bd76568780dc8214e7b75b3142ef5a2ebe0
|