一尘不染

如何通过字符比较来执行Unicode感知字符

c#

我的应用程序有一个国际目标,许多国家的人都会使用它,他们将使用自己的语言输入文本(我必须处理的文本)。

例如,如果我必须使用一个字符一个字符地列出两个字符串的差异,那么这个简单的C#代码是否足够?或者我缺少什么?

var differences = new List<Tuple<int, char, char>>();
for (int i=0; i < myString1.Length; ++i)
{
    if (myString1[i] != myString2[i])
        differences.Add(new Tuple<int, char, char>(i, myString1[i], myString2[i]));
}

给定的代码可以有效地以不同的语言执行此任务吗(我的用户不仅 限于 美国字符集)?


阅读 493

收藏
2020-05-19

共1个答案

一尘不染

编码方式

Unicode定义了一个 字符 列表(字母,数字,字母符号,控制代码等),但它们的表示形式(以字节为单位)定义为 encoding
。如今,最常见的Unicode编码是UTF-8,UTF-16和UTF-32。UTF-16通常与Unicode关联,因为它是在Windows,Java,NET环境,C和C
++语言(在Windows上)中为Unicode支持而选择的。请注意,这不是唯一的一种,并且在您的生活中,您肯定还会遇到UTF-8文本(尤其是来自Web和Linux文件系统的文本)和UTF-32(Windows世界之外)。必读的入门文章:绝对最低限度每个软件开发人员绝对,肯定必须了解Unicode和字符集(无借口!)UTF-8无处不在-
宣言
。IMO特别是第二个链接(无论您的意见是UTF-8还是UTF-16)都非常有启发性。

让我引用维基百科:

由于最常用的字符全部在“基本多语言平面”中,因此对代理对的处理通常没有经过全面测试。即使在流行且经过充分审查的应用程序软件(例如CVE-2008-2938,CVE-2012-2135)中,这也会导致持续的错误和潜在的安全漏洞。

要了解问题的出处,仅需一些简单的数学运算即可:Unicode定义了大约11万个代码点(请注意,并非所有代码点都是grapheme)。Windows环境下的C,C
++,C#,VB.NET,Java和许多其他语言中的“
Unicode字符类型”(旧ASP经典页面上的VBScript明显例外)使用UTF-16编码,然后是两个字节(此处的类型名称为直观但完全具有误导性,因为它是一个
代码单位 ,而不是字符或代码点)。

请检查此区别,因为它是基础的:代码单元在逻辑上与字符不同,即使有时它们重合,它们也不是同一回事。这如何影响您的编程生活?假设您有此C#代码,并且您的规范(由考虑字符的
真实 定义的人编写)说“密码长度必须为4个 字符 ”:

bool IsValidPassword(string text ) {
    return text.Length >= 4;
}

该代码是 丑陋的,错误的和残破的Length财产申报数量 代码单位
text字符串变量,现在你知道他们是不同的。您的代码将被验证n̊o̅为有效密码(但它由两个字符,四个代码点组成-
几乎总是与代码单元一致)。现在,试想一下将其应用于您的应用程序的所有层:一个经过UTF-8编码的数据库字段,该字段已通过先前的代码(输入为UTF-16)进行了有效验证,错误将累加,您的波兰朋友
ŚwiętosławKoźmicki
对此不会高兴。现在认为您必须使用相同的技术来验证用户的名字,并且您的用户是中国人(但不用担心,如果您不在乎,那么他们将在很短的时间内成为您的用户)。另一个例子:这种简单的C#算法计算字符串中的不同字符将由于相同的原因而失败:

myString.Distinct().Count()

如果用户输入此汉字字符,𠀑则您的代码将错误地返回… 2,因为它的UTF-16表示形式是0xD840 0xDC11(顺便说一句,由于它们分别是高和低替代,因此每个字符本身都不是有效的Unicode字符)。原因在这篇文章中有更详细的解释,还提供了一个可行的解决方案,因此我在这里重复以下基本代码:

StringInfo.GetTextElementEnumerator(text)
    .AsEnumerable<string>()
    .Distinct()
    .Count();

这大致相当于codePointCount()Java中计数字符串中的代码点的数量。我们需要,AsEnumerable<T>()因为GetTextElementEnumerator()return
IEnumerator而不是IEnumerable,在将字符串拆分为相同长度的块中描述了一个简单的实现(请记住要检查Unicode文本分段的所有规则,例如,如果尝试实现
省略号 算法以进行文本修剪)。

这仅与字符串长度有关吗?当然不是,如果您通过 键盘 Char来处理 输入
,则Char可能需要修复代码。例如,请参阅有关在事件中处理的朝鲜语字符的问题KeyUp

无关,但IMO有助于理解,此C代码(摘自本文)可在char(ASCII
/ ANSI或UTF-8)上运行,但是如果直接转换为use,它将失败wchar_t

wchar_t* pValue = wcsrchr(wcschr(pExpression, L'|'), L':') + 1;

注意,在C
11有一个新的大组类到手柄编码和更清晰的类型别名:char8_tchar16_tchar32_t分别用于,UTF-8,UTF-16和UTF-32编码的字符。要知道,你也有std::u8stringstd::u16stringstd::u32string。请注意,即使length()(及其size()别名)仍返回代码单元的数量,您也可以轻松地使用codecvt()模板功能执行编码转换,并使用IMO这些类型,可以使您的代码更加清晰明了(不会令人惊讶的size()u16string返回的数量char16_t
元素 )。有关C
中字符计数的更多详细信息,请查看此不错的文章。在C中,使用charUTF-8编码会更容易:本文
IMO是必读的。

文化差异

并非所有语言都相似,它们甚至没有共享一些基本概念。例如我们目前的定义字形可以从我们的理念很远字符。让我举一个例子来说明:在韩文韩文中,字母被组合成一个音节(字母和音节都是字符,当单独使用时,以不同的方式表示,并且在与其他字母的单词中表示)。字
古克
)是由三个字母组成一个音节(第一和最后一个字母相同,但他们有不同的声音发音时他们在开始的时候还是一个字的结束,这就是为什么他们音译gk)。

音节让我们引入了另一个概念: 预分解和分解序列 。韩文音节 han
可以表示为一个字符(U+0D55C)或分解的字母序列。例如,如果您正在阅读一个文本文件,则可能同时拥有这两个文件(并且用户可能在输入框中输入了两个序列),但是它们必须比较相等。请注意,如果您依次键入该字母,它们将始终显示为单个音节(复制并粘贴单个字符-
不带空格-并尝试),但最终形式(预分解或分解)取决于您的IME。

在捷克语中,“ ch”是有向图,它被视为单个字母。它有它自己的整理规则(它之间HI),与捷克的排序 fyzika 到来之前 化学
!如果算上Characters并告诉用户 Chechtal
由8个Characters组成,他们会认为您的软件已被窃听,并且您对他们的语言的支持仅限于翻译后的资源。让我们添加例外:在 puchoblík中
(以及其他几个词)CH它们不是有向图,并且它们是分开的。请注意,还有其他情况,例如斯洛伐克语中的“dž”
即使它使用两个/三个UTF-16代码点也算作单个字符!在许多其他语言中(例如,加泰罗尼亚语中的
ll )也是如此。真正的语言比PHP具有更多的例外和特殊情况!

注意单靠外观是不够的等价性,例如:AU+0041大写拉丁字母A)不等同于АU+0410CYRILLIC大写字母A)。相反,字符٢U+0662ARABIC-
INDIC DIGIT TWO)和۲U+06F2EXTENDED ARABIC-INDIC DIGIT
TWO)在视觉上和概念上都是等效的,但它们是不同的Unicode代码点(另请参见有关数字和 同义词的 下一段)。

?!等符号有时被用作字符,例如最早的Haida语言)。在某些语言中(例如最早的美洲印第安人书面形式),数字和其他符号也从拉丁字母中借用并用作字母(请注意这一点,如果您必须处理该语言并且必须从符号中去除字母数字,那么Unicode可以)不能区分这一点),例如
Khoisan非洲语言中的 Kung 。在加泰罗尼亚语中,当 ll
不是二字时,他们使用变音符号(或中点(+U00B7)…)来分隔字符,例如在 cel·les中
(在这种情况下,字符数为6,代码单位/代码点为7,其中假设不存在的词 塞勒 将导致5个字符)。

同一单词可以使用多种形式书写。例如,如果您提供全文搜索,则可能需要考虑一下。例如,中文单词(house)可以在 拼音中 音译为 Jiā
,在日语中,同样的单词也可以用相同的汉字家或 平假名 (以及其他)中的いえ写,或在 romaji中 音译为 ie
。这仅限于文字吗?不,字符也很常见,因为数字很常见:(罗马字母中的阿拉伯数字),阿拉伯语和波斯语以及中文和日文是完全相同的基数。让我们增加一些复杂性:用中文写相同的数字也很常见(简体:
2`` ٢``二``兩``两)。我什至没有提到前缀(微米,纳米,千克等)。有关此问题的真实示例,请参见本文。它不仅限于远东语言:撇号(撇号(U+0027或更好)(U+2019右单引号)经常用在捷克语和斯洛伐克语中,而不是其重叠的对等词(U+02BC修饰符使徒)):
d’ 等价(类似于我说过加泰罗尼亚语的middot)。

也许您应该正确处理要比较的德语小写字母“ ss” ß(并且大小写不敏感的比较会出现问题)。如果您必须提供 不完全
匹配的字符串i及其形式(请参见关于 Case的 部分),则在土耳其语中也会出现类似的问题。

如果您使用的是 专业 文本,则可能还会遇到连字。例如,即使在英语中,“ 美学” 也是9个代码点,但10个字符!例如,对于 ethel
字符–(U+0153拉丁小字体OE,如果您使用法语文本,则必不可少)也是如此; d’ouvre 等同于 d’œvre (也包括 ethel
œthel )。两者都是(连同德语 ß词汇 连字,但您也可能会遇到 印刷 连字(例如 U+FB00LATIN SMALL
LIGATURE FF),它们在Unicode字符集中是自己的一部分( 演示文稿
)。如今,甚至在英语中,变音符号也变得更加普遍(请参阅tchrist的帖子中有关摆脱打字机暴政的人们的信息,请仔细阅读Bringhurst的引用)。您是否认为您(和您的用户)永远不会键入
façade朴素prêt-à-porter 或“经典” noöne合作方式

在这里,我什至没有提到 单词计数,
因为它会带来更多问题:在韩语中,每个单词都是由音节组成的,但是在例如中文和日语中,字符被视为单词(除非您想使用一本字典)。现在让我们采用这个中文句子:这是一个示例文本rougly等效于日语句子これは,サンサルのテキストです。您如何计算它们?此外,如果将他们音
译成ShìyīgèshìlìwénběnKore wa,sanpuru no tekisutodesu,
那么应该在文本搜索中对它们进行匹配吗?

在谈到日本:全宽拉丁字符不同半宽的字符,如果你输入的是日本的 罗马字
的文本,你必须处理这个,否则你的用户会感到惊讶时,不会比等于T(在这种情况下应该是什么只是字形成了代码点)。例如,如果要提供要翻译的markdown文件,请记住这一点,因为[name](link)解析可能因此而中断。

好,这足以突出问题 表面 吗?

重复字符

Unicode(出于ASCII 兼容性 和其他历史原因,主要是Unicode
)具有重复的字符,在进行比较之前,您必须执行规范化,否则à(单个代码点)将不等于a加上U+0300COMBINING
GRAVE ACCENT)。这是一个罕见的角落吗?不完全是,还可以看看乔恩·斯基特(Jon
Skeet)的这个真实示例。同样(参见 文化差异
小节)预先分解的序列引入 重复 序列。

请注意,变音符号不仅是混乱的根源。当用户使用键盘打字时,他可能会输入'U+0027APOSTROPHE),但它也应该与排版中通常使用的U+2019RIGHT
SINGLE MARK)匹配(对于许多Unicode符号而言,这都是正确的,从用户的角度来看几乎是等效的,但在版式,想象在电子书中写文本搜索)。

简而言之,如果两个字符串在 规范上是等价的
,则必须被视为相等(即使它们是由不同的Unicode代码点组成的),如果它们具有相同的语言含义和外观,则它们在 规范上 是等价的。

案件

如果您必须执行不区分大小写的比较,那么您将 遇到 更多 问题
。我假设您不会使用toupper()或等价物来进行业余爱好者不区分大小写的比较,除非所有人都想向您的用户解释为什么'i'.ToUpper() != 'I'使用土耳其语I不是大写字母iİ。BTW小写字母Iı)。

另一个问题是德语中的 eszett ß(在古时使用长字+短字的连字-在英语中也提升为人物的尊严)。它具有大写版本,但(此时).NET
Framework错误返回"ẞ" != "ß".ToUpper()(但在某些情况下必须使用它,另请参见此文章)。不幸的是,并非总是
ss 变为 (大写),并非总是 ss 等于 ß (小写),并且 sz 有时也等于 。令人困惑,对不对?

全球化不仅涉及文本,还涉及日期和日历,数字格式和解析,颜色和布局。一本书不足以描述您应该关注的所有事情,但是我在这里要强调的是,很少有本地化的字符串无法使您的应用程序为国际市场做好准备。

即使只是文本,也会出现更多 问题 :这对正则表达式如何适用?应该如何处理空间?是 全角空格 等于一个 半角空格 ?在 专业
应用程序中,应如何将“美国”与“美国”进行比较(在自由文本搜索中)?站在同一条思路上:相比之下,如何管理变音符号?

如何处理文字储存空间?忘记您可以安全地 检测 编码了,要打开一个文件,您需要知道其编码。当然,除非你打算做像HTML解析器<meta charset="UTF-8">或XML / XHTML encoding="UTF-8"<?xml>)。

历史的“介绍”

我们在监视器上看到的文本只是计算机内存中的一小部分字节。按照惯例,每个值(或一组值,如int32_t代表一个数字)代表一个 字符
。然后如何在屏幕上绘制该字符的方法委托给其他对象(为简化起见,请考虑一下 font )。

如果我们任意决定每个字符与一个字节表示那么我们有可用的256个符号(如当我们使用int8_tSystem.SBytejava.lang.Byte为一些我们具有256个值的数值范围)。现在,我们需要确定每个值代表哪个字符,例如ASCII(限制为7位,128个值)以及
自定义 扩展名,以便也使用高128个值。

做到了,habemus 字符编码
为256个符号(包括字母,数字,字母字符和控制代码)。是的,每个ASCII扩展名都是专有的,但内容清晰且易于管理。文本处理非常普遍,以至于我们只需要用我们喜欢的语言添加一个适当的数据类型(char在C
语言中,请注意,它不是形式的别名,unsigned char或者signed char是一个不同的类型char在Pascal中;character在FORTRAN中等等)以及很少的库函数来管理。

不幸的是,这并不容易。ASCII仅限于非常基本的字符集,并且仅包含美国使用的拉丁字符(这就是其首选名称应为usASCII的原因)。它是如此有限,以至于甚至连带有变音符号的英语单词也不被支持(如果这改变了现代语言,反之亦然,则是另外一个故事)。您还会看到它还有其他问题(例如,排序错误和序数和字母比较问题)。

怎么处理呢?引入一个新概念: 代码页
。保留一组固定的基本字符(ASCII),并添加每种语言专用的另外128个字符。值0x81将代表西里尔字母Б(在DOS代码页866中)和希腊字符Ϊ(在DOS代码页869中)。

现在出现了严重的问题:1)您不能在同一文本文件中混合使用不同的字母。2)为了正确 理解
文本,您还必须知道用哪个代码页表示。哪里?没有标准的方法,您必须处理这个询问用户或进行合理的猜测(?!)。即使是现在,ZIP文件的“格式”文件名也仅限于ASCII(您可以使用UTF-8-稍后再见-
但它不是标准的-
因为没有标准的ZIP格式)。在本文中,一个Java工作解决方案。3)即使代码页也不是标准的,并且每个环境都有不同的设置(甚至DOS代码页Windows代码页是不同的),名称也有所不同。4)255个字符对于例如中文或日语来说仍然太少,因此引入了更复杂的编码(例如Shift
JIS
)。

当时(〜1985年),形势非常糟糕,绝对需要一个标准。ISO / IEC
8859
到达了,并且至少解决了先前问题列表中的第3点。第1、2和4点仍未解决,需要一个解决方案(特别是如果您的目标不仅是原始文本,而且是
特殊的
印刷字符)。如今,这个标准(经过多次修订)仍然存在(并且在某种程度上与Windows-1252代码页一致),但是除非您正在使用某些旧系统,否则您可能永远不会使用它。

使我们摆脱这种混乱的标准是众所周知的: Unicode
。来自维基百科

Unicode是一种计算行业标准,用于对世界上大多数书写系统中表示的文本进行一致的编码,表示和处理。最新的Unicode版本包含超过110,000个字符,涵盖100个脚本和多个符号集。

语言,库,操作系统已更新,以支持Unicode。现在我们有了所需的所有字符,每个字符都有一个共享的知名代码,过去只是一场噩梦。更换charwchar_t(并接受住在一起wcoutwstring和朋友),只需使用System.Charjava.lang.Character和生活幸福。对?

没有。 从来没有那么容易。Unicode的使命是 “ …编码,表示和处理文本…” ,它不会将不同的文化 转换
和适应成抽象代码(除非您杀死各种各样的美,否则不可能做到这一点。我们的语言)。此外,编码本身引入了一些我们必须关心的事情(不是那么明显吗?!)。

2020-05-19