如何比较“看起来相似”的Unicode字符?

我陷入了一个令人惊讶的问题。

我在我的应用程序中加载了一个文本文件,我有一些比较μ值的逻辑。

而且我意识到,即使文本相同,比较值也是错误的。

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

在后面的行中,字符μ被复制粘贴。

但是,这些可能不是唯一的字符。

在C#中有什么方法来比较看起来相同但实际上不同的字符?

在许多情况下,您可以在比较之前将两个Unicode字符归一化为特定的规范化表单,并且它们应该能够匹配。 当然,你需要使用哪种规范化forms取决于angular色本身; 只是因为它们看起来相似并不一定意味着它们代表了相同的性格。 您还需要考虑是否适合您的使用情况 – 请参阅Jukka K. Korpela的评论。

对于这个特殊的情况,如果你参考Tony的答案中的链接,你会发现U + 00B5的表格说:

分解<compat>希腊小字母MU(U + 03BC)

这意味着U + 00B5,原始比较中的第二个字符,可以分解为U + 03BC,第一个字符。

所以你会使用完全兼容性分解对字符进行规范化,使用规范化formsKC或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规范化和不同规范化forms的详细信息,请参阅System.Text.NormalizationForm和Unicode规范 。

因为它们确实是不同的符号,即使它们看起来相同,首先是实际的字母,并且具有字符code = 956 (0x3BC) ,第二个是微型符号并具有181 (0xB5)

参考文献:

  • Unicode字符'希腊小字母MU'(U + 03BC)
  • Unicode字符'MICRO SIGN'(U + 00B5)

所以如果你想比较它们,你需要它们是平等的,你需要手动处理它,或者在比较之前用另一个字符replace一个字符。 或者使用下面的代码:

 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); } 

和演示

他们都有不同的字符代码: 请参阅这个更多的细节

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

哪里,第一个是:

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

图片

对于μ (μ)和µ (微符号)的具体例子,后者与前者具有兼容性分解 ,因此您可以将string规范化为FormKCFormKD以将微标志转换为mus。

但是,在任何Unicode规范化forms下,有许多字符集看起来相似,但并不等同。 例如, A (拉丁语), A (希腊语)和А (西里尔语)。 Unicode网站有一个confusables.txt文件,其中列出了这些文件,旨在帮助开发人员防范同形异义(homograph)攻击 。 如果有必要的话,你可以parsing这个文件并为string的“可视化规范化”build立一个表格。

在Unicode数据库中 search这两个字符,并查看其差异

一个是希腊字母 µ ,另一个是Micro µ

 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) 

编辑这个问题合并后, 如何比较“μ”和“μ”在C#
原帖回复:

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

编辑阅读注释后,是的,这是不好的,因为它可能会提供一些其他types的input错误的结果,因此,我们应该使用维基中提到的完全兼容性分解使用规范化 。 (感谢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_eivaivalence信息时,我发现

等值标准的select会影响search结果。 例如U + FB03(ffi),…这样的U + 0066(f)这样的印刷string,在U + FB03的NFKC标准化中search 成功 ,但U + FB03的NFC标准化不成功

所以为了比较等价性,我们通常应该使用FormKC即NFKC标准化或FormKD即NFKD标准化。
我有点好奇,想知道更多关于所有的Unicode字符,所以我做了一个样本,它将遍历UTF-16所有Unicode字符,并且我得到了一些我想讨论的结果

  • 关于FormCFormD标准化值不相同的字符的信息
    Total: 12,118
    Character (int value): 192-197, 199-207, 209-214, 217-221, 224-253, ..... 44032-55203
  • 关于FormKCFormKD标准化值的字符信息不相同
    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
  • FormCFormD标准化值的所有字符都不相同, FormKCFormKD标准化值也是不相同的,除了这些字符
    字符: 901 '΅', 8129 '῁', 8141 '῍', 8142 '῎', 8143 '῏', 8157 '῝', 8158 '῞'
    , 8159 '῟', 8173 '῭', 8174 '΅'
  • FormKCFormKD归一化值的额外字符不等价,但是FormCFormD归一化值是等价的
    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' 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'
  • 有些字符不能被标准化 ,如果尝试的话,会抛出ArgumentExceptionexception
    Total:2081 Characters(int value): 55296-57343, 64976-65007, 65534

这个链接对于理解Unicode等价的规则是非常有帮助的

  1. Unicode_equivalence
  2. Unicode_compatibility_characters

最有可能的是,有两个不同的字符代码,使(可见)相同的字符。 尽pipe技术上并不相同,但它们看起来是平等的 看看angular色表,看看是否有多个angular色的实例。 或者在代码中打印两个字符的字符代码。

你问“如何比较”,但你不告诉我们你想做什么。

至less有两种主要的方法来比较它们:

要么你直接比较它们,因为它们是不同的

或者,如果您需要进行比较以find匹配,则使用Unicode兼容性标准化。

虽然Unicode兼容性规范化会使许多其他字符比较相同,但可能会有问题。 如果你只想把这两个字符看作是一样的,你应该推出你自己的规范化或比较函数。

对于更具体的解决scheme,我们需要了解您的具体问题。 你遇到这个问题的背景是什么?

如果我想迂腐,我会说你的问题没有意义,但是因为我们正在接近圣诞节,鸟儿在唱歌,所以我会继续这样做。

首先,你试图比较的2个实体是glyph ,字形是通常被称为“字体”的一组字形的一部分,通常在ttfotf或任何文件中出现你正在使用的格式。

字形是给定符号的表示forms,由于它们是依赖于特定集合的表示forms,所以不能期望具有2个相似或甚至更好的相同符号,这是一个没有意义的短语如果你考虑上下文,你至less应该在制定这样一个问题时指定你正在考虑的字体或字形集。

什么是通常用来解决类似的问题,你遇到的,这是一个OCR,本质上是一个软件,识别和比较字形,如果C#默认提供了一个OCR我不知道,但它通常是一个非常糟糕的想法,如果你不需要一个OCR,你知道如何处理它。

你可能最终将一本物理学书籍解释为一本古希腊的书,而没有提到OCR在资源方面通常是昂贵的。

有一个原因是为什么这些字符是本地化的方式,只是不这样做。

使用DrawString方法可以绘制两个字体样式和大小相同的字符。 在生成了两个带有符号的位图之后,可以逐个像素地进行比较。

这种方法的好处是,你不仅可以比较绝对相等的字符,而且可以比较相似(具有一定的容忍度)。