Привет, Хабр.

Для одного из моих проектов мне понадобилось сделать фильтр мата. Сегодня мы попытаемся сделать его за несколько минут. Ну что же, приступим.

Анализирование


Сперва я должен бы как-то разбивать текст на части, чтобы потом сравнивать его с нецензурной лексикой. Решение нашлось очень просто. Я составил список запрещенных слов и стал проходится циклом по введенному тексту, разбивая его на куски размером с каждое запрещенное слово.

#Фраза, которую будем проверять.
phrase = input("Введите фразу для проверки: ")

#Искомое слово. Пока что только одно.
words = ["банан"]

#Фрагменты, которые получатся после разбиения слова.
fragments = []
#Проходимся по всем словам.
for word in words:
    #Разбиваем слово на части, и проходимся по ним.
    for part in range(len(phrase)):
        #Вот сам наш фрагмент.
        fragment = phrase[part: part+len(word)]
        #Сохраняем его в наш список.
        fragments.append(fragment)

#Выводим получившиеся фрагменты.
print(fragments)

Запускаем наш файл и вводим фразу.

Введите фразу для проверки: Привет, я банан.
['Приве', 'ривет', 'ивет,', 'вет, ', 'ет, я', 'т, я ', ', я б', ' я ба', 'я бан', ' бана', 'банан', 'анан.', 'нан.', 'ан.', 'н.', '.']

Вот что у нас получилось. Смотрим на все фрагменты и видим среди них искомое слово «банан». Теперь нам осталось сравнить эти фрагменты с искомыми словами.

Сравнение


Для того, чтобы сравнивать фрагменты с искомыми словами, я решил просто использовать цикл.


#Проходимся по всем словам.
for word in words:
    #Проходимся по всем фрагментам.
    for fragment in fragments:
        #Сравниваем фрагмент и искомое слово
        if word == fragment:
            #Если они равны, выводим надпись о их нахождении.
            print("Найдено", word)

Смотрим.

Введите фразу для проверки: Привет, я банан.
['Приве', 'ривет', 'ивет,', 'вет, ', 'ет, я', 'т, я ', ', я б', ' я ба', 'я бан', ' бана', 'банан', 'анан.', 'нан.', 'ан.', 'н.', '.']
Найдено банан

Все, простейший фильтр нецензурной лексики готов!

Доработки


Простейший фильтр был готов, но я решил его чуточку дополнить.

Так как русский человек очень изобретателен, то он может поменять некоторые буквы на другой язык. Например, «бaнaн». Здесь место обычной «а», я поставил английскую. И теперь наш фильтр не будет распознавать это слово.

Введите фразу для проверки: Привет, я бaнaн.
['Приве', 'ривет', 'ивет,', 'вет, ', 'ет, я', 'т, я ', ', я б', ' я бa', 'я бaн', ' бaнa', 'бaнaн', 'aнaн.', 'нaн.', 'aн.', 'н.', '.']

Фильтр не вывел ничего, а значит не нашел слово. Поэтому мы должны создать дополнительный фильтр, который будет переводить буквы английского алфавита и похожие символы в русский текст.

В интернете я нашел вот такой список, который чуточку доработал.

d =   {'а' : ['а', 'a', '@'],
  'б' : ['б', '6', 'b'],
  'в' : ['в', 'b', 'v'],
  'г' : ['г', 'r', 'g'],
  'д' : ['д', 'd', 'g'],
  'е' : ['е', 'e'],
  'ё' : ['ё', 'e'],
  'ж' : ['ж', 'zh', '*'],
  'з' : ['з', '3', 'z'],
  'и' : ['и', 'u', 'i'],
  'й' : ['й', 'u', 'i'],
  'к' : ['к', 'k', 'i{', '|{'],
  'л' : ['л', 'l', 'ji'],
  'м' : ['м', 'm'],
  'н' : ['н', 'h', 'n'],
  'о' : ['о', 'o', '0'],
  'п' : ['п', 'n', 'p'],
  'р' : ['р', 'r', 'p'],
  'с' : ['с', 'c', 's'],
  'т' : ['т', 'm', 't'],
  'у' : ['у', 'y', 'u'],
  'ф' : ['ф', 'f'],
  'х' : ['х', 'x', 'h' , '}{'],
  'ц' : ['ц', 'c', 'u,'],
  'ч' : ['ч', 'ch'],
  'ш' : ['ш', 'sh'],
  'щ' : ['щ', 'sch'],
  'ь' : ['ь', 'b'],
  'ы' : ['ы', 'bi'],
  'ъ' : ['ъ'],
  'э' : ['э', 'e'],
  'ю' : ['ю', 'io'],
  'я' : ['я', 'ya']
}

Перед фильтрацией мы должны перевести весь текст в нижний регистр и убрать все пробелы, так как кто-нибудь может ввести искомые слова вот так: «БАНАН» или «б а н а н».

phrase = phrase.lower().replace(" ", "")

Теперь мы должны как-то сравнить этот список с нашим текстом. Для этого я создал вот такую функцию.

#Проходимся по нашему словарю.
for key, value in d.items():
    #Проходимся по каждой букве в значении словаря. То есть по вот этим спискам ['а', 'a', '@'].
    for letter in value:
        #Проходимся по каждой букве в нашей фразе.
        for phr in phrase:
            #Если буква совпадает с буквой в нашем списке.
            if letter == phr:
                #Заменяем эту букву на ключ словаря.
                phrase = phrase.replace(phr, key)

Что у нас получилось теперь.

Введите фразу для проверки: Привет, я б@н@н.
['Приве', 'ривет', 'ивет,', 'вет, ', 'ет, я', 'т, я ', ', я б', ' я ба', 'я бан', ' бана', 'банан', 'анан.', 'нан.', 'ан.', 'н.', '.']
Найдено банан

То, что у нас не находилось раньше теперь легко распознаётся.

Расстояние Левенштейна


Я понял, что если хоть чуточку изменить слово, то его уже невозможно найти.

Введите фразу для проверки: Я люблю бонан.
['Я люб', ' любл', 'люблю', 'юблю ', 'блю б', 'лю бо', 'ю бон', ' бона', 'бонан', 'онан.', 'нан.', 'ан.', 'н.', '.']

Наши опасения подтвердились, но решения этой проблемы есть расстояние Левенштейна.

В интернете я нашел функцию этого алгоритма для python. Вот как она выглядит.

def distance(a, b): 
    "Calculates the Levenshtein distance between a and b."
    n, m = len(a), len(b)
    if n > m:
        # Make sure n <= m, to use O(min(n, m)) space
        a, b = b, a
        n, m = m, n

    current_row = range(n + 1)  # Keep current and previous row, not entire matrix
    for i in range(1, m + 1):
        previous_row, current_row = current_row, [i] + [0] * n
        for j in range(1, n + 1):
            add, delete, change = previous_row[j] + 1, current_row[j - 1] + 1, previous_row[j - 1]
            if a[j - 1] != b[i - 1]:
                change += 1
            current_row[j] = min(add, delete, change)

    return current_row[n]

Теперь мы должны переписать функцию сравнения.

#Проходимся по всем словам.
for word in words:
    #Проходимся по всем фрагментам.
    for fragment in fragments:
        #Если отличие этого фрагмента меньше или равно 25% этого слова, то считаем, что они равны.
        if distance(fragment, word) <= len(word)*0.25:
            #Если они равны, выводим надпись о их нахождении.
            print("Найдено", word)

Что у нас получилось теперь.

Введите фразу для проверки: Я люблю бонан.
['Я люб', ' любл', 'люблю', 'юблю ', 'блю б', 'лю бо', 'ю бон', ' бона', 'бонан', 'онан.', 'нан.', 'ан.', 'н.', '.']
Найдено банан


Небольшие проблемы


Наверное вы уже догадались, что если в списке для фильтрации будет больше одного слова, то он будет некорректно работать. Поэтому я переписал это всё в один цикл.

#Проходимся по всем словам.
for word in words:
    #Разбиваем слово на части, и проходимся по ним.
    for part in range(len(phrase)):
        #Вот сам наш фрагмент.
        fragment = phrase[part: part+len(word)]
        #Если отличие этого фрагмента меньше или равно 25% этого слова, то считаем, что они равны.
        if distance(fragment, word) <= len(word)*0.25:
            #Если они равны, выводим надпись о их нахождении.
            print("Найдено", word, "\nПохоже на", fragment)

Все, наш фильтр полностью готов.

Код фильтра


import string

words = ["банан", "помидор"]

print("Фильтруемые слова:", words)

#Фраза, которую будем проверять.
phrase = input("Введите фразу для проверки: ").lower().replace(" ", "")

def distance(a, b): 
    "Calculates the Levenshtein distance between a and b."
    n, m = len(a), len(b)
    if n > m:
        # Make sure n <= m, to use O(min(n, m)) space
        a, b = b, a
        n, m = m, n

    current_row = range(n + 1)  # Keep current and previous row, not entire matrix
    for i in range(1, m + 1):
        previous_row, current_row = current_row, [i] + [0] * n
        for j in range(1, n + 1):
            add, delete, change = previous_row[j] + 1, current_row[j - 1] + 1, previous_row[j - 1]
            if a[j - 1] != b[i - 1]:
                change += 1
            current_row[j] = min(add, delete, change)

    return current_row[n]

d =   {'а' : ['а', 'a', '@'],
  'б' : ['б', '6', 'b'],
  'в' : ['в', 'b', 'v'],
  'г' : ['г', 'r', 'g'],
  'д' : ['д', 'd'],
  'е' : ['е', 'e'],
  'ё' : ['ё', 'e'],
  'ж' : ['ж', 'zh', '*'],
  'з' : ['з', '3', 'z'],
  'и' : ['и', 'u', 'i'],
  'й' : ['й', 'u', 'i'],
  'к' : ['к', 'k', 'i{', '|{'],
  'л' : ['л', 'l', 'ji'],
  'м' : ['м', 'm'],
  'н' : ['н', 'h', 'n'],
  'о' : ['о', 'o', '0'],
  'п' : ['п', 'n', 'p'],
  'р' : ['р', 'r', 'p'],
  'с' : ['с', 'c', 's'],
  'т' : ['т', 'm', 't'],
  'у' : ['у', 'y', 'u'],
  'ф' : ['ф', 'f'],
  'х' : ['х', 'x', 'h' , '}{'],
  'ц' : ['ц', 'c', 'u,'],
  'ч' : ['ч', 'ch'],
  'ш' : ['ш', 'sh'],
  'щ' : ['щ', 'sch'],
  'ь' : ['ь', 'b'],
  'ы' : ['ы', 'bi'],
  'ъ' : ['ъ'],
  'э' : ['э', 'e'],
  'ю' : ['ю', 'io'],
  'я' : ['я', 'ya']
}

for key, value in d.items():
    #Проходимся по каждой букве в значении словаря. То есть по вот этим спискам ['а', 'a', '@'].
    for letter in value:
        #Проходимся по каждой букве в нашей фразе.
        for phr in phrase:
            #Если буква совпадает с буквой в нашем списке.
            if letter == phr:
                #Заменяем эту букву на ключ словаря.
                phrase = phrase.replace(phr, key)

#Проходимся по всем словам.
for word in words:
    #Разбиваем слово на части, и проходимся по ним.
    for part in range(len(phrase)):
        #Вот сам наш фрагмент.
        fragment = phrase[part: part+len(word)]
        #Если отличие этого фрагмента меньше или равно 25% этого слова, то считаем, что они равны.
        if distance(fragment, word) <= len(word)*0.25:
            #Если они равны, выводим надпись о их нахождении.
            print("Найдено", word, "\nПохоже на", fragment)

Комментарии (14)


  1. AlexTheCleaner
    30.11.2023 20:12
    +14

    Такие пятиминутные фильтры обычно слово "оскорблять" банят.))


    1. csharpreader
      30.11.2023 20:12
      +6

      Слово «аналитика» ещё очень страдает )


    1. IvanPetrof
      30.11.2023 20:12

      Потому что нельзя людей оскорблять))


  1. Rsa97
    30.11.2023 20:12
    +25

    Какая у вашего фильтра защита от ложноположительных срабатываний?
    "Застрахуй команду корабля со скипидаром"


    1. Dolios
      30.11.2023 20:12
      +3

      Вас навсегда забанят за такое )


    1. Squoworode
      30.11.2023 20:12
      +3

      Таким ещё Олеговна занимается


    1. Oz_Alex
      30.11.2023 20:12
      +1

      Я бодибилдер, у меня есть сабля, можно ли ей делать педикюр?
      А недавно были дебаты про судебную систему...


  1. Rend
    30.11.2023 20:12
    +1

    В русской терминологии "фильтр нижних частот" звучит не очень однозначно, но обозначает, что через него будут проходить только нижние частоты. По-английски это звучит более однозначно - "low-pass filter".

    А теперь перечитаем название статьи.


    1. hard_sign
      30.11.2023 20:12


  1. sedyh
    30.11.2023 20:12

    В первом случае у вас получился нечеткий поиск по пятиграммам. Низкая эффективность была в основном из-за размера сканирующего окна. В случае триграмм результат был бы лучше. Кроме того, во всех случаях у вас будет большое количество ложноположительных срабатываний, потребуется довольно большой словарь исключений.

    Ps: В ссылке на расстояние Левенштейна пропущено двоеточие.


    1. MountainGoat
      30.11.2023 20:12
      +2

      Словарь исключений - всегда провальная концепция.
      -- С уважением, индус Мухападхуяй.


      1. NickDoom
        30.11.2023 20:12

        Но, простите, чего такого непристойного в мухопаде? Они всегда это делают по осени…


  1. Grey83
    30.11.2023 20:12
    +1

    А ещё некоторые пишут с ошибками используя обсценную лексику. Потому что даже матерные слова написать без ошибок не умеют.
    В этом случае Ваш фильтр мало поможет.
    Кроме того есть любители использовать весь юникод (привет иероглифы и прочие символы разных языков, похожие очертаниями на буквы кириллицы/латиницы).

    Πㄗ|/|ß∑ㅜ、я ɓⓐㅐαH


    1. NickDoom
      30.11.2023 20:12

      メソモイタ

      ノㄑㄐイタㄊㄈノㄑタㄕ|