多年前仔细了解过一次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必须小写。例如:<
表示小于号<
。
可以进行引用的实体,称之为命名实体。命名实体有两种,一种是语法中内置的,另一种是在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 + xxxxxxxxxx
UTF8
变长编码格式,是直接兼容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
,\BF
3个字节。记事本应用在读取一个文本文件的时候,发现前三个字节为\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