Почему-то про эту «фичу» не любят распространяться опытные коллеги, а первая встреча с таким в вашем проекте гарантирует бессонные ночи и разбитые об стенку лбы и клавиатуры. Читайте и берегите нервы, говорят они не восстанавливаются.

Начнем мы внезапно с вот такого простенького стихотворения:

cоловей c лиcой в леcу, cтроят домик навеcу.

Берем эту строку и целиком вставляем в код на вашем любимом языке программирования, затем пробуем найти например слово «лес».

В этот раз в качестве любимого языка был взят 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 — общероссийский классификатор административно-территориальных образований) — классификатор объектов административно-территориального деления Российской Федерации, входит в состав «Единой системы классификации и кодирования технико-экономической и социальной информации Российской Федерации» (ЕСКК). ОКАТО предназначен для обеспечения достоверности, сопоставимости и автоматизированной обработки информации в разрезах административно-территориального деления в таких сферах, как статистика, экономика и другие.

На этот раз уберем все среды разработки и компиляторы — «они все равно вам врут» (ц), поэтому просто открываем в любимом браузере Chrome «режим разработчика» (клавиша F12) и пишем в консоли на JavaScript:

let quote = "Общероссийский классификатор объектов административно-территориального деления (сокр. OKАТO — общероссийский классификатор административно-территориальных образований) — классификатор объектов административно-территориального деления Российской Федерации, входит в состав «Единой системы классификации и кодирования технико-экономической и социальной информации Российской Федерации» (ЕСКК).";

затем добавляем условие поиска:

quote.includes('ОКАТО')

И... оно не найдется:

И любимый браузер тоже не помог, о ужас!
И любимый браузер тоже не помог, о ужас!

Теперь пишем в этой же самой консоли вторую часть цитаты:

let quote2 = "ОКАТО предназначен для обеспечения достоверности, сопоставимости и автоматизированной обработки информации в разрезах административно-территориального деления в таких сферах, как статистика, экономика и другие.";

И еще одну проверку:

quote2.includes('ОКАТО')

И.. оно внезапно сработает:

"Ну как так-то?" Часть вторая.
"Ну как так-то?" Часть вторая.

Ну что, все еще хотите «вкатиться в ИТ» и стать программистом? Может водить трактор и рубить лес — не такая уж плохая затея?

Как же это работает

Если у вас есть маленькие дети — покажите им две картинки ниже и попросите найти схожести и отличия, справятся они очень быстро ;)

Вот первая:

И вторая:

Как только ребенок тыкнет пальцем в пару квадратов — источник проблемы до вас сразу дойдет, максимально натуральным образом. Но если детей под рукой нет — расскажу своими словами, хотя будет и не так эффектно:

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

На стандартных офисных шрифтах визуально разницы вы не увидите совсем, только если специально взять шрифт со стилизацией получится увидеть отличия:

Кривое 'с' - из английского алфавита
Кривое 'с' - из английского алфавита

Теперь про техническую часть.

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

Взгляните:

99 - код символа латинского 'c', 1089 - код кириллического 'c'
99 — код символа латинского 'c', 1089 — код кириллического 'c'

Первый символ 'c' — латиница, второй 'c' — кириллица. Визуально они близнецы-братья, но коды при этом отличаются.

Именно по этой причине сравнение в лоб не работает:

Видите визуальную разницу между 'c' и 'c' ? А она есть.
Видите визуальную разницу между 'c' и 'c' ? А она есть.

Существует очень простой способ проверки — перекодирование подозрительного текста в чистый ASCII:

Перекодировка немедленно подсветит проблему, поскольку после нее останутся только латинские символы. Но к сожалению далеко не всегда есть возможность использовать подобный подход:

большинство вводимых текстовых данных происходит в поля форм в браузере или в приложении — вставить туда подобную проверку каким‑то универсальным способом невозможно.

Насколько это серьезно

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

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

Чаще всего ошибаются с символами 'c' и 'с', поскольку за них отвечает одна и та же клавиша на клавиатуре, сильно реже со всеми остальными.

Ошибаются не только пользователи, но и сами разработчики:

Русская 'c' попавшая в название поля JPA сущности и уехавшая в таком виде в базу.
Русская 'c' попавшая в название поля JPA сущности и уехавшая в таком виде в базу.

Как видите проблема массовая и серьезная, поскольку попадание текста с неправильным '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("два")}")
}

В качестве тестов как раз те самые стихи, приведенные в начале статьи

Теперь все работает.
Теперь все работает.

Пользуйтесь на здоровье.

Другие языки

Удивительно, но в других европейских языках подобной проблемы нет:

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

Пример с текстом на французском.
Пример с текстом на французском.
И на немецком, убился всего лишь один символ.
И на немецком, убился всего лишь один символ.

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

P.S.

Это немного отцезурированная версия статьи, трешевый оригинал которой доступен в нашем блоге.

0x08 Software

Мы небольшая команда ветеранов ИТ‑индустрии, создаем и дорабатываем самое разнообразное программное обеспечение, наш софт автоматизирует бизнес‑процессы на трех континентах, в самых разных отраслях и условиях.

Оживляем давно умершеечиним никогда не работавшее и создаем невозможное — затем рассказываем об этом в своих статьях.

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


  1. AccountForHabr
    30.08.2024 16:19
    +32

    Кто букву Н настраивал у деда, таким не удивишь!


    1. alex0x08 Автор
      30.08.2024 16:19

      Очень мало таких осталось )


      1. PereslavlFoto
        30.08.2024 16:19
        +5

        [*] Эха не для переживаний.


    1. CitizenOfDreams
      30.08.2024 16:19
      +14

      МЕЯ ВИДО?


      1. alex0x08 Автор
        30.08.2024 16:19

        Неужели еще столько живых фидошников осталось ) Вам же минимум за 45-50 уже должно быть )


        1. Wesha
          30.08.2024 16:19
          +12

          Неужели еще столько живых фидошников осталось

          е дождётесь!

          Вам же минимум за 45-50 уже должно быть

          Почему "должно быть" — есть!


        1. vorphalack
          30.08.2024 16:19
          +1

          некоторым и 40 нет


          1. alex0x08 Автор
            30.08.2024 16:19

            Это в ФИДО с 3го класса чтоли? Респект!


            1. vorphalack
              30.08.2024 16:19
              +1

              нет, где-то с рубежа нулевых - причем под конец активности уже вовсю over IP сидел.


              1. PereslavlFoto
                30.08.2024 16:19

                over IP не тру.


                1. vorphalack
                  30.08.2024 16:19
                  +1

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


            1. sergio_deschino
              30.08.2024 16:19

              38)) у нас ноды по диалапу были доступны ещё в начале 2000х

              А с 98го у меня был адрес 2:5058/ххх.22


              1. HardWrMan
                30.08.2024 16:19
                +3

                2:5088/13.3 на связи. Тоже сидел с конца 90х и первую половину 00х. Сидел строго на мопеде, когда перешли на IP то даже при наличии у меня ADSL я всё же вышел из системы. Сеть 5088 продержалась примерно до 2016 года и последним узлом перед её удалением из нодлиста был 13й... 45+, фото тех дней:


                1. sergio_deschino
                  30.08.2024 16:19
                  +1

                  Роботикс) курьер — это прям мечта на то время, у меня был аккорповский мопед)

                  А усилок прям слезу ностальгии вышиб! Прям плюсую!


                  1. HardWrMan
                    30.08.2024 16:19

                    Ну да, бузинес модем. Для простых крестьян был обрубок Шпрот. Но мне повезло. :)


                    1. AccountForHabr
                      30.08.2024 16:19

                      Для крестьян были винмодемы, а шпрот был ничего!


                      1. HardWrMan
                        30.08.2024 16:19

                        Не правда. Винмодемы были для чисто виндузятников. А для бедной касты крестьян были всякие внешние СОМ портовые гениусы и акорпы. А из виндузятников мало кто вообще даже про BBS даже слышал, им сразу гипертекстовый фидонет подавай!


                      1. Olegsoft
                        30.08.2024 16:19

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


                      1. alex0x08 Автор
                        30.08.2024 16:19

                        Угу, до тех пор пока не было большой нагрузки на CPU, например из-за запуска компиляции или просмотра фильма.


                      1. HardWrMan
                        30.08.2024 16:19

                        Ну, заикание винампа в моменты дозвона - это же тёплые воспоминания... Правда, ещё вместе с ним и мёртвая мама после грозы. Всякое бывало.


                      1. HardWrMan
                        30.08.2024 16:19

                        Кое-кто пытался даже вообще унифицировать и протянуть AMR. Мол, раз это всё равно звук, давайте к кодеку AC97 подключим сопрягалово с линией. Слоты видел, карточек - нет. Не взлетело, не фартануло.


                      1. alex0x08 Автор
                        30.08.2024 16:19

                        Это случайно не Socket-A на картинке? AC97 это же 2003й год где-то?


                      1. HardWrMan
                        30.08.2024 16:19

                        Конкретно на этой картинке - да, Socket A. Но он был и на Socket 370. Вот Chaintech 6AIA4, у меня была такая в 2002м как раз. AMR тут между ISA и PCI:


                      1. vadimr
                        30.08.2024 16:19

                        Это просто чуть разные исторические периоды. К тому моменту, как появились винмодемы, спортстеры уже допилили до более-менее пристойного состояния.


                      1. HardWrMan
                        30.08.2024 16:19

                        Да речь то не про это. Шпрот всё равно был подороже того же Акорпа или Гениуса. Ведь он был популярным и на него существовали кастомные прошивки известных авторов под АТС СНГ и прочие заморочки, особенно с ловлей Бузи. А к моменту появления винмодемов практически все виндузятники без затей перешли на них пышто никаких лишних проводов, всё работает "искаропки" и стоит сущие копейки по сути.

                        Да чё там, у меня на машине с виндой был Гениталиус Люсент, а у знакомого - Гениталиус Коммерсант (Connexant). Они, кстати, на удивление, очень чётко держали линию на 46,6к+ у меня, старый район города, старая АТС и плохие кроссы (особенно в межсезонье). Там только Куря выдерживал, а внешние Акорпы с Гениусами сливались в 33,6к максимум и те не стабильно.


                      1. unreal_undead2
                        30.08.2024 16:19

                        Что то про IDC забыли, а они на декадно-шаговых АТС хорошо бегали. Правда, я свой купил уже после того, как отвалился от ФИДО - туда ходил флоппинетом, домашнего телефона не было.


                      1. vadimr
                        30.08.2024 16:19

                        Это уж когда v.92 появился. Классические модемы больше 33600 не умели, а до того вообще только HST/ZyX.


                      1. unreal_undead2
                        30.08.2024 16:19

                        Ну какой v92 на декадно шаговых... IDC 2814 BXL умел только 33600 на v34 - но умел хорошо.


                      1. vadimr
                        30.08.2024 16:19

                        IDC, конечно, да. У меня был IDC 2814BL. Потом поменял его на доставшийся по случаю ZyXEL U-336E. До сих пор на полке стоит.


        1. dryja
          30.08.2024 16:19

          да почему? 37
          на заре нулевых был поинтом (класса с девятого), а потом нодом 2:5054/74


        1. Imaginarium
          30.08.2024 16:19

          Вам же минимум за 45-50 уже должно быть )

          А разве это много?)


          1. Wesha
            30.08.2024 16:19
            +1

            А разве это много?)

            Можно даже сказать — в меру упитанный мужчина в полном расцвете сил!


    1. pae174
      30.08.2024 16:19

      FoxPro ещё. Там русская р была каким-то служебным символом изначально.


    1. dryja
      30.08.2024 16:19

      ТЕСТ! МЕЯ ВИДО?


      1. saboteur_kiev
        30.08.2024 16:19

        Старый фидошник молодому:

        - Можешь так: HHHHHH
        - ет...


    1. mr_protev
      30.08.2024 16:19

      Да помню, у меня был русификатор (назывался vrun, http://old-dos.ru/index.php?page=files&mode=files&do=show&id=3402

      )

      Автор - программист из нашего города (Йошкар-Ола), уехал в Израиль давно уже.

      Там всё автоматон заменялось, ничего настраивать не надо было.


  1. Spiritschaser
    30.08.2024 16:19
    +1

    Ну в самом деле, у любой русскоязычной NLP есть функции приведения к норм алфавиту и норм написанию


    1. alex0x08 Автор
      30.08.2024 16:19

      А еще есть римские цифры, которые к (моему великому удивлению) используются в адресах улиц. Так что "в лоб" не получится.


      1. babypowder
        30.08.2024 16:19
        +2

        видел как в 2008 в одном гос-икубаторе секретаря заставляли шуршать сайт госзакупок а она жаловалась что поиском никак не работает - через год-два оказалось что на тендер намерено выставляли текст из смешаных алфавитов / на хабре чего-то не гуглится про это


        1. pae174
          30.08.2024 16:19

          на хабре чего-то не гуглится про это

          https://navalny.livejournal.com/753475.html


        1. vadimr
          30.08.2024 16:19

          Частая практика.


  1. tuxi
    30.08.2024 16:19
    +6

    Была как-то давно история одна. Лет 10 назад. 6 часов жизни коту под хвост. Надо было с ФТП забирать файлы с расширением ".1c"... Я тогда перепробовал штук 10 разных библиотек которые с ФТП работать умели. То есть вот через FAR файлы есть, а через фтп-клиента их нет. Я тогда изучил все режимы работы ФТП протокола. Узнал кучу тонких настроек.

    Ну кто же знал, что человек-из-1С не скопирует маску имени файла из ТЗ, а набьет ее руками.

    И вот с тех пор всегда сначала проверяю буковки прежде чем начинать ломать библиотеки, клавиатуру и голову.