The "royal clusterfuck" или одна особенность русского языка
Почему-то про эту «фичу» не пишут статей на Хабре, зато встреча с таким в жизни гарантирует бессонные ночи и разбитые об стенку лбы и клавиатуры. Это старый добрый классический п#здец, как он есть. Читайте и берегите нервы, говорят они не восстанавливаются.
Начнем мы внезапно с вот такого простенького стихотворения:
Берете эту строку целиком, вставляете в код на вашем любимом языке программирования и пробуете найти в ней например слово «лес».
Я в этот раз в качестве любимого языка взял Kotlin:
fun main() { val quote = "cоловей c лиcой в леcу, cтроят домик навеcу" println("найдено: ${quote.contains("лес")}") }
И получил внезапный результат:
Но конечно одного примера мало для понимания, поэтому вот следующий:
Буква B — Beрблюд двугopбый,
Он большой и очень гopдый.
У вeрблюдa двa горбa,
И у буквы B их двa.
Давайте проверим, содержит ли наш стих слово «два»:
fun main() { val quote = "Буква B — Beрблюд двугopбый,\n" + "Он большой и очень гopдый.\n" + "У вeрблюдa двa горбa,\n" + "И у буквы B их двa." println("найдено: ${quote.contains("два")}") }
Может это баг в компиляторе или.. самом языке Kotlin !? Кто знает что туда напихали эти ваши современные криворукие «погромисты»!
Я тоже так подумал и взял топор C++:
#include <iostream> int main(int argc, char **argv) { std::string quote = "Буква B — Beрблюд двугopбый," "Он большой и очень гopдый." "У вeрблюдa двa горбa," "И у буквы B их двa. "; if (quote.find( "Верблюд") != std::string::npos) { std::cout << "Нашлось!" << std::endl; } else { std::cout << "неа" << std::endl; } return 0; }
Ну ладно, видимо стихи — говно, нужно что-то более научное и осмысленное:
Общероссийский классификатор объектов административно-территориального деления (сокр. OKАТO — общероссийский классификатор административно-территориальных образований) — классификатор объектов административно-территориального деления Российской Федерации, входит в состав «Единой системы классификации и кодирования технико-экономической и социальной информации Российской Федерации» (ЕСКК). ОКАТО предназначен для обеспечения достоверности, сопоставимости и автоматизированной обработки информации в разрезах административно-территориального деления в таких сферах, как статистика, экономика и другие.
Так сказать жахнем канцеляритом по #балу реальности!
И еще в этот раз пошлем нахер все тяжелые среды разработки для тупых программистов — они все равно вам врут, поэтому просто открываете в любимом браузере «режим разработчика» (клавиша F12) и пишете прямо в консоли:
let quote = "Общероссийский классификатор объектов административно-территориального деления (сокр. OKАТO — общероссийский классификатор административно-территориальных образований) — классификатор объектов административно-территориального деления Российской Федерации, входит в состав «Единой системы классификации и кодирования технико-экономической и социальной информации Российской Федерации» (ЕСКК).";
quote.includes('ОКАТО')
Теперь пишете в этой же консоли вторую часть цитаты:
let quote2 = "ОКАТО предназначен для обеспечения достоверности, сопоставимости и автоматизированной обработки информации в разрезах административно-территориального деления в таких сферах, как статистика, экономика и другие.";
quote2.includes('ОКАТО')
Ну что мои дорогие, вы все еще хотите «вкатиться в ИТ» и стать программистом? Может водить трактор или рубить лес — не такая уж плохая затея?
Как же так бл#дь это работает
Если у вас есть маленькие дети — покажите им две картинки ниже и попросите найти схожести и отличия, справятся они очень быстро ;)
Как только маленький ребенок тыкнет пальцем в пару квадратов — источник проблемы до вас сразу дойдет, максимально натуральным образом.
Но если детей под рукой нет — расскажу своими словами, хотя будет и не так эффектно:
Так получилось исторически, что часть символов в cовременных английском и русском визуально очень похожи, но технически являются разными.
На стандартных офисных шрифтах визуально разницы вы не увидите совсем, только если специально взять шрифт с изъ@бом стилизацией — получится увидеть отличия:
Компьютеры как известно оперируют числами, а не строками. У каждого символа в строке есть свой числовой код, любое сравнение строк и поиск по ним происходят с использованием этих самых числовых кодов.
Первый символ 'c' — латиница, второй 'c' — кириллица. Визуально они близнецы-братья, но коды при этом отличаются.
Именно поэтому сравнение в лоб не работает:
Есть еще один вариант проверки это перекодирование подозрительного текста в чистый ASCII:
Перекодировка немедленно подсветит проблему, поскольку после нее останутся только латинские символы.
Насколько это серьезно
Абсолютно все компьютеры в РФ имеют минимум два языка ввода: русский и английский, между которыми пользователи переключаются во время работы. Поэтому взаимодействие и ввод данных во время работы постоянно происходят на двух языках.
Везде где есть заполнение каких-либо форм и ввод данных пользователем есть эта проблема с похожими символами
Чаще всего ошибаются с символами 'c' и 'с', поскольку за них отвечает одна и та же кнопка на клавиатуре, сильно реже со всеми остальными.
Ошибаются не только пользователи, но даже сами разработчики:
Как видите проблема массовая и серьезная, поскольку попадание текста с неправильным 'c' например в поисковый индекс сломает вам поиск — такая строка просто не будет находиться в выдаче, хотя ни технически ни визуально проблемы видно не будет.
Почему при такой серьезности нет какого-либо освещения этой проблемы не знаю: ни хваленые дата-сайентисты, работающие с огромными массивами данных ни мамкины AI-эксперты, сутками не вылезающие из своих чат-ботов о таком почему-то не говорят.
Вообщем как всегда — описания нет, а жопа есть.
Автозамена
Я написал небольшой класс на Kotlin для автоматической замены похожих символов — в качестве простого решения описанной проблемы.
Его можно достаточно легко адаптировать под ваши реалии.
Выложен в виде gist на Github, вот код:
package com.x0x08.yoba /** Класс для поиска и автозамены визуально похожих символов латиницы на кириллицу: 'c' -> 'с' и другие */ class Matcher { /** * Находит и заменяет похожие латинские буквы на кириллические * @param input * входящая строка * @return * строка с замененными символами */ fun replaceSimilarRuEnChars(input: String): String { val chars = input.toCharArray() for (i in chars.indices) { val c = chars[i] // символ в нижнем регистре используется в качестве ключа val cLow = c.lowercaseChar() // поиск по словарю if (RU_EN_MATCH.containsKey(cLow)) { // замена chars[i] = RU_EN_MATCH[cLow]!! // если оригинальный символ был в верхнем регистре - ставим его и у замены if (Character.isUpperCase(c)) chars[i] = chars[i].uppercaseChar() println("найден ASCII символ: '$c' , заменен на: '${chars[i]}'") } } return String(chars) } companion object { // справочник заменяемых символов private val RU_EN_MATCH: MutableMap<Char, Char> = HashMap() init { RU_EN_MATCH['c'] = 'с' RU_EN_MATCH['b'] = 'ь' RU_EN_MATCH['o'] = 'о' RU_EN_MATCH['p'] = 'р' RU_EN_MATCH['x'] = 'х' RU_EN_MATCH['m'] = 'м' RU_EN_MATCH['h'] = 'н' RU_EN_MATCH['e'] = 'е' RU_EN_MATCH['t'] = 'т' RU_EN_MATCH['k'] = 'к' RU_EN_MATCH['a'] = 'а' } } } fun main() { val m = Matcher() println("Тест 1") var quote = "cоловей c лиcой в леcу, cтроят домик навеcу" println("найдено: ${quote.contains("лес")}") quote = m.replaceSimilarRuEnChars(quote) println("теперь найдено: ${quote.contains("лес")}") println("Тест 2") quote = "Буква B — Beрблюд двугopбый,\n" + "Он большой и очень гopдый.\n" + "У вeрблюдa двa горбa,\n" + "И у буквы B их двa." println("найдено: ${quote.contains("два")}") quote = m.replaceSimilarRuEnChars(quote) println("теперь найдено: ${quote.contains("два")}") }
В качестве тестов как раз те самые стихи, приведенные выше:
Другие языки
Как бы удивительно не было, но в других европейских языках такой проблемы со вводом просто нет: визуально похожие на латинские буквы французского, немецкого или испанского имеют тот же самый код, поэтому перекодирование в ASCII убивает лишь небольшую часть текста, где есть специфика языка:
Вот такая она, специфика «великого и могучего», возникшая ввиду исторических причин и обстоятельств.
Мне лично поиск и решение проблемы с кириллическим символом, попавшим «куда не надо» в реальном большом проекте когда-то стоило двух разбитых клавиатур.
Но я хотя-бы нашел причину. А сможете ли найти ее вы в вашем большом проекте — вопрос.