当“Zoë”!==“Zoë”时,或者为什么需要标准化Unicode字符串

听说过Unicode规范化吗? 你并不孤单。 但是每个人都需要知道这一点。 标准化可以为您节省很多问题。 任何开发人员迟早都会发生与下图中所示类似的事情。
Zoë不是Zoë

顺便说一下,这并不是另一个奇怪的 JavaScript的例子。 该材料的作者(我们今天出版的翻译版)说,他可以证明使用几乎所有现有的编程语言时,同样的问题如何表现出来。 特别是,我们谈论的是Python,Go甚至shell脚本。 怎么处理呢?

背景知识


多年前,当我编写一个应用程序(在Objective-C中)从用户的通讯录及其社交网络中导入联系人列表时,我首先遇到了Unicode问题,之后我消除了重复项。 在某些情况下,事实证明有些人两次出现在名单上。 发生这种情况的原因是,根据程序,它们的名称不是相同的字符串。

尽管在上面的示例中这两行看起来完全相同,但是它们在系统中的显示方式不同,但它们在磁盘上存储的字节不同。 在名字"Zoë" ë”(带有变音符)字符表示单个Unicode代码点。 在第二种情况下,我们正在处理分解,即使用多个字符表示字符的方法。 如果在您的应用程序中使用Unicode字符串,则需要考虑以下事实:相同的字符可以用不同的方式表示。

表情符号的使用方法:简而言之,字符编码


计算机使用字节来工作,字节只是数字。 为了能够在计算机上处​​理文本,人们一致同意字符和数字的对应关系,并就字符的外观表示方式达成一致。

第一个这样的协议由ASCII(美国信息交换标准代码)编码表示。 该编码使用7位,可以表示128个字符,其中包括拉丁字母(大写和小写字母),数字和基本标点符号。 ASCII还包括许多“不可打印”字符,例如换行符,制表符,回车符等。 例如,在ASCII中,拉丁字母M(大写m)被编码为77(十六进制表示为4D)。

ASCII的问题在于,尽管128个字符足以代表使用英语文本的人们通常使用的所有字符,但是此数量的字符不足以表示其他语言的文本和各种特殊字符(如表情符号)。

解决此问题的方法是采用Unicode标准,该标准旨在表示所有现代和古代文本中使用的每个字符,包括表情符号之类的字符。 例如,在最新的Unicode 12.0标准中,有超过137,000个字符。

可以使用多种字符编码方法来实现Unicode标准。 最常见的是UTF-8和UTF-16。 应当指出,在网络空间中,最常见的是用于对文本进行UTF-8编码的标准。

UTF-8标准使用1到4个字节来表示字符。 UTF-8是ASCII的超集,因此其前128个字符与ASCII码表中表示的字符匹配。 另一方面,UTF-16标准使用2到4个字节来表示1个字符。

为什么都有这两个标准? 事实是西方语言的文本通常使用UTF-8标准进行最有效的编码(因为此类文本中的大多数字符都可以表示为1字节大小的代码)。 如果我们谈论东方语言,那么可以说使用UTF-16来存储以这些语言编写的文本的文件通常较少。

Unicode代码点和字符编码


Unicode标准中的每个字符都分配有一个称为代码点的标识号。 例如,一个代码点表情符号 U + 1F436

对该图标进行编码时,它可以表示为各种字节序列:

  • UTF-8:4个字节, 0xF0 0x9F 0x90 0xB6
  • UTF-16:4个字节, 0xD83D 0xDC36

在下面的JavaScript代码中,所有这三个命令将相同的字符输出到浏览器控制台。

//
console.log(' ') // =>
// Unicode (ES2015+)
console.log('\u{1F436}') // =>
// UTF-16
// ( 2 )
console.log('\uD83D\uDC36') // =>


大多数JavaScript解释器(包括Node.js和现代浏览器)的内部机制都使用UTF-16。 这意味着我们正在考虑的狗图标使用两个UTF-16代码单元(每个16位)存储。 因此,以下代码输出对您来说似乎不应该理解:

console.log(' '.length) // => 2

字符组合


现在回到开始的地方,即让我们讨论为什么对于一个人来说看起来相同的符号具有不同的内部表示形式。

某些Unicode字符旨在修改其他字符。 它们被称为组合字符。 它们适用于基本字符,例如:

  • n + ˜ = ñ
  • u + ¨ = ü
  • e + ´ = é

从前面的示例中可以看到,可组合字符使您可以在基本字符中添加变音符号。 但是Unicode的字符转换功能不限于此。 例如,某些字符序列可以表示为连字(因此ae可以变成æ)。

问题在于特殊字符可以以各种方式表示。

例如,字母é可以用两种方式表示:


使用上述任何一种表示字母é的方式产生的字符看起来都是相同的,但是相比之下,结果却发现它们是不同的。 包含它们的行将具有不同的长度。 您可以通过在浏览器控制台中运行以下代码来验证这一点。

 console.log('\u00e9') // => é console.log('\u0065\u0301') // => é console.log('\u00e9' == '\u0065\u0301') // => false console.log('\u00e9'.length) // => 1 console.log('\u0065\u0301'.length) // => 2 

这可能会导致意外错误。 例如,它们可以表示为以下事实:由于未知原因,该程序无法在数据库中找到某些条目,因为输入正确密码的用户无法登录系统。

线归一化


上面的问题有一个简单的解决方案,其中包括规范化字符串,将其带入“规范表示”。

归一化有四种标准形式(算法):

  • NFC:规范化表格规范组成。
  • NFD:规范化形式规范分解。
  • NFKC:标准化表格兼容性组成。
  • NFKD:标准化表格兼容性分解。

最常用的规范化形式是NFC。 使用此算法时,首先分解所有字符,然后按照标准定义的顺序重新组合所有组合序列。 对于实际使用,您可以选择任何形式。 最主要的是始终如一地应用它。 结果,在程序输入处接收相同的数据将始终导致相同的结果。

在JavaScript中,从ES2015(ES6)标准开始,存在一种用于标准化字符串的内置方法-String.prototype.normalize([form]) 。 您可以在Node.js环境和几乎所有现代浏览器中使用它。 此方法的form参数是规范化表单的字符串标识符。 默认值为NFC表单。

我们回到前面考虑的示例,这次应用规范化:

 const str = '\u0065\u0301' console.log(str == '\u00e9') // => false const normalized = str.normalize('NFC') console.log(normalized == '\u00e9') // => true console.log(normalized.length) // => 1 

总结


如果要开发Web应用程序并使用用户在其中输入的内容,请始终规范化接收到的文本数据。 在JavaScript中,您可以使用标准的字符串方法normalize()进行规范化。

亲爱的读者们! 您是否遇到过可以通过规范化解决的字符串问题?

Source: https://habr.com/ru/post/zh-CN445274/


All Articles