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

103

我遇到了一个令人惊讶的问题。

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

我意识到,即使两个文本内容相同,比较的结果也是错误的。

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

后来的行中复制粘贴了字符 µ。

然而,这些可能不是唯一看起来相同但实际上不同的字符。

C# 中有没有比较这些外观相同但实际上不同的字符的方法?


165
看起来你找到了薛定谔的μ。 - BoltClock
19
它们是不同的字符 - 尽管它们看起来相同,但它们有不同的字符代码。 - user2864740
98
欢迎来到Unicode。 - ta.speot.is
12
你想要实现什么?是让那两个字符即使编码不同但显示相同吗? - Jade
30
“看起来像”和“看起来一样”是模糊的概念。它们指的是字形的相同还是接近相似?接近到什么程度?请注意,两个字符在某些字体中可能具有相同的字形,在另一些字体中非常相似,在另一个字体中则完全不同。重要的是为什么你会做这样的比较以及在哪种情况下(以及误判率和漏判率的可接受程度)。 - Jukka K. Korpela
显示剩余19条评论
10个回答

151

因为它们是不同的符号,即使它们看起来相同,第一个是实际的字母并具有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
根据维基百科,这是出于历史原因。 - BoltClock
12
Unicode包含了许多兼容字符,这些字符从旧的字符集(比如ISO 8859-1)中引入,以便更轻松地进行转换。在字符集被限制为8位时,它们会包含一些最常见的数学和科学用途的符号(比如一些希腊字母)。基于外观的符号重用是常见的,因此没有添加专门的“K”。但这始终是一种变通方法;“微”符号的正确符号是实际的希腊小写字母μ,欧姆符号的正确符号是实际的大写希腊字母Ω,等等。 - VGR
8
这句话暂无法翻译,因为它可能是一句英语的俚语或者谐音梗,缺乏上下文和背景信息,无法确定其确切含义。请提供更多信息以便我更好地为您服务。 - paulm
11
麦片有特别的K吗? - user764357
显示剩余6条评论

127
在许多情况下,您可以在比较Unicode字符之前将它们都规范化到特定的规范化形式,这样它们就应该能够匹配。当然,您需要根据字符本身来确定需要使用哪种规范化形式;仅仅因为它们看起来相似并不意味着它们代表相同的字符。您还需要考虑是否适用于您的使用场景-请参阅Jukka K. Korpela的评论。
对于这种特殊情况,如果您参考Tony的答案中的链接,您会发现U+00B5的表格上写着:

分解 <compat> 希腊小写字母μ(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.NormalizationFormUnicode规范


26
感谢提供 Unicode 规范链接。这是我第一次阅读它。从中了解到一个小提示:"Normalization Forms KC 和 KD 不应盲目应用于任意文本...最好将这些规范形式视为大写或小写映射:在某些情况下用于识别核心含义,但也对文本进行修改,这可能并不总是适当的。" - user2864740

86

他们两个有不同的字符编码:点击此处了解更多细节

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

图片


40

针对特定的例子,例如μµ,后者具有到前者的兼容分解,因此您可以将字符串 标准化FormKCFormKD,以将微符号转换为mu。

然而,有许多一组看起来相似但在任何Unicode标准化形式下都不等效的字符。例如,A(拉丁字母)、Α(希腊字母)和А(西里尔字母)。 Unicode网站上有一个confusables.txt文件,其中列出了这些字符,旨在帮助开发人员防范同形攻击。如有必要,您可以解析此文件并构建用于“视觉标准化”字符串的表格。


当使用Normalize时,这是绝对需要知道的。它们保持不同似乎令人惊讶。 - user2864740
4
如果大写希腊字母tau与罗马字母T没有保持区别,则希腊文和罗马文本在按字母顺序排序时会非常困难。此外,如果字体为希腊字母和罗马字母使用不同的视觉风格,则类似于罗马字母形状的希腊字母与那些不相似的字母之间使用不同的呈现方式会非常干扰。 - supercat
8
更重要的是,统一欧洲字母表将使 ToUpper / ToLower 难以实现。你需要让 "B".ToLower() 在英语中成为 b,在希腊语中成为 β,在俄语中成为 в。目前只有土耳其语(无点的 i)和其他几种语言需要与默认大小写规则不同的规则。 - dan04
@dan04:我想知道是否有人曾考虑为土耳其语中四种变体的“i”和“I”分配唯一的代码点?这将消除toUpper/toLower行为中的任何歧义。 - supercat

36

搜索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个赞?它并没有回答这个问题(“如何比较Unicode字符”),只是评论了为什么这个特定的例子不相等。最好的情况下,它应该作为一个对问题的评论。我知道评论格式选项不允许像答案格式选项那样漂亮地发布它,但这不应该成为将其发布为答案的有效理由。 - Konerak
6
实际上,问题是关于为什么μ和µ的相等性检查会返回false,这个回答解决了这个问题。后来提问者又问了另一个问题,即如何比较两个看起来相似的字符。这两个问题都有最佳答案,后来管理员将两个问题合并,并选择第二个问题的最佳答案作为最佳答案。有人编辑了这个问题,以便总结。 - Subin Jacob
实际上,在合并之后我没有添加任何内容。 - Subin Jacob

24

编辑:在将此问题与如何在C#中比较'μ'和'µ'合并后,发布的原始答案如下:

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

编辑 阅读评论后,是的,使用上述方法可能会为某些其他类型的输入提供错误的结果,因此我们应该使用wiki中提到的完全兼容分解的normalize。 (感谢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+FB03的NFKC归一化中,对U+0066(f)的子字符串进行搜索将成功,但在U+FB03的NFC归一化中将不会成功。因此,为了比较等价性,我们通常应该使用FormKC即NFKC归一化或FormKD即NFKD归一化。我有点好奇想更多地了解所有Unicode字符,所以我制作了一个示例,可以迭代UTF-16中的所有Unicode字符,并得到了一些结果,我想讨论一下。
  • 关于字符的信息,它们的FormCFormD规范化值不相等
    总计: 12,118
    字符(int值): 192-197,199-207,209-214,217-221,224-253,..... 44032-55203
  • 关于字符的信息,它们的FormKCFormKD规范化值不相等
    总计: 12,245
    字符(int值): 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规范化值相等
    总计: 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 '㌷'
    这些链接可以帮助您了解Unicode等价性的规则:
    1. Unicode_equivalence
    2. Unicode_compatibility_characters

    请注意,保留原文中的HTML标签。

4
奇怪但有效...我的意思是它们是两个具有不同含义的字符,转换为大写后它们变得相等?我不明白逻辑,但是解决方法不错+1。 - BudBrot
45
这个解决方案掩盖了问题,并且在一般情况下可能会引起问题。这种测试会发现 "m".ToUpper().Equals("µ".ToUpper());"M".ToUpper().Equals("µ".ToUpper()); 也是正确的,这可能不是想要的结果。 - Andrew Leach
6
这是个糟糕的想法。不要像这样使用Unicode。 - Konrad Rudolph
1
不要使用基于 ToUpper() 的技巧,为什么不使用 String.Equals("μ", "μ", StringComparison.CurrentCultureIgnoreCase)? - svenv
6
区分“MICRO SIGN”和“GREEK SMALL LETTER MU”的一个很好的理由是为了说明“MICRO SIGN”的大写形式仍然是“MICRO SIGN”。但是,当将小写“micro”变成大写时,它会变成“mega”,祝工程愉快。 - Greg
显示剩余2条评论

9

很可能存在两个不同的字符编码呈现相同的字符,虽然从技术上讲它们并不相等,但它们看起来是相等的。查看字符表,看看是否存在该字符的多个实例。或者在代码中打印出这两个字符的字符编码。


6
你问“如何比较它们”,但你没有告诉我们你想做什么。
至少有两种主要方法可以比较它们:
一种是直接比较它们,因为它们不同;
另一种是使用Unicode兼容性规范化,如果您需要进行匹配查找,则可以进行比较。
不过可能会有问题,因为Unicode兼容性规范化将使许多其他字符相等。如果您只想让这两个字符被视为相似,那么您应该自己编写规范化或比较函数。
对于更具体的解决方案,我们需要了解您遇到此问题的特定情况。在什么情况下出现这个问题?

1
“微符号”和小写的μ字符在规范上是等价的吗?使用规范化可以得到更严格的比较。 - Tanner Swett
@TannerL.Swett:其实我甚至不确定如何立刻检查这个问题…… - hippietrail
1
实际上,我正在导入一个包含物理公式的文件。你关于标准化的说法是正确的。我需要更深入地研究它。 - D J
什么类型的文件?是由个人手工制作的纯Unicode文本文件吗?还是由应用程序以特定格式输出的文件? - hippietrail

5

如果我想要严格一点,我会说你的问题没有意义,但既然我们正在接近圣诞节,鸟儿在唱歌,我就继续回答。

首先,你试图比较的两个实体是“字形”,一个字形是由通常被称为“字体”的一组字形提供的,这个东西通常以ttfotf或其他文件格式的形式出现。

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

通常用于解决类似于你遇到的问题的方法是OCR,本质上是一种识别和比较字形的软件,如果C#默认提供OCR,我不知道,但如果你不真正需要OCR并且知道如何使用它,这通常是一个非常糟糕的想法。

你可能会将一本物理书解释成古希腊书籍,而不提及OCR通常在资源方面昂贵的事实。
这些字符被本地化的方式有其原因,不要这样做。

2
使用DrawString方法可以使用相同的字体风格和大小绘制两个字符。生成具有符号的两个位图后,可以逐像素进行比较。
这种方法的优点是,您不仅可以比较绝对相等的字符,还可以比较相似的字符(带有明确定义的容差)。

这个答案是无意义的。如果你有数百个字符串的列表,这将非常慢。 - Elmue

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接