0%

字符编码那点事

几个概念

存储

进制(二进制、八进制、十进制、十六进制)、位(bit)、字节(byte)、字符

进制回顾

计算机是用二进制存储的,即 1 和 0,表示一位。
进制的转换:八进制(取 3 位二进制计算数值),十六进制(取 4 位二进制计算数值),十进制。
负数怎么表示?原码、反码(正数反码 = 原码,负数反码 = 正数逐位取反 & 符号位为1)、补码(反码 + 1)

CPU电路中只有加法器,减法是通过补码实现的。

补码为什么是反码 + 1?
反码 + 1 只是补码的一种计算方式,真正的理论基础是 模 和 同余数。
拿时钟来说,一圈 12 小时,超过 12 就重新计算,所以 12 称为
时钟运算中,减去一个数,其实就相当于加上另外一个数(这个数与减数相加正好等于 12,也称为同余数)。
eg. 当前为 10 点,回到 6 点,我们需要减去 4,但实际加上 8 也能让时针指向 6 点。

字节

计量单位,1byte = 8bit(即,1b = 8B),1KB = 1024bytes,MB / GB / TB / PB /…。

字符

即各种符号,包括字母、数字、运算符号、标点符号和其他符号,以及一些功能性符号。

编码

  • 字符集 - 可以理解为字典,即,为每一个「字符」分配一个唯一的码位(也称码点 / Code Point),eg. Unicode。
  • 编码规则 - 将「码位」转换为字节序列的规则(编码 / 解码 可以理解为 加密 / 解密 的过程),eg. UTF-8。

常见的字符编码

ASCII

全称 American Standard Code for Information Interchange,美国信息交换标准代码…>>
用一个字节(8位)存储,最高位为 0(保留),剩余7位二进制可表示 128 个字符,涵盖了所有的大小写字母、数字(0…9)、标点符号,以及在美式英语中使用的特殊控制字符。
扩展ASCII码:启用原先保留的最高二进制位,用于确定附加的 128 个特殊符号字符、外来语字母和图形符号。

Unicode

又称 万国码,是 多语言软件制造商组成的统一码联盟(一个国际组织)定义的编码…>>
广义的 Unicode 是一个标准,定义了一个字符集以及一系列的编码规则,即,Unicode 字符集和 UTF-8 / UTF-16 / UTF-32 等等编码规则。

UTF 表示 Unicode Transformation Format,即,Unicode 转码格式。

存储长度

不同的编码规则分 定长存储 和 变长存储 两种:

  • 定长 - 所有字符采用固定长度进行存储,转码时只需按固定长度读取,但比较浪费存储空间,以空间换时间。
  • 变长 - 区分高低字节字符进行存储,根据不同的编码规则有一定的计算消耗,以时间换空间。

UTF-8 - 一个英文字符占1个字节,一个中文字符(含繁体)占 3 个字节。
UTF-16 - 一个英文字符或一个中文字符(含繁体)都需要 2 个字节,Unicode 扩展区的一些汉字存储需要 4 个字节。
UTF-32 - 任何字符的存储都需要 4 个字节。

编码规则

UTF-8 是当下比较流行的 Unicode 编码规则。

  • 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码;因此对于英文字母,UTF-8 编码和 ASCII 码是相同的。
  • 对于 n 字节的符号(n > 1),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10;剩下的二进制位,全部为这个符号的 Unicode 码。

转换表如下(字母 x 表示可用于编码的二进制位):

Unicode 符号范围(十六进制) UTF-8 编码方式(二进制)
0000 0000 ~ 0000 007F 0xxxxxxx
0000 0080 ~ 0000 07FF 110xxxxx 10xxxxxx
0000 0800 ~ 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 ~ 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举个栗子:
“严”的 Unicode 码位是 4E25(二进制为 100 1110 0010 0101),处在转换表中第三行的 Unicode 符号范围内(0000 0800 - 0000 FFFF),因此“严”的 UTF-8 编码需要三个字节(格式是 1110xxxx 10xxxxxx 10xxxxxx);
然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的 x,多出的位补 0。
这样就得到了,“严”的 UTF-8 编码是:11100100 10111000 10100101
转换成十六进制就是 E4 B8 A5

UTF-8 编码本身不会嵌入 NUL 字节(0值),这便于某些程序语言用 NUL 标记字符串结尾,eg. C的字符串操作用 \0 做结尾判断
但这其实是非二进制安全的,所以 Redis 用了 SNS(简单动态字符串)、Go 在1.5封装了 String 结构体。

大端 & 小端

为什么有 大端(BE,Big Endian) 和 小端(LE,Little Endian) 之分呢?
《格列佛游记》中有一个关于怎么剥鸡蛋的故事:先打破鸡蛋较大的一端,还是先打破鸡蛋较小的一端。
现实中,这样的情况也很多,比如,古人的阅读习惯是从右向左,而现在普遍是从左向右。
所以,计算机的世界中,多字节数据在存储时也有这样的分歧(字节序):是将高序字节存储在起始地址,还是将低序字节存储在起始地址。
eg. 双字节数据 0x12345678:

字节序 存储结果
BE 0x78 0x56 0x34 0x12
LE 0x12 0x34 0x56 0x78

多字节字符编码(UTF-16 / UCS-2 / UTF-32 等)同样面临字节序的问题,以“严”为例,Unicode 码位是 4E25,UCS-2 Big Endian 编码存储是 4E 25,UCS-2 Little Endian 编码存储是 25 4E

Windows 中的 Unicode 编码,其实就是 UTF-16 Little Endian(Windows2000 以前称 UCS-2 LE,后面 UCS 和 Unicode 统一标准了)。

那么,计算机怎么识别编码顺序?Unicode 规范定义,每个文件的最前面加入一个表示编码顺序的字符,比如 UTF-16 中用 FE FF 表示
如果开头是 FE FF,则表示大端方式,若为 FF FE,则为小端方式。

BOM

BOM,全称 Byte Order Mark,用作定义字节顺序和编码形式的签名,存在于多字节字符编码的文本文件开头。

BOM 字节 编码方式
00 00 FE FF UTF-32 BE
FF FE 00 00 UTF-32 LE
FE FF UTF-16 BE
FF FE UTF-16 LE
EF BE BF UTF-8

BOM 对 UTF-8 是没有意义的,因为 UTF-8 是以字节为单位,按码位顺序存储的。
而 UTF-16 / UTF-32 以字为单位,一个个字符(2 个字节或 4 个字节)读取,所以会涉及先读取第一个或第二个字节的情况(大小端之分)。

既然 BOM 对 UTF-8 无意义,为什么要加上呢?
在 UTF-8 文件中放置 BOM 主要是微软的习惯,因为这样可以把 UTF-8 和 ASCII 等编码区分开,但这样的文件在 Windows 之外会带来问题。
比如,网页开头出现莫名其妙的空白符。

Windows 中的 UTF-8 和 Unicode 都是带 BOM 头的。

UCS

UCS,全称 Universal Character Set,通用字符集,是国际标准化组织 ISO 开展的 ISO/IEC 10646 项目定义的编码。
UCS 和 Unicode 有着相同的字库和字码,相同的字符在两个标准中有着相同的位置。
1991年前后,UCS 和 Unicode 开始合并成果,并为创立一个单一编码表而协同工作(从 Unicode2.0 开始,Unicode 采用了与 ISO 10646-1 相同的字库和字码)。
相比 UCS,Unicode 发布更频繁、也支持更多特性(排序、比较等算法)。
编码规则有 UCS-2(2字节) 和 UCS-4(4字节)。

ANSI

不同的国家和地区制定了不同的标准,由此产生了 GB2312 / GBK / GB18030 / Big5 / Shift_JIS 等各自的编码标准。
这些使用多个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码
在简体中文 Windows 操作系统中,ANSI 编码代表 GBK 编码;在繁体中文 Windows 操作系统中,ANSI 编码代表 Big5;若为英文文件,ANSI 编码代表 ASCII。

编码规则

  • GB2312 - 国家标准,采用 EUC 存储,兼容 ASCII,每个汉字及符号占 2 个字节。
  • GBK - 微软标准,单双字节变长编码,兼容 GB2312。
  • GB18030 - 国家标准,兼容 GBK,符合 Unicode 规范,和 UTF-8 一样,是一种 Unicode 实现,变长编码:单字节 ASCII、双字节 GBK 以及用于填补所有 Unicode 码位的 4 字节。

字符集

  1. 内码 & 国标码
    汉字机内码,又称 汉字 ASCII 码,简称 内码,指计算机内部存储,处理加工和传输汉字时所用的由 0 和 1 符号组成的代码。
    国标码,全称 国家标准代码,强制标准冠以“GB”,推荐标准冠以“GB/T”,较早的标准是 GB2312,现时官方强制使用 GB18030 标准。
    汉字处理系统要保证中西文的兼容,当系统中同时存在 ASCII 码和汉字国标码时,将会产生二义性。
    eg. 有两个字节的内容为 30H 和 21H,它们既可以表示汉字 的国标码,又可以表示西文 0! 的 ASCII 码。
    为此,汉字内码应对国标码加以适当处理和变换。
    GBK 编码的汉字的内码为 2 字节长,它是在相应国标码的每个字节最高位上加 1,即,汉字内码=汉字国标码+8080H(两个字节的最高位分别置为 1).。
    例如,上述 字的国标码是 3021,其汉字机内码则是 B0A1
    附:在线中文转ASCII,http://www.jsons.cn/ascii/

  2. 国际码 & 国标码
    中国汉字通行的国际标准为我国于1981年制订的“信息交换用汉字编码字符集”,其标准号为 GB2312-80,简称国际码,是我国应用最广泛的汉字编码字符集。
    所以,通俗理解,国际码即最早的强制标准,在 GB2312-80 之后都称为 国标码。
    注:网上资料有限,如上结论可能有偏差,但也不用太纠结这两个概念的关系,大体知道就好,以国标码为主。

  3. 国标码 & 区位码
    区位码实际等同于国标码,国标码是一个 4 位十六进制数,区位码是一个 4 位十进制数,所以,国标码 = 区位码 + 2020H
    每个国标码或区位码都对应着一个唯一的汉字或符号,因为十六进制数很少用到,所以常用的是区位码,它的前两位叫做区码,后两位叫做位码。

几个问题

网上,形如 &#20005 是什么编码?

&#20005 是一种 Unicode 表示方式,为十进制码点值表示,常用于 HTML 中。
eg. “严”的 Unicode 码为 4E25(十六进制表示),转换为十进制是:4 * 16^3 + E * 16^2 + 2 * 16^1 + 5 * 16^0 = 20005。
其他表示方式如:

  • \uhhhh,16位码点值表示,eg. \u4e25
  • \Uhhhhhhhh,32位码点值表示,eg. \U00004e25
  • \xhh,字符编码后存储的十六进制表示,eg. \xe4\xb8\xa5
  • %hh,同 \xhh,常用于URL中,eg. %E4%B8%A5

其中,h 代表一个十六进制数。

\x\u 在字符表示上有什么区别?

码点值小于 256 的文字符号可以写成单个十六进制数转义的形式,eg. A 可用 \x41 表示,但更高的码点值必须用 \u\U 转义。
eg. JavaScript 中:

1
2
3
console.log("\u4e25"); // "严"
console.log("\x41"); // "A"
console.log("\xe4\b8\a5"); // 乱码...

不过,某些语言有提供了更高的码点值的 \x 表示,如 Golang:

1
2
3
fmt.Println("\u4e25"); // "严"
fmt.Println("\x41"); // "A"
fmt.Println("\xe4\xb8\xa5"); // "严"

NUL 和 NULL 的区别

NUL 用于结束一个 ASCII 字符串;NULL 用于表示什么也不指向(空指针)。
ASCII 字符中的 \0 被称为 NUL;表示哪里也不指向的特殊的指针值则是 NULL。

参考资料

二进制补码计算原理详解,https://blog.csdn.net/zhuozuozhi/article/details/80896838
深入理解原码、反码、补码,https://blog.csdn.net/afsvsv/article/details/94553228
字符编码笔记:ASCII,Unicode 和 UTF-8
从字节理解 Unicode、UTF8、UTF16,https://www.cnblogs.com/crazylqy/p/10183715.html
UTF-8 与 UCS-2 之间有何区别与联系,https://www.zhihu.com/question/302200063
字符串横向对比:C、Golang、Redis,https://studygolang.com/articles/1230