Почему-то про эту «фичу» не любят распространяться опытные коллеги, а первая встреча с таким в вашем проекте гарантирует бессонные ночи и разбитые об стенку лбы и клавиатуры. Читайте и берегите нервы, говорят они не восстанавливаются.
![](https://habrastorage.org/getpro/habr/upload_files/d2b/83a/2f6/d2b83a2f6da3c9edb47e563b26039834.png)
Начнем мы внезапно с вот такого простенького стихотворения:
cоловей c лиcой в леcу, cтроят домик навеcу.
Берем эту строку и целиком вставляем в код на вашем любимом языке программирования, затем пробуем найти например слово «лес».
В этот раз в качестве любимого языка был взят Kotlin:
fun main() {
val quote = "cоловей c лиcой в леcу, cтроят домик навеcу"
println("найдено: ${quote.contains("лес")}")
}
Запустив код выше, получаем внезапный результат:
![Внезапно. Временами глаза вам врут. Внезапно. Временами глаза вам врут.](https://habrastorage.org/getpro/habr/upload_files/fd6/4bd/76e/fd64bd76e7b4dfcc7b28ad28eb26c77d.png)
Но разумеется одного примера будет мало для понимания, поэтому вот еще один:
Буква 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("два")}")
}
И.. снова седая ночь облом:
![Ну как так-то? Ну как так-то?](https://habrastorage.org/getpro/habr/upload_files/16e/ec4/aa0/16eec4aa043d565c93bab77bc540369b.png)
Может это баг в компиляторе или.. в самом языке 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;
}
И... нет, не работает:
![Когда даже топор не помог. Когда даже топор не помог.](https://habrastorage.org/getpro/habr/upload_files/fd6/dfa/a8d/fd6dfaa8d7df899e72913d584feda838.png)
Ну ладно, видимо стихи — не мое, нужно что-то более научное и осмысленное.
Например вот такая цитата из википедии:
Общероссийский классификатор объектов административно-территориального деления (сокр. OKАТO — общероссийский классификатор административно-территориальных образований) — классификатор объектов административно-территориального деления Российской Федерации, входит в состав «Единой системы классификации и кодирования технико-экономической и социальной информации Российской Федерации» (ЕСКК). ОКАТО предназначен для обеспечения достоверности, сопоставимости и автоматизированной обработки информации в разрезах административно-территориального деления в таких сферах, как статистика, экономика и другие.
На этот раз уберем все среды разработки и компиляторы — «они все равно вам врут» (ц), поэтому просто открываем в любимом браузере Chrome «режим разработчика» (клавиша F12) и пишем в консоли на JavaScript:
let quote = "Общероссийский классификатор объектов административно-территориального деления (сокр. OKАТO — общероссийский классификатор административно-территориальных образований) — классификатор объектов административно-территориального деления Российской Федерации, входит в состав «Единой системы классификации и кодирования технико-экономической и социальной информации Российской Федерации» (ЕСКК).";
затем добавляем условие поиска:
quote.includes('ОКАТО')
И... оно не найдется:
![И любимый браузер тоже не помог, о ужас! И любимый браузер тоже не помог, о ужас!](https://habrastorage.org/getpro/habr/upload_files/a30/fde/061/a30fde0611d177b5670f5b3448a27dd2.png)
Теперь пишем в этой же самой консоли вторую часть цитаты:
let quote2 = "ОКАТО предназначен для обеспечения достоверности, сопоставимости и автоматизированной обработки информации в разрезах административно-территориального деления в таких сферах, как статистика, экономика и другие.";
И еще одну проверку:
quote2.includes('ОКАТО')
И.. оно внезапно сработает:
!["Ну как так-то?" Часть вторая. "Ну как так-то?" Часть вторая.](https://habrastorage.org/getpro/habr/upload_files/191/02b/80b/19102b80b990139ab97dc8cd54339694.png)
Ну что, все еще хотите «вкатиться в ИТ» и стать программистом? Может водить трактор и рубить лес — не такая уж плохая затея?
Как же это работает
Если у вас есть маленькие дети — покажите им две картинки ниже и попросите найти схожести и отличия, справятся они очень быстро ;)
Вот первая:
![](https://habrastorage.org/getpro/habr/upload_files/5a9/229/4f0/5a92294f06a5b3a554f8adfba330ffb6.jpeg)
И вторая:
![](https://habrastorage.org/getpro/habr/upload_files/ad7/ff8/e26/ad7ff8e26e248b88059f2dba1467461a.png)
Как только ребенок тыкнет пальцем в пару квадратов — источник проблемы до вас сразу дойдет, максимально натуральным образом. Но если детей под рукой нет — расскажу своими словами, хотя будет и не так эффектно:
Так получилось исторически, что часть символов в cовременных английском и русском визуально очень похожи, но технически являются разными.
На стандартных офисных шрифтах визуально разницы вы не увидите совсем, только если специально взять шрифт со стилизацией получится увидеть отличия:
![Кривое 'с' - из английского алфавита Кривое 'с' - из английского алфавита](https://habrastorage.org/getpro/habr/upload_files/e1e/b9b/c04/e1eb9bc04fcb75ca9b404b31c772e2e0.png)
Теперь про техническую часть.
Компьютеры как известно оперируют числами а не строками. У каждого символа в строке есть свой числовой код, любое сравнение строк и поиск по ним происходят с использованием этих самых числовых кодов.
Взгляните:
![99 - код символа латинского 'c', 1089 - код кириллического 'c' 99 - код символа латинского 'c', 1089 - код кириллического 'c'](https://habrastorage.org/getpro/habr/upload_files/65e/ec7/242/65eec72424f840fd52bf7e4b0be648ef.png)
Первый символ 'c' — латиница, второй 'c' — кириллица. Визуально они близнецы-братья, но коды при этом отличаются.
Именно по этой причине сравнение в лоб не работает:
![Видите визуальную разницу между 'c' и 'c' ? А она есть. Видите визуальную разницу между 'c' и 'c' ? А она есть.](https://habrastorage.org/getpro/habr/upload_files/8ff/5cb/286/8ff5cb286d76cf45de10a75c70acc742.png)
Существует очень простой способ проверки — перекодирование подозрительного текста в чистый ASCII:
![](https://habrastorage.org/getpro/habr/upload_files/55b/d19/764/55bd19764765101e4bc1a635b6b22728.png)
Перекодировка немедленно подсветит проблему, поскольку после нее останутся только латинские символы. Но к сожалению далеко не всегда есть возможность использовать подобный подход:
большинство вводимых текстовых данных происходит в поля форм в браузере или в приложении — вставить туда подобную проверку каким‑то универсальным способом невозможно.
Насколько это серьезно
Все компьютеры используемые в РФ имеют минимум два языка ввод — русский и английский, между которыми пользователи переключаются во время работы. Поэтому взаимодействие и ввод данных во время работы постоянно происходят на двух языках.
Везде где есть заполнение каких-либо форм и ввод данных пользователем существует и описанная проблема с похожими символами.
Чаще всего ошибаются с символами 'c' и 'с', поскольку за них отвечает одна и та же клавиша на клавиатуре, сильно реже со всеми остальными.
Ошибаются не только пользователи, но и сами разработчики:
![Русская 'c' попавшая в название поля JPA сущности и уехавшая в таком виде в базу. Русская 'c' попавшая в название поля JPA сущности и уехавшая в таком виде в базу.](https://habrastorage.org/getpro/habr/upload_files/e9b/ef0/b20/e9bef0b207619c018b1a0de6666a04e3.png)
Как видите проблема массовая и серьезная, поскольку попадание текста с неправильным 'c' например в поисковый индекс сломает вам поиск — такая строка просто не будет находиться в выдаче, хотя ни технически ни визуально проблемы видно не будет.
Автозамена
Я написал небольшой класс на 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("два")}")
}
В качестве тестов как раз те самые стихи, приведенные в начале статьи
![Теперь все работает. Теперь все работает.](https://habrastorage.org/getpro/habr/upload_files/93b/085/30d/93b08530d0b71ab3c537a5683852cc31.png)
Пользуйтесь на здоровье.
Другие языки
Удивительно, но в других европейских языках подобной проблемы нет:
визуально похожие на латинские буквы французского, немецкого или испанского имеют тот же самый код, поэтому перекодирование в ASCII убивает лишь небольшую часть текста, где есть специфика языка:
![Пример с текстом на французском. Пример с текстом на французском.](https://habrastorage.org/getpro/habr/upload_files/326/db1/5d9/326db15d998695fe29c270bb0023f2cf.png)
![И на немецком, убился всего лишь один символ. И на немецком, убился всего лишь один символ.](https://habrastorage.org/getpro/habr/upload_files/efe/c8a/2e5/efec8a2e595bd16e745922c2d3dcef7a.png)
Вот такая она, специфика «великого и могучего», возникшая ввиду исторических причин и обстоятельств.
P.S.
Это немного отцезурированная версия статьи, трешевый оригинал которой доступен в нашем блоге.
0x08 Software
Мы небольшая команда ветеранов ИТ‑индустрии, создаем и дорабатываем самое разнообразное программное обеспечение, наш софт автоматизирует бизнес‑процессы на трех континентах, в самых разных отраслях и условиях.
Оживляем давно умершее, чиним никогда не работавшее и создаем невозможное — затем рассказываем об этом в своих статьях.
Комментарии (46)
Spiritschaser
30.08.2024 16:19+1Ну в самом деле, у любой русскоязычной NLP есть функции приведения к норм алфавиту и норм написанию
alex0x08 Автор
30.08.2024 16:19А еще есть римские цифры, которые к (моему великому удивлению) используются в адресах улиц. Так что "в лоб" не получится.
tuxi
30.08.2024 16:19+6Была как-то давно история одна. Лет 10 назад. 6 часов жизни коту под хвост. Надо было с ФТП забирать файлы с расширением ".1c"... Я тогда перепробовал штук 10 разных библиотек которые с ФТП работать умели. То есть вот через FAR файлы есть, а через фтп-клиента их нет. Я тогда изучил все режимы работы ФТП протокола. Узнал кучу тонких настроек.
Ну кто же знал, что человек-из-1С не скопирует маску имени файла из ТЗ, а набьет ее руками.
И вот с тех пор всегда сначала проверяю буковки прежде чем начинать ломать библиотеки, клавиатуру и голову.
AccountForHabr
Кто букву Н настраивал у деда, таким не удивишь!
alex0x08 Автор
Очень мало таких осталось )
PereslavlFoto
[*] Эха не для переживаний.
CitizenOfDreams
МЕЯ ВИДО?
alex0x08 Автор
Неужели еще столько живых фидошников осталось ) Вам же минимум за 45-50 уже должно быть )