Антимат в чате на iOS: как я мешаю пользователям сквернословить
Привет, Хабр! На связи снова Александр Пиманов (по-прежнему 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, и наблюдаем магию:
Скрытый текст
Выводы
По-хорошему такую регулярку можно и нужно получать с бэкэнда, чтобы не хранить локально. Со своей задачей она справится и сделает общение в чате «чище». На этом у меня все, надеюсь, статья была вам о***о полезна!