如何通过字符比较来执行一个Unicode意识的字符?
我的应用程序有一个国际性的目标,许多国家的人都会使用它,他们会用自己的语言input文本(我必须处理的文本)。
例如,如果我不得不使用一个字符来比较两个string的差异,那么这个简单的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])); }
被赋予有效的代码以不同的语言执行这个任务(我的用户不限于US字符集)?
编码
Unicode定义了一个字符列表(字母,数字,analphabetic符号,控制代码和其他),但它们的表示(以字节为单位)被定义为编码 。 目前最常见的Unicode编码是UTF-8,UTF-16和UTF-32。 UTF-16通常与Unicode相关,因为它是Windows,Java,NET环境,C和C ++语言(在Windows上)为Unicode支持而select的。 请注意,这并不是唯一一个,在您的生活中,您肯定也会遇到UTF-8文本(尤其是来自网页和Linux文件系统)和UTF-32(Windows世界之外)。 一个非常介绍性的必须阅读文章: 绝对最低限度每个软件开发人员绝对,积极必须知道Unicode和字符集(没有借口!)和UTF-8无处不在 – 宣言 。 国际海事组织特别是第二个链接(不pipe你的意见UTF-8与UTF-16)是相当有启发性的。
让我引用维基百科:
由于最常用的字符都在基本多语言平面中,所以对代理对的处理往往没有经过彻底的testing。 即使在stream行且经过充分审查的应用软件(例如,CVE-2008-2938,CVE-2012-2135)中,这也会导致持续的错误和潜在的安全漏洞。
要看看问题出在哪里,只需从一些简单的math开始:Unicode定义了大约110K个代码点(注意并不是所有的都是字形 )。 C,C ++,C#,VB.NET,Java和Windows环境下的许多其他语言的“Unicode字符types”(旧ASP经典页面上VBScript的明显例外)是UTF-16编码,然后是两个字节(types名称是直观,但完全误导,因为它是一个代码单元 ,而不是一个字符或代码点)。
请检查这个区别,因为它是基本的:一个代码单元在逻辑上与一个字符不同,即使有时它们一致,它们也不是同一个东西。 这如何影响你的编程生活? 想象一下,你有这个C#代码和你的规范(由思考真正的字符定义的人写的)说:“密码长度必须是4个字符 ”:
bool IsValidPassword(string text ) { return text.Length >= 4; }
该代码是丑陋的,错误的和破碎的 。 Length
属性返回text
stringvariables中代码单元的数量,现在你知道它们是不同的。 你的代码将validationn̊o̅为有效的密码(但是它是由两个字符,四个代码点 – 几乎总是与代码单元一致)。 现在试着想象这适用于你的应用程序的所有层次:一个UTF-8编码的数据库字段用前面的代码(其中input是UTF-16)进行了validation,错误将加起来,你的波兰朋友ŚwiętosławKoźmicki不会高兴。 现在认为你必须用相同的技术来validation用户的名字,而你的用户是中国人(但是不要担心,如果你不在乎,那么他们将是你的用户很短的时间)。 另一个例子:这个幼稚的C#algorithm来计算string中的不同字符将失败,原因相同:
myString.Distinct().Count()
如果用户input这个汉字𠀑,那么你的代码会错误地返回… 2,因为它的UTF-16表示是0xD840 0xDC11
(顺便说一句,它们每一个都不是一个有效的Unicode字符,因为它们分别是高和低的代理)。 原因在这篇文章中有更详细的解释,也提供了一个工作解决scheme,所以我在这里重复一下基本的代码:
StringInfo.GetTextElementEnumerator(text) .AsEnumerable<string>() .Distinct() .Count();
这大致相当于Java中的codePointCount()
来计算string中的代码点。 我们需要AsEnumerable<T>()
因为GetTextElementEnumerator()
返回IEnumerator
而不是IEnumerable
,在一个简单的实现中, 将string拆分为长度相同的块 。
这只是与string长度有关吗? 当然不是,如果你通过Char
处理键盘input Char
,你可能需要修复你的代码。 请参阅有关在KeyUp
事件中处理韩文字符的示例。
无关的,但IMO有助于理解,这个C代码(取自这个post )在char
(ASCII / ANSI或UTF-8)上工作,但直接转换为使用wchar_t
会失败:
wchar_t* pValue = wcsrchr(wcschr(pExpression, L'|'), L':') + 1;
请注意,在C ++ 11中,有一组新的类来处理编码和更清晰的types别名:分别为UTF-8,UTF-16和UTF-32编码字符的char8_t
, char16_t
和char32_t
。 请注意,你也有std::u8string
, std::u16string
和std::u32string
。 请注意,即使length()
(和size()
别名)仍然会返回代码单元的计数,您可以使用codecvt()
模板函数轻松执行编码转换,并使用这些types的IMO,您将使代码更加清晰明确不是惊人的size()
u16string
将返回char16_t
元素的数量)。 有关在C ++字符计数的更多细节检查这个不错的职位 。 在C中,使用char
和UTF-8编码可以让事情变得更容易: 这个后期的 IMO是必读的。
文化差异
并非所有的语言都是相似的,他们甚至不共享一些基本的概念。 例如,我们目前的字形定义可能与我们的字符概念相差甚远。 让我以一个例子来解释一下:在韩文中,韩文字母组合成一个单音节 (而且字母和音节都是字符,只是单独用一种不同的方式表示,用一个字母来表示)。 Word 국 ( Guk )是由三个字母ㄱ , ㅜ和composed组成的一个音节(第一个和最后一个字母是相同的,但是当它们在单词的开头或结尾时,它们会用不同的声音发音),这就是为什么它们是音译g和k )。
音节让我们介绍另一个概念: 预分解和分解序列 。 韩语音节可以表示为单个字符( U+0D55C
)或字母ᄒ , ᅡ和decom的分解序列。 例如,如果你正在阅读一个文本文件,你可能同时拥有这两个文本文件(用户可能会在input框中input两个序列),但它们必须相同。 请注意,如果您按顺序键入这些字母,它们将始终显示为单音节(复制并粘贴单个字符 – 不带空格 – 尝试),但最终forms(预分解或分解)取决于您的IME。
在捷克语中,“ch”是一个有向图,它被视为单个字母。 它有它自己的规则整理(这是在H和我之间),与捷克sortingfyzika之前化学 ! 如果你算字符,你告诉你的用户单词Chechtal由8个字符组成,他们会认为你的软件被窃听,你对他们的语言的支持仅限于一堆翻译的资源。 让我们来补充例外:在puchoblík (和其他几个字) C和H不是有向图,它们是分开的。 请注意,在斯洛伐克还有其他一些情况,如斯洛伐克的“dž”,即使使用两个/三个UTF-16编码点,它也被视为单个字符。 在其他许多语言中也是如此(例如加泰罗尼亚语中的ll )。 真正的语言比PHP有更多的例外和特殊情况!
请注意,单凭外表并不总是足够等效的,例如: A ( U+0041
拉丁文大写字母 A)不等于А ( U+0410
大写字母 A)。 相反,字符2 ( U+0662
ARABIC-INDIC DIGIT TWO)和2 ( U+06F2
EXTENDED ARABIC-INDIC DIGIT TWO)在视觉上和概念上是等同的,但是它们是不同的Unicode代码点(另见关于数字和同义词的下一段)。
符号像? 和! 有时用作字符,例如最早的海达语 )。 在某些语言(如最早的美洲原住民语言的书面forms)中,数字和其他符号也从拉丁字母中借用,并用作字母(如果必须处理该语言,则必须从字符中去除字母数字,Unicode可以“不能区分这一点),举一个例子! Khoisan非洲语言中的Kung 。 在加泰罗尼亚语中,当ll不是有向图时,它们使用一个变音符号(或一个+U00B7
符号( +U00B7
)…)来分隔字符,就像在cell中一样 (在这种情况下,字符数是6,代码单元/代码点是7一个假设的不存在的单词celles会导致5个字符)。
相同的单词可以用多种forms书写。 例如,如果您提供全文search,则可能需要关注这些内容。 例如中文单词家(house)可以拼音为Jiā ,日文同样的单词也可以用同样的汉字家或平假名 (也可以是其他) 写成 ,或者以romaji的forms音译。 这是有限的话吗? 不,数字也是很常见的: 2 (阿拉伯数字,罗马字母), 2 (阿拉伯语和波斯语)和二 (中文和日文)是完全相同的基数。 让我们来添加一些复杂性:用中文写同样的数字也是很常见的两个(简化:两个)。 我甚至不提前缀(微,纳,千等)。 看到这个职位的真实世界这个问题的例子。 它不仅限于远东语言:撇号( U+0027
APOSTROPHE或更好的( U+2019
右单引号)经常在捷克和斯洛伐克使用,而不是叠加的对应U+02BC
( U+02BC
MODIFIER LETTER U+02BC
) d'是相当的(类似于我在加泰罗尼亚语中所说的)。
也许你应该正确处理德语中的小写“ss”来与ß相比较(对于不区分大小写的比较,会出现问题)。 类似的问题是土耳其语,如果你必须提供一个非精确的string匹配我和它的forms(请参阅关于案例部分)。
如果您使用的是专业文字,您也可能会遇到连字; 即使是英文,例如æsthetics是9码点,但10个字符! 同样适用于,例如ethel字符 – ( U+0153
拉丁小写字母OE,如果您正在使用法语文本,绝对必要); 马 ðvou相当于马d'œvre (但ethel和œthel )。 两者都是(连同德语ß ) 词汇连字,但你也可能会遇到印刷连字(例如U+FB00
LATIN SMALL LIGATURE FF),并且它们在Unicode字符集( 表示forms )上是它们自己的一部分。 现在,即使在英语中,变音符号也是比较常见的(请参阅特里斯特里特关于人们摆脱打字机暴政的post,请仔细阅读Bringhurst的引文)。 你是否认为你(和你的用户)不会input外观 , 天真和成熟 的门房或“优雅”的noöne或cooperation ?
在这里,我甚至不提到单词计数,因为它会带来更多的问题:在韩语中,每个单词由音节组成,但是例如中文和日文,字符被计算为单词(除非要使用单词计数一本字典)。 现在,让我们来看看这个中文句子:这是一个例子文本,相当于日语句子これは,サンプルのテキストです。 你怎么数他们? 另外,如果他们音译为Shìyīgèshìlìwénběn和Kore wa,sanpuru no tekisutodesu那么他们应该在文本search匹配?
说到日语:全angular拉丁字符不同于半angular字符,如果你的input是日本罗马字的文本,你必须处理这个,否则你的用户会惊讶当T不会等于T (在这种情况下应该是什么字形成为代码点)。
好的,这足以突出问题的表面 ?
重复的字符
Unicode(主要用于ASCII 兼容性和其他历史原因)具有重复的字符,在进行比较之前,您必须执行标准化,否则à (单个代码点)将不等于à (加U+0300
联合GRAVE ACCENT)。 这是一个不常见的情况吗? 不是真的,也请看一下Jon Skeet的这个现实世界的例子 。 另外(见文化差异部分)预分解和分解序列引入重复 。
请注意,变音符号不仅是混淆的来源。 当用户用他的键盘input时,他可能会input' ( U+0027
APOSTROPHE),但它也应该匹配通常在排版中使用的' ( U+2019
右单引号)(许多Unicode符号几乎相同从用户的angular度来看,但在排版方面不同,想象在数字图书里面写文本search)。
简而言之,如果两个string具有相同的语义意义和外观,即使它们是由不同的Unicode代码点组成的,它们也是正常等价的,并且是正则等价的 ,但它们必须被认为是相等的(这是一个非常重要的概念!
案件
如果你不得不执行不区分大小写的比较,那么你将会遇到更多的问题 。 我假设你不使用toupper()
或等价物执行业余爱好者大小写比较,除非你想向用户解释为什么'i'.ToUpper() != 'I'
是土耳其语 ( 我不是上层我的情况是İ。BTW小写字母为我是我 )。
另一个问题是德语中的eszettß (古代用的long s + short s的连词 – 也是用英语提升到了angular色的尊严)。 它有一个大写的版本ẞ但是(在这个时候).NET Framework错误地返回"ẞ" != "ß".ToUpper()
(但是在某些场景中它的使用是强制性的 ,请参阅这篇文章 )。 不幸的是,并不总是ss变成了upper(大写),并不总是ss等于ß (小写),而且sz有时也是upper大写。 令人困惑,对吧?
更
全球化不仅仅是关于文本:date和日历,数字格式和parsing,颜色和布局如何? 一本书不足以描述你应该关心的所有事情,但是我要强调的是,很less的本地化string不会让你的应用程序准备好进入国际市场。
即使只是文本,也会出现更多的问题 :这是如何适用于正则expression式? 如何处理空间? em空间是否等于en空间 ? 在一个专业的应用程序中,“USA”应该如何与“USA”进行比较(在自由文本search中)? 在同样的思路上:如何pipe理变音符号呢?
如何处理文本存储? 忘记你可以安全地检测编码,打开一个文件,你需要知道它的编码。 当然,除非你打算在<?xml>
使用<meta charset="UTF-8">
或XML / XHTML encoding="UTF-8"
)的HTMLparsing器。
历史“介绍”
我们在监视器上看到的文本只是计算机内存中的一大块字节。 按照惯例,每个值(或一组值,如int32_t
代表一个数字)表示一个字符 。 如何在屏幕上绘制这个字符被委托给别的东西(简化一点思考一个字体 )。
如果我们任意决定每个字符用一个字节表示,那么我们有256个符号(当我们使用int8_t
, System.SByte
或java.lang.Byte
代表一个数字,我们有一个数值范围为256的值)。 我们现在需要决定每个值代表哪个字符,例如ASCII (限制为7位,128个值), 自定义 扩展也可以使用128个上限值。
这样做了 ,为256个符号的habemus 字符编码 (包括字母,数字,analphabetic字符和控制代码)。 是的,每个ASCII扩展名都是专有的,但事情很清楚,易于pipe理。 文本处理非常普遍,我们只需要在我们最喜欢的语言中添加一个合适的数据types(C中的char
,注意,它不是unsigned char
或signed char
的别名,而是一个不同的types ; Pascal中的char
; FORTRAN中的character
和等等)和很less的库函数来pipe理。
不幸的是,这并不容易。 ASCII仅限于一个非常基本的字符集,它只包含在美国使用的拉丁字符(这就是为什么它的首选名称应该是usASCII)。 这是有限的,即使英语单词与变音标记不支持(如果这使得现代语言的变化,反之亦然是另一回事 )。 你会看到它也有其他问题(例如,它的sorting顺序错误, 顺序和字母比较的问题)。
如何处理? 介绍一个新的概念: 代码页 。 保留一组固定的基本字符(ASCII),并为每种语言添加另外128个字符。 值0x81将代表西里尔字符Б (在DOS代码页866)和希腊字符Ϊ (在DOS代码页869)。
现在出现了严重的问题:1)不能在同一个文本文件中混合不同的字母。 2)为了正确理解一个文本,你必须知道它expression了哪个代码页。 哪里? 没有一个标准的方法,你将不得不处理这个要求用户或合理的猜测(?!)。 即使是现在, ZIP文件的“格式”也只限于文件名的ASCII码 (你可以使用UTF-8 – 稍后再看 – 但这不是标准的 – 因为没有标准的ZIP格式)。 在这篇文章中的Java工作解决scheme。 3)即使代码页不是标准的,每个环境都有不同的集合(甚至DOS代码页和Windows代码页也不同),名称也不尽相同。 4)对于例如中文或日文的语言来说,255个字符仍然太less,于是引入了更复杂的编码(例如Shift JIS )。
当时情况很糟(〜1985),而且绝对需要一个标准。 ISO / IEC 8859到达,它至less解决了以前的问题列表中的第3点。 第1,2,4点还没有解决,需要解决scheme(特别是如果你的目标不仅是原始文本,而且还有特殊的印刷字符)。 这个标准(经过很多版本的修改)现在还在我们这里(和Windows-1252的代码页一致),但是除非你使用一些遗留系统,否则你可能永远不会使用它。
为了把我们从这个混乱中拯救出来的标准已经被世界广为人知: Unicode 。 维基百科 :
Unicode是大多数世界写作系统中expression的文本的一致编码,表示和处理的计算行业标准。 Unicode的最新版本包含超过110,000个字符的曲目,涵盖100个脚本和多个符号集。
语言,图书馆,操作系统已经更新,以支持Unicode。 现在我们有我们需要的所有angular色,每个angular色都有一个共享的知名代码,过去只是一场噩梦。 用wchar_t
replacechar
(并接受与wcout
, wstring
和朋友一起生活),只需使用System.Char
或java.lang.Character
并生活愉快。 对?
没有。 从来没有这么容易 。 Unicode的使命是关于“……编码,表示和处理文本…” ,它不会将不同的文化转化为一个抽象的代码(除非你杀死所有的人我们的语言)。 此外,编码本身介绍了一些(不太明显的?!)我们必须关心的事情。