Unicode 字符集、编码方式、端序、内码与外码
2022-10-17 17:15:52
`Unicode` 是字符集,而 `UTF-8`、`UTF-16` 和 `UTF-32` 是编码方案。**字符集**是各种文字和符号的集合,包括各个国家文字、标点符号、图形符号、数字等。
`Unicode` 字符集将所有字符都分配一个数字。比如字符 `'a'` 在 `ASCII` 字符集中被分配到了数字 `97`,所以 `97` 就是 `'a'` 的字符代码,参见下面一段 `JavaScript` 代码。
```javascript
console.log('a'.codePointAt(0))
// 97
```
代码点需要转换成二进制序列才能传输和存储,`UTF-8`、`UTF-16` 和 `UTF-32` 这些编码方案可以做到这一点。**编码方案**是将字符集中的字符代码转换为二进制序列的一种映射规则。
我们要区分字符代码和字符编码这两个概念:
- **字符代码**:某个特定字符在字符集中的序号,又叫做代码点、码点,英文是 `code point`。
- **字符编码**:传输、存储过程中用于表示字符的二进制序列,字符编码的基本单位是代码单元 `code unit`,所以字符编码又被称为**码元序列**。
诸如 `UTF-8`、`UTF-16` 和 `UTF-32` 这些**编码方案**,或者叫**编码方式**,会按照一定规则将代码点编码成字符编码。
**`UTF-8` 编码**是一种变长编码方式,长度为 `1` 到 `4` 个字节,大部分中文字符占 `3` 个字节。现在普遍采用 `UTF-8` 编码,是因为大部分的字符都被编码成 `1` 个字节,在传输时能节省网络带宽,在存储时能节省存储空间。
下面是代码点和字符编码的对照表:
|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|
当只用一个字节表示字符时,`UTF-8` 编码兼容 `ASCII` 的 `128` 个编码,也就是说一个用 `ASCII` 编码存储的文件,如果以 `UTF-8` 格式打开,不会出现乱码。当使用 `n` 个($n \ge 2$)字节表示字符时,第一个字节的前几位由 `n` 个 `1` 和一个 `0` 组成,后续字节前两位都是 `10`。
在这里需要引入一个概念,**端序**,英文是 `edianness`,又叫做**字节序**。端序就是字节在传输或者存储时的顺序。
一共有两种端序:
- 大端序 `BE`,英文是 `big-endian`,用低地址存高字节 `most significant byte`,可以理解为*高字节在前*。
- 小端序 `LE`,英文是 `little-endian`,用低地址存低字节 `least significant byte`,可以理解为*低字节在前*。
我们采用 `BOM` 来区分这两种字节序,`BOM` 是 `Byte Order Mark` 的缩写,也就是**字节顺序标记**,它是插入在文本文件开头的特殊字符,用来标记端序或表示编码方式。如果 `BOM` 符出现在数据流的中间,`Unicode` 会将其解释成**零宽不间断空格** `zero-width non-breaking space`,这种用法在 `Unicode 3.2` 中被废弃。
由于 `UTF-8` 编码采用单字节码元,因此不需要考虑字节序的问题,但是 `Windows` 的记事本会自动地在 `UTF-8` 文件的开头加上 `EFBBBF`,用来表示这是一个 `UTF-8` 编码的文件,这样的行为经常会导致乱码,所以推荐 `Windows` 用户使用其他文本编辑器而不是自带的记事本。
**`UTF-16` 编码**使用 `2` 个字节或者 `4` 个字节表示字符,它的代码单元长度为 `2` 个字节,因此需要考虑字节序问题,按照大小端序,编码方式可以具体分为 `UTF-16BE` 和 `UTF-16LE`,对应的 `BOM` 分别是 `FEFF` 和 `FFFE`。
一门编程语言在处理编码时可以分为两种编码:
- 内码,全称内部编码,英文是 `internal encoding`,它是程序内部使用的字符编码,特别是某种语言实现其 `char` 或 `String` 类型时在内存中使用的内部编码。
- 外码,全程外部编码,英文是 `external encoding`,它是程序与外部交互时外部使用的字符编码。
内码倾向于使用定长码,和内存对齐一个原理,便于处理。外码倾向于使用变长码,变长码将常用字符编为短编码,罕见字符编为长编码,节省存储空间与传输带宽。
`Java` 使用 `UTF-16` 作为内码,它的 `char` 类型占用 `2` 个字节,刚好对应 `UTF-16` 的一个代码单元。对于码点位于 `0000 - FFFF` 之间的基本字符,使用一个 `char` 类型即可存储,且不用进行编码转换。对于码点位于 `10000 - 10FFFF` 之间的辅助字符,需要使用两个 `char` 类型存储,需要进行编码转换。常用的汉字可以在 `Java` 中使用一个 `char` 存储,不会产生乱码。
**`UTF-32` 编码**是一种定长编码,用 `4`个字节表示字符,它的代码单元长度为 `4` 个字节。`Unicode` 的范围从 `00FFF - 10FFF`,`4` 个字节表示范围为 `00000000 - FFFFFFFF`,所以 `UTF-32` 编码可以完全覆盖 `Unicode` 字符集,`Unicode` 代码点可以直接转换为码元序列。
与 `UTF-16` 类似,从逻辑编码转换为物理编码时,也需要考虑字节序问题,所以编码方式进一步划分为 `UTF-32BE` 和 `UTF-32LE`,相应的 `BOM` 符分别是 `0000FEFF` 与 `FFFE0000`。总体而言,`UTF-32` 编码方式使用的不是很多。
编码格式与 `BOM` 的对照表如下:
|编码格式|`BOM`|
|---|---|
|`UTF-8`|`EFBBBF`|
|`UTF-16BE`|`FEFF`|
|`UTF-16LE`|`FFFE`|
|`UTF-32BE`|`0000 FEFF`|
|`UTF-16LE`|`FFFE 0000`|
参考资料:
- [理论篇:常见字符集与编码方式](https://blog.csdn.net/weixin_43197380/article/details/123482902)
- [unicode编码详解,一看就懂](https://www.cnblogs.com/hahlzj/p/11908713.html)
- [AscII码 和 unicode码是什么关系?](https://www.zhihu.com/question/57461614)
- [刨根究底字符编码之十五——UTF-32编码方式](https://blog.csdn.net/sinolover/article/details/109497582)
- [Endianness](https://en.wikipedia.org/wiki/Endianness)
- [BOM](https://en.wikipedia.org/wiki/Byte_order_mark)
- [Java内码编码之UTF-16讲解](https://blog.csdn.net/m0_57001006/article/details/126457158)
- [JAVA内码和外码](https://blog.csdn.net/chen404897439/article/details/102295833)
- [JAVA中其实用的是UTF-16编码](https://blog.csdn.net/weixin_44958119/article/details/115579329)
- `Java SDK`:`String` 表示一个 `UTF-16` 格式的字符串。