Как сравнить «похожие» символы Юникода?

94

Я попадаю в удивительную проблему.

Я загрузил текстовый файл в свое приложение, и у меня есть логика, которая сравнивает значение, имеющее µ.

И я понял, что даже если тексты одинаковые, значение сравнения ложное.

 Console.WriteLine("μ".Equals("µ")); // returns false
 Console.WriteLine("µ".Equals("µ")); // return true

В следующей строке копируется символ µ.

Однако это могут быть не единственные подобные персонажи.

Есть ли в C # способ сравнить символы, которые выглядят одинаково, но на самом деле разные?

Диджей
источник
159
Похоже, вы нашли му Шредингера.
BoltClock
19
Это разные символы - хотя они выглядят одинаково, у них разные коды символов.
user2864740
94
Добро пожаловать в Юникод.
ta.speot.is
11
чего ты хочешь добиться? что эти двое должны быть равны, тогда даже код их символов разный, но лицо одинаковое?
Jade
28
Понятия «выглядеть одинаково» и «выглядеть одинаково» - расплывчатые. Означают ли они идентичность глифов или просто близкое сходство? Как близко? Обратите внимание, что два символа могут иметь одинаковые глифы в одном шрифте, очень похожие в другом и совершенно разные в еще одном шрифте. Важно то, зачем вы проводите такое сравнение и в каком контексте (а также допустимость ложных срабатываний и ложных отрицаний).
Jukka K. Korpela

Ответы:

125

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

В этой конкретной ситуации, если вы обратитесь к ссылкам в ответе Тони , вы увидите, что таблица для U + 00B5 говорит:

Разложение <compat> ГРЕЧЕСКАЯ СТРОЧНАЯ БУКВА MU (U + 03BC)

Это означает, что U + 00B5, второй символ в исходном сравнении, можно разложить на U + 03BC, первый символ.

Итак, вы нормализуете символы, используя полную декомпозицию совместимости, с формами нормализации KC или KD. Вот небольшой пример, который я написал, чтобы продемонстрировать:

using System;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        char first = 'μ';
        char second = 'µ';

        // Technically you only need to normalize U+00B5 to obtain U+03BC, but
        // if you're unsure which character is which, you can safely normalize both
        string firstNormalized = first.ToString().Normalize(NormalizationForm.FormKD);
        string secondNormalized = second.ToString().Normalize(NormalizationForm.FormKD);

        Console.WriteLine(first.Equals(second));                     // False
        Console.WriteLine(firstNormalized.Equals(secondNormalized)); // True
    }
}

Для получения дополнительной информации о нормализации Unicode и различных формах нормализации см System.Text.NormalizationFormи спецификация Unicode .

BoltClock
источник
26
Спасибо за ссылку на спецификацию Unicode. Я впервые прочитал об этом. Небольшое примечание: «Формы нормализации KC и KD не должны слепо применяться к произвольному тексту. Лучше думать об этих формах нормализации как о отображении в верхнем или нижнем регистре: полезно в определенных контекстах для определения основных значений, но также и для выполнения изменения в тексте, которые не всегда могут быть уместными ".
user2864740
149

Поскольку это действительно разные символы, даже если они выглядят одинаково, первая - это настоящая буква и имеет char, code = 956 (0x3BC)а вторая - микрознак и имеет 181 (0xB5).

Ссылки:

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

public void Main()
{
    var s1 = "μ";
    var s2 = "µ";

    Console.WriteLine(s1.Equals(s2));  // false
    Console.WriteLine(RemoveDiacritics(s1).Equals(RemoveDiacritics(s2))); // true 
}

static string RemoveDiacritics(string text) 
{
    var normalizedString = text.Normalize(NormalizationForm.FormKC);
    var stringBuilder = new StringBuilder();

    foreach (var c in normalizedString)
    {
        var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
        if (unicodeCategory != UnicodeCategory.NonSpacingMark)
        {
            stringBuilder.Append(c);
        }
    }

    return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}

И демо

Тони
источник
11
Из любопытства, какова причина наличия двух символов µ? Вы не видите выделенную букву K с названием «Знак Кило» (или вы?).
MartinHaTh
12
@MartinHaTh: Согласно Википедии, это «по историческим причинам» .
BoltClock
12
Unicode имеет много символов совместимости, перенесенных из старых наборов символов (например, ISO 8859-1 ), чтобы упростить преобразование из этих наборов символов. Когда набор символов был ограничен 8 битами, они включали несколько глифов (например, некоторые греческие буквы) для наиболее распространенных математических и научных целей. Повторное использование глифов в зависимости от внешнего вида было обычным явлением, поэтому не было добавлено специальной буквы «K». Но это всегда было обходным путем; правильный символ для «микро» - это настоящая греческая строчная буква «мю», правильный символ для «ом» - настоящая заглавная омега и так далее.
VGR
8
Нет ничего лучше, чем когда что-то делается для истерического изюма
Паульм
11
Есть ли специальный К для хлопьев?
86

У них обоих разные коды символов: Подробнее см. Здесь.

Console.WriteLine((int)'μ');  //956
Console.WriteLine((int)'µ');  //181

Где 1-й:

Display     Friendly Code   Decimal Code    Hex Code    Description
====================================================================
μ           &mu;            &#956;          &#x3BC;     Lowercase Mu
µ           &micro;         &#181;          &#xB5;      micro sign Mu

Образ

Вишал Сутхар
источник
39

В конкретном примере μ(mu) и µ(micro sign) последний имеет разложение совместимости с первым, поэтому вы можете нормализовать строку FormKCили FormKDпреобразовать микрознаки в mus.

Однако существует множество наборов символов, которые выглядят одинаково, но не эквивалентны ни в одной форме нормализации Unicode. Например, A(латиница), Α(греческий) и А(кириллица). На веб-сайте Unicode есть файл confusables.txt со списком, предназначенный для помощи разработчикам в защите от атак омографа . При необходимости вы можете проанализировать этот файл и построить таблицу для «визуальной нормализации» строк.

dan04
источник
Определенно полезно знать при использовании Normalize. Кажется удивительным, что они остаются отличными.
user2864740
4
@ user2864740: Если бы греческий тау в верхнем регистре не отличался от римской буквы T, было бы очень трудно добиться разумной сортировки греческого и римского текста в алфавитном порядке. Кроме того, если бы в гарнитуре использовался другой визуальный стиль для греческих и римских букв, было бы очень неприятно, если бы греческие буквы, форма которых напоминала римские буквы, отображались иначе, чем те, которые не отображались.
supercat
7
Что еще более важно, объединение европейских алфавитов затруднит ToUpper/ ToLowerусложнит реализацию. Вы должны "B".ToLower()быть bна английском, но βна греческом и врусском языках. Как бы то ни было, только турецкий (без точки i) и несколько других языков нуждаются в правилах регистра, отличных от установленных по умолчанию.
dan04
@ dan04: Интересно, задумывался ли когда-нибудь о присвоении уникальных кодовых точек всем четырем вариациям турецких «i» и «I»? Это устранило бы любую двусмысленность в поведении toUpper / toLower.
supercat
34

Найдите оба символа в базе данных Unicode и увидите разницу .

Один из них - греческая строчная буква, µ а другой - микро-знак µ .

Name            : MICRO SIGN
Block           : Latin-1 Supplement
Category        : Letter, Lowercase [Ll]
Combine         : 0
BIDI            : Left-to-Right [L]
Decomposition   : <compat> GREEK SMALL LETTER MU (U+03BC)
Mirror          : N
Index entries   : MICRO SIGN
Upper case      : U+039C
Title case      : U+039C
Version         : Unicode 1.1.0 (June, 1993)

Name            : GREEK SMALL LETTER MU
Block           : Greek and Coptic
Category        : Letter, Lowercase [Ll]
Combine         : 0
BIDI            : Left-to-Right [L]
Mirror          : N
Upper case      : U+039C
Title case      : U+039C
See Also        : micro sign U+00B5
Version         : Unicode 1.1.0 (June, 1993)
Субин Джейкоб
источник
4
Как это было набрано 37 голосов? Он не отвечает на вопрос («Как сравнивать символы Юникода»), он просто комментирует, почему этот конкретный пример не равен. В лучшем случае это должен быть комментарий к вопросу. Я понимаю, что параметры форматирования комментария не позволяют публиковать его так же хорошо, как параметры форматирования ответа, но это не должно быть веской причиной для публикации в качестве ответа.
Konerak
5
На самом деле вопрос был другим: почему проверка равенства μ и μ возвращает false. Этот ответ ответит на него. Позже OP задал другой вопрос (этот вопрос), как сравнить два одинаковых символа. На оба вопроса были даны наилучшие ответы, и позже один из модераторов объединил оба вопроса, выбрав лучший ответ второго как лучший. Кто-то отредактировал этот вопрос, чтобы он суммировал
Субин Джейкоб
На самом деле, я не добавлял никакого контента после слияния
Субин Джейкоб
24

ИЗМЕНИТЬ После слияния этого вопроса с Как сравнить «μ» и «µ» в C #
Исходный ответ опубликован:

 "μ".ToUpper().Equals("µ".ToUpper()); //This always return true.

РЕДАКТИРОВАТЬ После прочтения комментариев, да, использовать вышеуказанный метод нецелесообразно, потому что он может давать неправильные результаты для некоторых других типов входных данных, для этого мы должны использовать нормализацию с использованием полной декомпозиции совместимости, как указано в вики . (Благодаря ответу BoltClock )

    static string GREEK_SMALL_LETTER_MU = new String(new char[] { '\u03BC' });
    static string MICRO_SIGN = new String(new char[] { '\u00B5' });

    public static void Main()
    {
        string Mus = "µμ";
        string NormalizedString = null;
        int i = 0;
        do
        {
            string OriginalUnicodeString = Mus[i].ToString();
            if (OriginalUnicodeString.Equals(GREEK_SMALL_LETTER_MU))
                Console.WriteLine(" INFORMATIO ABOUT GREEK_SMALL_LETTER_MU");
            else if (OriginalUnicodeString.Equals(MICRO_SIGN))
                Console.WriteLine(" INFORMATIO ABOUT MICRO_SIGN");

            Console.WriteLine();
            ShowHexaDecimal(OriginalUnicodeString);                
            Console.WriteLine("Unicode character category " + CharUnicodeInfo.GetUnicodeCategory(Mus[i]));

            NormalizedString = OriginalUnicodeString.Normalize(NormalizationForm.FormC);
            Console.Write("Form C Normalized: ");
            ShowHexaDecimal(NormalizedString);               

            NormalizedString = OriginalUnicodeString.Normalize(NormalizationForm.FormD);
            Console.Write("Form D Normalized: ");
            ShowHexaDecimal(NormalizedString);               

            NormalizedString = OriginalUnicodeString.Normalize(NormalizationForm.FormKC);
            Console.Write("Form KC Normalized: ");
            ShowHexaDecimal(NormalizedString);                

            NormalizedString = OriginalUnicodeString.Normalize(NormalizationForm.FormKD);
            Console.Write("Form KD Normalized: ");
            ShowHexaDecimal(NormalizedString);                
            Console.WriteLine("_______________________________________________________________");
            i++;
        } while (i < 2);
        Console.ReadLine();
    }

    private static void ShowHexaDecimal(string UnicodeString)
    {
        Console.Write("Hexa-Decimal Characters of " + UnicodeString + "  are ");
        foreach (short x in UnicodeString.ToCharArray())
        {
            Console.Write("{0:X4} ", x);
        }
        Console.WriteLine();
    }

Вывод

INFORMATIO ABOUT MICRO_SIGN    
Hexa-Decimal Characters of µ  are 00B5
Unicode character category LowercaseLetter
Form C Normalized: Hexa-Decimal Characters of µ  are 00B5
Form D Normalized: Hexa-Decimal Characters of µ  are 00B5
Form KC Normalized: Hexa-Decimal Characters of µ  are 03BC
Form KD Normalized: Hexa-Decimal Characters of µ  are 03BC
 ________________________________________________________________
 INFORMATIO ABOUT GREEK_SMALL_LETTER_MU    
Hexa-Decimal Characters of µ  are 03BC
Unicode character category LowercaseLetter
Form C Normalized: Hexa-Decimal Characters of µ  are 03BC
Form D Normalized: Hexa-Decimal Characters of µ  are 03BC
Form KC Normalized: Hexa-Decimal Characters of µ  are 03BC
Form KD Normalized: Hexa-Decimal Characters of µ  are 03BC
 ________________________________________________________________

Читая информацию в Unicode_equivalence, я обнаружил

Выбор критериев эквивалентности может повлиять на результаты поиска. Например, некоторые типографические лигатуры, такие как U + FB03 (ffi), ..... поэтому поиск U + 0066 (f) в качестве подстроки будет успешным в нормализации NFKC для U + FB03, но не в нормализации NFC для U + FB03.

Таким образом, для сравнения эквивалентности мы обычно должны использовать FormKCнормализацию NFKC или нормализацию NFKD FormKD.
Мне было немного любопытно узнать больше обо всех символах Unicode, поэтому я сделал образец, который будет перебирать все символы Unicode, UTF-16и я получил некоторые результаты, которые хочу обсудить.

  • Информация о символах, чьи FormCи FormDнормализованные значения не эквивалентны
    Total: 12,118
    Character (int value): 192-197, 199-207, 209-214, 217-221, 224-253, ..... 44032-55203
  • Информация о символах, чьи FormKCи FormKDнормализованные значения не эквивалентны
    Total: 12,245
    Character (int value): 192-197, 199-207, 209-214, 217-221, 224-228, ..... 44032-55203, 64420-64421, 64432-64433, 64490-64507, 64512-64516, 64612-64617, 64663-64667, 64735-64736, 65153-65164, 65269-65274
  • Все символы, чьи FormCи FormDнормализованные значения не были эквивалентны, там FormKCи FormKDнормализованные значения также не были эквивалентными, за исключением следующих
    символов.901 '΅', 8129 '῁', 8141 '῍', 8142 '῎', 8143 '῏', 8157 '῝', 8158 '῞'
    , 8159 '῟', 8173 '῭', 8174 '΅'
  • Дополнительный символ, у которого FormKCи FormKDнормализованное значение не были эквивалентны, но там FormCи FormDнормализованные значения были эквивалентными
    Total: 119
    Символы:452 'DŽ' 453 'Dž' 454 'dž' 12814 '㈎' 12815 '㈏' 12816 '㈐' 12817 '㈑' 12818 '㈒' 12819 '㈓' 12820 '㈔' 12821 '㈕', 12822 '㈖' 12823 '㈗' 12824 '㈘' 12825 '㈙' 12826 '㈚' 12827 '㈛' 12828 '㈜' 12829 '㈝' 12830 '㈞' 12910 '㉮' 12911 '㉯' 12912 '㉰' 12913 '㉱' 12914 '㉲' 12915 '㉳' 12916 '㉴' 12917 '㉵' 12918 '㉶' 12919 '㉷' 12920 '㉸' 12921 '㉹' 12922 '㉺' 12923 '㉻' 12924 '㉼' 12925 '㉽' 12926 '㉾' 13056 '㌀' 13058 '㌂' 13060 '㌄' 13063 '㌇' 13070 '㌎' 13071 '㌏' 13072 '㌐' 13073 '㌑' 13075 '㌓' 13077 '㌕' 13080 '㌘' 13081 '㌙' 13082 '㌚' 13086 '㌞' 13089 '㌡' 13092 '㌤' 13093 '㌥' 13094 '㌦' 13099 '㌫' 13100 '㌬' 13101 '㌭' 13102 '㌮' 13103 '㌯' 13104 '㌰' 13105 '㌱' 13106 '㌲' 13108 '㌴' 13111 '㌷' 13112 '㌸' 13114 '㌺' 13115 '㌻' 13116 '㌼' 13117 '㌽' 13118 '㌾' 13120 '㍀' 13130 '㍊' 13131 '㍋' 13132 '㍌' 13134 '㍎' 13139 '㍓' 13140 '㍔' 13142 '㍖' .......... ﺋ' 65164 'ﺌ' 65269 'ﻵ' 65270 'ﻶ' 65271 'ﻷ' 65272 'ﻸ' 65273 'ﻹ' 65274'
  • Есть некоторые символы, которые нельзя нормализовать , они выкидывают ArgumentExceptionпри попытке
    Total:2081 Characters(int value): 55296-57343, 64976-65007, 65534

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

  1. Unicode_equivalence
  2. Unicode_compatibility_characters
dbw
источник
4
Странно, но работает ... Я имею в виду, что это два разных символа с разными значениями, и преобразование их в верхнее делает их равными? Я не вижу логики, но хорошее решение +1
BudBrot
45
Это решение маскирует проблему и может вызвать проблемы в общем случае. Такой тест обнаружит это, "m".ToUpper().Equals("µ".ToUpper());и "M".ToUpper().Equals("µ".ToUpper());это тоже правда. Это может быть нежелательно.
Эндрю Лич
6
-1 - это ужасная идея. Не работайте с Unicode таким образом.
Конрад Рудольф
1
Вместо трюков, основанных на ToUpper (), почему бы не использовать String.Equals ("μ", "μ", StringComparison.CurrentCultureIgnoreCase)?
svenv
6
Есть одна веская причина различать «МИКРОЗНАК» и «ГРЕЧЕСКАЯ СТРОЧНАЯ БУКВА MU» - сказать, что «прописные буквы» микрознака все еще остаются микрознаками. Но капитализация меняет микро на мега, счастливая инженерия.
Грег
9

Скорее всего, есть два разных кода символа, которые делают (визуально) один и тот же символ. Хотя технически они не равны, они выглядят равными. Взгляните на таблицу символов и посмотрите, есть ли несколько экземпляров этого символа. Или распечатайте символьный код двух символов в вашем коде.

PMF
источник
6

Вы спрашиваете «как их сравнить», но не говорите нам, что хотите делать.

Есть как минимум два основных способа их сравнить:

Либо вы сравниваете их напрямую, как есть, а они разные

Или вы используете нормализацию совместимости Unicode, если вам нужно сравнение, которое обнаружит, что они совпадают.

Однако может возникнуть проблема, потому что нормализация совместимости Unicode заставит сравнивать многие другие символы. Если вы хотите, чтобы только эти два символа рассматривались как одинаковые, вам следует использовать собственные функции нормализации или сравнения.

Для более конкретного решения нам необходимо знать вашу конкретную проблему. В каком контексте вы столкнулись с этой проблемой?

Hippietrail
источник
1
Эквивалентны ли «микрознак» и строчная буква «мю»? Использование канонической нормализации даст вам более строгое сравнение.
Tanner Swett
@ TannerL.Swett: На самом деле я даже не знаю, как это проверить с головы до
ног
1
Собственно, я импортировал файл с формулой физики. Вы правы насчет нормализации. Я должен пройти через это более глубоко ..
DJ
Что за файл? Что-то сделанное человеком из простого текста Unicode? Или что-то выводимое приложением в определенном формате?
hippietrail
5

Если бы я хотел быть педантичным, я бы сказал, что ваш вопрос не имеет смысла, но поскольку мы приближаемся к Рождеству и птицы поют, я продолжу.

Во- первых, на 2 лица , которые вы пытаетесь сравнить это glyphс, глиф является частью набора символов , предоставляемых тем , что, как правило , знают , как «шрифт», то , что обычно приходит в ttf, otfили любой другой формат файла , вы с помощью.

Глифы представляют собой представление данного символа, и поскольку они представляют собой представление, которое зависит от определенного набора, вы не можете просто ожидать, что у вас будет 2 похожих или даже «лучших» идентичных символа, это фраза, которая не имеет смысла если вы учитываете контекст, вы должны, по крайней мере, указать, какой шрифт или набор глифов вы учитываете, когда формулируете подобный вопрос.

То, что обычно используется для решения проблемы, аналогичной той, с которой вы сталкиваетесь, - это OCR, по сути, программное обеспечение, которое распознает и сравнивает глифы. Если C # предоставляет OCR по умолчанию, я этого не знаю, но обычно это очень плохо идея, если вам действительно не нужно OCR, и вы знаете, что с ним делать.

Вы можете в конечном итоге интерпретировать книгу по физике как древнегреческую книгу, не упоминая о том, что OCR, как правило, является дорогостоящим с точки зрения ресурсов.

Есть причина, по которой эти символы локализованы так, как они локализованы, просто не делайте этого.

user2485710
источник
1

Можно нарисовать оба символа с одинаковым стилем и размером шрифта с помощью DrawStringметода. После создания двух растровых изображений с символами их можно сравнивать попиксельно.

Преимущество этого метода в том, что можно сравнивать не только абсолютно одинаковые символы, но и похожие (с определенным допуском).

Иван Кочуркин
источник