Привет, Хабр! На связи снова Александр Пиманов (по-прежнему iOS-разработчик МТС Диджитал). Сегодня поделюсь своим опытом в одной интересной нишевой теме: фильтрации нецензурной лексики в приложении для iOS.

Да, мало кому может понадобиться фильтровать мат на клиенте, но если у вас есть функция нейминга элементов в UI (добавление кастомного имени страницы, кнопки и так далее), запрос от бизнеса на такой фильтр и вы хотите сделать «проверку на дурака», то эта статья для вас. Прелюдия окончена, все подробности под катом.

Как я докатился до такой жизни

Решение делать первичную фильтрацию текста на клиенте пришло внезапно: бэк-разработчик закинул идею, мне она понравилась, а бизнес оценил положительно.

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

Регулярное выражение

Потратив N времени на ресерч, я нашел open-source регулярку под Java-машину. В этой регулярке берутся все популярные и часто используемые вариации мата (на одно слово несколько вариаций синтаксиса) и летят в основу фильтра, который я покажу чуть позже. Я переписал её на Swift с учетом всех особенностей языка и немного подправил.

Хочу отметить, что это первичная проверка на самые популярные слова и их разновидности. Никакая регулярка не покроет вам 100% кейсов, ибо на сцену вступает человеческий фактор: если юзер захочет выругаться, поверьте, он этo_!cделает, какая бы защита у вас не стояла. В нашем случае первичная фильтрация при отправке сообщений в чат покроет порядка 80–90% нецензурных выражений.

Проверка

Как я говорил выше, прогонять через фильтр по регулярке и искать совпадения мы будем только в пользовательских сообщениях:

func filterSwearWords(in message: String) -> String {
        do {
            let regex = try NSRegularExpression(pattern: swearWordsPattern, options: [.caseInsensitive, .allowCommentsAndWhitespace])
            let range = NSRange(location: 0, length: message.count)
            let matches = regex.matches(in: message, options: [], range: range)
            
            var filteredMessage = message as NSString
            
            // reversed() is to ensure that earlier replacements do not affect the positions of later matches.
            for match in matches.reversed() {
                let matchedWord = filteredMessage.substring(with: match.range)
                let isFirstWord = message.starts(with: matchedWord)
                
                guard matchedWord.count > 2,
                      let firstCharacter = isFirstWord ? matchedWord.first?.uppercased() : matchedWord.first?.lowercased(),
                      let lastCharacter = matchedWord.last else { return message }
                
                let replacement = "\(firstCharacter)***\(lastCharacter)"
                filteredMessage = filteredMessage.replacingCharacters(in: match.range, with: replacement) as NSString
            }
            
            return filteredMessage as String
            
        } catch {
            print("Creating regex error: \(error.localizedDescription)")
            return message
        }
    }

Обратите внимание на строчку, где мы входим в цикл. Я там поставил reversed() так как словил интересный баг: ранние замены влияют на позиции более поздних совпадений.

Затем создается подстрока с нашим совпадением. Ее содержимое проверяется на положение этого слова в контексте всего сообщения (в начале или нет).

Если слово удовлетворяет требованию регулярки и в нем больше двух букв, я беру первую и последнюю букву и вставляю между ними ***. Я решил не подгонять число звездочек под количество заменяемых символов. Это вкусовщина, но фиксированный вариант показался самым подходящим.

Ну вот, собственно, и все! Теперь просто вызываем этот метод в момент отправки сообщения, куда скармливаем текст из нашего text field, и наблюдаем магию:

Скрытый текст

Выводы

По-хорошему такую регулярку можно и нужно получать с бэкэнда, чтобы не хранить локально. Со своей задачей она справится и сделает общение в чате «чище».  На этом у меня все, надеюсь, статья была вам о***о полезна!

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


  1. anonymous
    22.08.2024 16:56

    НЛО прилетело и опубликовало эту надпись здесь


  1. randomsimplenumber
    22.08.2024 16:56

    Застра##й ком@@@у корабля!

    Кто-то прочитал старинные манускрипты и решил пробудить древнее зло? Есть всякие чудеса в unicode, есть картинки в сообщениях, есть ASCII art.. Кто захочет - найдет способ выразиться. А ему зачем то мешают.


    1. exTvr
      22.08.2024 16:56
      +2

      Застра##й ком@@@у корабля!

      со скипидаром!

      У вас таки случился прорыв:))


    1. AleksandrPimanov Автор
      22.08.2024 16:56

      Срам разводят…


    1. exTvr
      22.08.2024 16:56

      Дубля удалил.


    1. kenomimi
      22.08.2024 16:56
      +2

      А еще, когда на двачах запретили слово баттхерт - были такие времена - аноны стали писать любое слово капсом - "да у вас БАГЕТ, батенька!"

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


      1. randomsimplenumber
        22.08.2024 16:56
        +1

        написал задачу на фене - минус премия

        Поставил задачу на словах - нормально так? ;) Если у вас в коллективе принято общаться матом - к чему эти ограничения? Если не принято - то и так никто не будет писать нехороших слов.


        1. AleksandrPimanov Автор
          22.08.2024 16:56

          Где написано, что задача была поставлена на словах? Во первых, это просто идея от бэк разработчика, во вторых она прошла этапы согласования и одобрения). Кроме того, у нас в команде матом не общаются) не пойму, к чему этот комментарий


          1. randomsimplenumber
            22.08.2024 16:56

            Где написано, что задача была поставлена на словах?

            Если задача поставлена на словах, матом, и не записана - с кого минус премия? ;)

            Во первых, это просто идея от бэк разработчика, во вторых она прошла этапы согласования и одобрения)

            Ну и замечательно. Начальство виднее чем занять программистов;)

            , у нас в команде матом не общаются

            Ну и прекрасно ;)

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

            Кстати, голосовые сообщения ваш чатик умеет? ;)


            1. AleksandrPimanov Автор
              22.08.2024 16:56

              голосовых пока не предвидится) может быть в будущем, но вряд ли


              1. randomsimplenumber
                22.08.2024 16:56

                это я к тому, что пока олды решают свои регэкспы, школота посылает друг другу голосовые сообщения.


    1. AleksandrPimanov Автор
      22.08.2024 16:56


  1. Tirarex
    22.08.2024 16:56

    Было бы забавно написать фильтр но с использованием нейронки, дабы сообщения аккуратно разворачивались в светский диалог.


    1. AleksandrPimanov Автор
      22.08.2024 16:56

      Ага)) использование нейронки это следующий этап рефакторинга фичи)


    1. exTvr
      22.08.2024 16:56

      Что-то из из баша/автозамены в почте и "Челом бьём боярин, но гонец твой не добёг до нас ещё, посему не раскурили мы таску толком - ещё хоть пару спринтов дай нам днесь".


      1. SquareRootOfZero
        22.08.2024 16:56
        +3

        И ответ приходит: "Ой вы, гой еси, сыны блудниц, идите на уд срамной."


    1. konst90
      22.08.2024 16:56
      +2

      Токарь в ответ на претензии мастера утверждает, что вступал в интимные отношение с матерью мастера, директором завода и изготавливаемой деталью.


  1. jonic
    22.08.2024 16:56

    Это что же надо употреблять..


  1. SquareRootOfZero
    22.08.2024 16:56

    Есть куда дорабатывать регексп. Например:

    Спиздил до пизды пёзд: пизда пизды пизже. Сижу, пизжу про пёзды спизженные.

    Не ловит слова "пизже", "пизжу", "спизженные".


    1. AleksandrPimanov Автор
      22.08.2024 16:56

      Верно, там могут быть слова, которые он пропускает, по мере получения таких «пропусков» вношу изменения) это нормально, всех слов сразу не уловишь, да и не к чему, я писал, что это лишь первоначальная проверка


  1. Bardakan
    22.08.2024 16:56

    За regex спасибо, только как это относится к iOS?