多年前仔细了解过一次Unicode编码,但是没有落下笔记,最终今天尝试改一下过去的代码的时候,竟然发现看不懂了(我的两个小项目:正则可视化 和 JS工具函数库)。所以再次记录一下,本文尝试搞懂对Unicode的所有主要疑惑,目标是看完该文章,足以应对所有非专业字符编码相关的需求开发,不过多深究细节。
简介
本文会依次介绍ASCII、Unicode、全角半角、HTML/XML转义、UTF32、UTF16、UTF8,BOM。且每个知识点都会加上TL;DR;用于一句话明白这个是干嘛使的,赶时间就只看TL;DR;也可以。文章同步发在我的个人站和知乎中。
如有转载,请注明出处,感谢
原文: https://zhuanlan.zhihu.com/p/370601172
ASCII
TL;DR;
最基础的编码格式,打字机时代的产物,共128个字符,其包含键盘上每一个可显字符,可应对只有英文字母场景下的编码需求。
More
ASCII编码,共128个字符,编码范围为0~127,每个字符占用一个字节(8位),但ASCII实际7位就够用了,所以第8位可用于保存额外的信息,早期多用来做奇偶校验,目前都是填充0。
其中第32到126的字符(\x20 - \x7E),共95个,为可显字符(printable character),为空格、数字、字母、标点符号、和几个特殊符号(例如脱字符^)。其中:
- 32(\x20),为空格
- 48-57(\x30-\x39),数字0-9
- 65-90(\x41-\x5A),字母A-Z
- 97-122(\x61-\x7A),字母a-z
前32个字符和最后1个字符(0到32和127),共33个字符,称之为控制字符(Control character)。
每个控制字符除了通过ASCII编码表示,还有另外的「脱字符表示法」(Caret notation ),第0个为^0,后续26个为^A~^Z,剩余的为^[,^\,^],^^,^_,最后一个为^?。在JS中的正则表达式中,用\c来替代脱字符匹配控制字符,例如\cJ代表^J。 另外,有几个特殊的在现在编码中仍然有用的控制字符,还有「转义符表示法」(escape sequence)。如下表所示:
| 十进制 | 脱字符表示 | 转义符表示 | 名称 | 名称简写 | 作用 |
|---|---|---|---|---|---|
| 0 | ^@ | \0 | Null | NUL | 空 |
| 7 | ^G | \a | Bell | Bel | 振铃 |
| 8 | ^H | \b | Backspace | BS | 退格 |
| 9 | ^I | \t | Horizontal Tab | HT | 水平制表 |
| 10 | ^J | \n | Line Feed | LF | 换行 |
| 11 | ^K | \v | Vertical Tab | VT | 垂直制表 |
| 12 | ^L | \f | Form feed | FF | 换页 |
| 13 | ^M | \r | Carriage Return | CR | 回车 |
| 27 | ^[ | \e | Escape | ESC | 换码 |
显然128个字符对于英文表达足够了,但是明显不适用中文,以及其他非英文表达的语言,所以中国后来有了GBK编码。ASCII编码在早期遇到编码不够用的时候,也有一些扩展和变体,例如「扩展ASCII」可表示256个字符。不过由于后来有了Unicode编码格式,这些都没用了,所以就没必要去了解了。
Unicode
TL;DR;
Unicode编码标准,可表示目前全世界所有语言的所有字符。同时兼容ASCII编码。
More
Unicode的前128个字符编码和ASCII是一致的,即向后兼容ASCII,对于使用ASCII编码的程序可以直接使用Unicode规范。在Unicode中,对于每一个字符编码的值,叫做code point。例如小写字母a的code point为97,对应十六进制为\x61。下文为了方便对code point称作「码位」。
在Unicode中,码位的总范围为\x0到\x10FFFF,共1,114,112个码位。2048个用于编码代理(UTF-16),66个非字符码位(例如BOM),137,468个预留给私人使用,最终剩余974,530用于普通字符分配。
码位的最大值为\x10FFFF,对应二进制有21位,我们将2^16个值分为一组,则Unicode总共可以分为17份,每一份称之为平面(Plane),每一个平面有65,536(2^16)个码位。
为什么Unicode的最大值为\x10FFFF?因为对于UTF16编码,双字节最多可编码2^20个字符,单字节可编码2^16个字符,加起来共17个平面的字符数。
下表为每个平面详情:
| 平面编号 | 码位范围(十六进制) | 名称简写 | 名称 |
|---|---|---|---|
| Plane 0 | 0000–FFFF | BMP | 基础多语言平面(Basic Multilingual Plane) |
| Plane 1 | 10000–1FFFF | SMP | 补充多语言平面(Supplementary Multilingual Plane) |
| Plane 2 | 20000–2FFFF | SIP | 补充表意语言平面(Supplementary Ideographic Plane) |
| Plane 3 | 30000–3FFFF | TIP | 第三表意语言平面(Tertiary Ideographic Plane) |
| Planes 4–13 | 40000–DFFFF | - (未分配) | - (未分配) |
| Plane 14 | E0000–EFFFF | SSP | 补充特殊用途平面(Supplementary Special-purpose Plane) |
| Planes 15–16 | F0000–10FFFF | SPUA-A/B | 补充私有使用区平面(Supplementary Private Use Area planes) |
BMP为基础平面,目前收录了全球范围内大部分的字符。剩余的16个平面均为补充平面,用于进行新的字符的补充。其中私有平面,用于给个人做编码扩展,Unicode不指定字符编码。比如我编写了一个英雄联盟相关的程序,然后定义某一个字符代表一种游戏里的操作,就可以使用私有平面。
Unicode中还有一个概念:对于逻辑上属于一类的字符,称之为块(block)。例如:
C0 Controls and Basic Latin块,\x0000-\x007F,就是从ASCII继承来的前128个字符。CJK Unified Ideographs块,\x4E00-\x9FFC,包含大部分的中日韩文字,Halfwidth and Fullwidth Forms,\xFF00-\xFFEF,用于英文字母/数字/日文/个别符号等一些字符的全角-半角相互转换。Miscellaneous Symbols and Pictographs,\x1F300-\x1F5FF,Supplemental Symbols and Pictographs,\1F900-\1F9FF,包含大部分emoji表情
另外还有一个比较重要的块General Punctuation,码位在[2000,206F],包含一些符号以及一些特殊的分隔符、连接符、空格符等,这些符号不一定是可显字符,而是告诉解释器该如何操作当前字符。对于所有块,可通过该链接查阅。
半角/全角
TL;DR;
对于全角字符,在展示上占用的宽度是半角字符的两倍。每个字符都在Unicode标准里定义了是全角还是半角,对于不需要精确计算的简单业务场景,也可以简单的认为码位大于128的都是全角字符。
More
半角和全角,对应英文为halfwidth,fullwidth。半角全角对应的是UI显示的概念,对于定宽的字体,全角字符占用的宽度是半角字符的两倍。Unicode中每个字符都有一个East_Asian_Width属性,用于指示当前是全角字符还是半角字符,具有以下值:
- A, Ambiguous,根据上下文决定
- F, Fullwidth,全角
- H, Halfwidth,半角
- N, Neutral,中立,作为半角
- Na, Narrow,半角
- W, Wide,全角
在EastAsianWidth.txt文件中列举了已显示声明East_Asian_Width属性的字符。对于不在该文件内的字符,符合下列规则的为W(全角):
- the CJK Unified Ideographs Extension A block, 对应区间:
\x3400..\x4DBF - the CJK Unified Ideographs block, 对应区间:
\x4E00..\x9FFF - the CJK Compatibility Ideographs block, 对应区间:
\xF900..\xFAFF - the Supplementary Ideographic Plane, 对应区间:
\x20000..\x2FFFF - the Tertiary Ideographic Plane, 对应区间:
\x30000..\x3FFFF
其余未列出的,默认为N(半角)。
在一些编码集中,有的字符既有全角形式也有半角形式,Unicode为了实现与这些编码集之间的无损转换,在第一平面的最后,\xFF00到\xFFEF区段,定义了用于半角全角转换的字符,如下所示:
\xFF01–\xFF5E为ASCII的\x21到\x7E的全角形式。其中空格没有纳入进来,因为全角空格已通过\x3000定义。\xFF65–\xFF9F为半角的日语字符。\xFFA0–\xFFDC为半角的汉语字符。\xFFE0–\xFFEE包含了一些符号,有半角有全角。
对于在JS中判断字符是全角还是半角,目前下载量比较多的一个npm包:is-fullwidth-code-point。string-width依赖is-fullwidth-code-point计算字符长度。不过实际测试,is-fullwidth-code-point没有完全覆盖所有全角字符(issue),不过对于日常中文场景的开发够用了。
在日常开发中,对于UI展示的场景中,会比较关心字符宽度的问题。但是在涉及存储的时候,更关心的其实是存储该字符占用了几个字节。所以在涉及存储的场景下,关注点就不应该是全角/半角的概念,而是字符编码所占用的字节数。对于UTF8编码,码位小于等于128的使用1字节存储,大于128的会根据需要,使用双字节,三字节或四字节存储。所以多数场景下,为了简便,前后端都可以通过码位是否大于128来判断全角/半角。
HTML/XML实体转义
TL;DR;
我们常说的HTML转义,实际正式应该称之为HTML实体引用。对应有两种引用方式:数字字符引用(numeric character reference) 和 字符实体引用(character entity reference)。
先说常见的字符实体引用,语法为:&name;,name必须小写。例如:&lt;表示小于号<。
可以进行引用的实体,称之为命名实体。命名实体有两种,一种是语法中内置的,另一种是在DTD中显示声明的:<!ENTITY name "value">。
数字字符引用方式:
- 十进制:
&#nnnn; - 十六进制:
&#xhhhh;, x必须小写。hhhh大小写可以混用。
还是同样的例子,小于号<如果使用数字字符引用的方式,为:&#60;。
HTML
XML
XML规范中,有5个预定义的实体,如下所示,如果需要使用更多的实体转义,需要在DTD中声明。
| 名称 | 字符 | 码位十六进制 | 码位十进制 | 标准 | 名称全称 |
|---|---|---|---|---|---|
| quot | " | \x0022 | 34 | XML 1.0 | quotation mark |
| amp | & | \x0026 | 38 | XML 1.0 | ampersand |
| apos | ' | \x0027 | 39 | XML 1.0 | apostrophe (1.0: apostrophe-quote) |
| lt | < | \x003C | 60 | XML 1.0 | less-than sign |
| gt | > | \x003E | 62 | XML 1.0 | greater-than sign |
Unicode Encoding Forms
TL;DR;
Unicode字符编码格式(Unicode Encoding Forms),简写为:UTF,即:将一个Unicode字符保存为字节序列的格式规范,用于文件存储、数据传输等。Unicode标准支持3种编码格式,如下:
- UTF-32: 使用4字节表示一个Unicode字符。
- UTF-16: 变长的编码格式,码位大于
\xFFFF的字符,使用4字节存储,小于等于\xFFFF的字符,使用2字节存储。 - UTF-8: 变长的编码格式,码位大于
\xFFFF的字符,使用4字节存储,小于等于\xFFFF大于\x07FF的使用3字节,小于等于\x07FF大于\x007F的使用2字节,小于等于\x007F使用1字节。
More
Unicode标准支持3种编码格式,UTF32/UTF16/UTF8,用于映射码位为 \x0000到\xD7FF 和 \xE000到\x10FFFF 的字符,即除去高位代理和低位代理的所有字符。至于什么是高位代理和低位代理后面会讲到。
UTF32
是一种定长编码格式,使用32位(4字节)表示Unicode中的一个码位。由于Unicode的码位实际只用了21位,所以多余部分前导0。例如字符小写字母a,对应码位为\x61,存储的字节序列为:\x00000061。
UTF16
变长编码格式,按平面区分,位于第一平面中的字符(\x0000..\xD7FF和\xE000..\xFFFF),使用16位(2字节)存储,使用和码位相同的值。位于其他平面的字符(\x10000..\x10FFFF),通过高位和低位代理使用32位(4字节)表示。
对于位于第一平面的值,即小于等于\xFFFF的值,使用2个字节就足够表示,所以直接使用两个字节表示其码位的值,如下所示:
| code point | UTF16编码后实际存储的值 |
|---|---|
| xxxx xxxx xxxx xxxx | xxxx xxxx xxxx xxxx |
位于其他平面平面的值,即大于\xFFFF的值,使用4个字节表示,如下所示:
| code point | UTF16编码后实际存储的值(wwww = uuuuu - 1) |
|---|---|
| 000u uuuu hhhh hhxx xxxx xxxx | 1101 10ww wwhh hhhh 1101 11xx xxxx xxxx |
位于其他平面的值,即\x10000到\x10FFFF的值,二进制最高使用21位。将其拆分为两部分,即前11位和后10位,前11为用hhhhhh hhhh表示,后10位用xxxxx xxxxx表示。其中,前11位中,前5位是用来表示位于第几个平面,所以这里也特殊标注出来,用u表示,即前11位为:uuuuuh hhhhh。
由于这里前五位的有效值为\x1到\x10,所以可以减1,让有效值从0开始,则有效值变成了\x00到\x0F,即4位,减1后的值用w表示,从而前11位可以表示为: wwwwh hhhhh。
将前10位前导110110,后10位前导110111,即UTF16对于大于\xFFFF字符的表示如上述表格所示。
这里, 二进制1101 1000 0000 0000为\xD800,二进制1101 1100 0000 0000为\xDC00,从而,该规则简单描述如下:
- 假设某个字符x位于
\x10000到\x10FFFF之间,将其减去\x10000,得到x',x'的范围为:\x00000–\xFFFFF。 - 将x'分成两部分,前10位和后10位,用w1和w2表示,其范围为
\x0000–\x03FF。 - 将w1加上
\xD800,得到w1',范围为:\xD800–\xDBFF. - 将w2加上
\xDC00,得到w2',范围为:\xDC00–\xDFFF.
将w1'和w'2转换为二进制,即UTF16下x存储的字节序列。
x' = yyyyyyyyyyxxxxxxxxxx // x - 0x10000
x1' = 110110yyyyyyyyyy // 0xD800 + yyyyyyyyyy
x2' = 110111xxxxxxxxxx // 0xDC00 + xxxxxxxxxxUTF8
变长编码格式,是直接兼容ASCII的编码格式,对于能在1字节内保存的,直接保存为1字节。否则进行类似UTF16高低位代理的方式,最高位使用4字节。
UTF8中没有减1的逻辑,只是简单的增加前缀,具体规则如下:
| 范围 | 码位(二进制) | 第1个字节 | 第2个字节 | 第3个字节 | 第4个字节 |
|---|---|---|---|---|---|
| \x0000 .. \x007F(7位) | 00000000 0xxxxxxx | 0xxxxxxx | - | - | - |
| \x0080 .. \x07FF(11位) | 00000yyy yyxxxxxx | 110yyyyy | 10xxxxxx | - | - |
| \x0800 .. \xFFFF | zzzzyyyy yyxxxxxx | 1110zzzz | 10yyyyyy | 10xxxxxx | - |
| \x10000 .. \x10FFFF | 000uuuuu zzzzyyyy yyxxxxxx | 11110uuu | 10uuzzzz | 10yyyyyy | 10xxxxxx |
在UTF8中,
- 如果字节序列以
0开头,代表当前字节本身表示了一个字符。 - 如果为
10开头,则代表当前字节为多字节字符中的一个字节。 - 如果当前字符以
11开头,则前面1的个数,代表当前字符所使用的字节数,2个1代表使用两个字节表示一个字符,3个1代表使用3个字节表示一个字符。
Byte order mark
TL;DR;
字节顺序标记(Byte order mark),指预定义的,放置在文本流开头的,一段特殊的字节序列,用于标记当前文本使用的哪种编码格式(UTF32/UTF16/UTF8)。具体规则如下:
| 编码格式 | 文本流开头的字节序列 |
|---|---|
| UTF-8 | EF BB BF |
| UTF-16 (BE) | FE FF |
| UTF-16 (LE) | FF FE |
| UTF-32 (BE) | 00 00 FE FF |
| UTF-32 (LE) | FF FE 00 00 |
例如Windows的记事本应用,将文本保存为UTF8格式时,会在文本内容的开头添加\xEF,\xBB,\BF3个字节。记事本应用在读取一个文本文件的时候,发现前三个字节为\xEF,\xBB,\BF,则认为接下来的字节流通过UTF8形式解析。
endianness
字节顺序(endianness),这里特指当保存一个数字类型数据时,存储的字节序列的顺序。分为大端序(big-endian,简写BE)和小端序(little-endian,简写LE)。
假设当前要将一个16位的整型数字\x0A0B指向内存地址\x100。
对于大端序的CPU,随着内存地址的增加,认为其存储的值的重要性是递减的,所以大端序的CPU会在\x100的位置上存储\x0A,在\x101的位置上存储\x0B。
对于小端序的CPU,随着内存地址的增加,认为其存储的值的重要性是递增的,所以小端序的CPU会在\x100的位置上存储\x0B,在\x101的位置上存储\x0A。
所以反过来,假设现在在内存中,地址\x100的地方存储了\xAA,在\x101的地方存储了\xBB,假设有一个int16变量指向\x100,对于大端序CPU会认为该变量的值为\xAABB,对于小端序CPU会认为该变量的值为\xBBAA。
Byte order mark
因为各个系统之间的字节顺序不同,所以在传输和交换Unicode文本时,要告诉对方当前是以什么顺序保存的,从而接收方才能有效的进行解析。
字节序列标记(Byte order mark,简写BOM),特指\xFEFF字符。在文本的开头,添加\xFEFF字符,用于标识当前文本的字节顺序。
- 对于
UTF8编码格式,该字符会被保存为\xEFBBBF - 对于
UTF16 BE编码格式,该字符会被保存为\xFEFF - 对于
UTF16 LE编码格式,该字符会被保存为\xFFFE - 对于
UTF32 BE编码格式,该字符会被保存为\x0000FEFF - 对于
UTF32 LE编码格式,该字符会被保存为\xFFFE0000
所以,解析程序通过判断BOM即可确定接下来的文本所使用的编码格式以及字节顺序。在Unicode中,\xFEFF是专门用作BOM的,如果该字符出现在文本中间,会被当做「零宽非换行空格」(zero-width non-breaking space),其实就是跳过的意思。同样的,对于它的一个镜像字符\xFFFE,如果出现也会被跳过。
BOM可以省略,不是必须的,因为:
- 在某些场景下已经预设了编码格式或字节顺序,例如W3C的HTML5规范中,如果指定charset为utf-8,则会默认按照utf-8解析,而如果文件流指定了BOM,则会优先使用BOM指定的编码格式和字节顺序。
- 当BOM被省略时,大部分解析器都会对文本流进行推算,推算出编码格式和字节顺序,但是这个推算并不是绝对可靠的。
当使用UTF8格式保存文本时,Unicode标准建议,如果原文本没有BOM,则不要添加BOM。因为:
UTF8是单字节存储的,不存在字节顺序问题。- 解析器会默认使用
UTF8解析文本。 - 因为ASCII和
UTF8是一一对应的,如果不添加BOM,则ASCII和Unicode可以相互兼容,如果加上了BOM,就打破了相互兼容。
不过当前很多系统或平台并没有按照规范来,在解析文本的时候会要求UTF8要有BOM,以及在保存文本的时候会加上BOM,例如windows系统的记事本。
而对于UTF16和UTF32,要添加BOM,不然在解析的出的文本可能就是乱码,因为解析器在对字节顺序的推算上,并不能保证完全可靠。
如有转载,请注明出处,感谢
原文: https://zhuanlan.zhihu.com/p/370601172