2.3 Unicode与UTF编码
Unicode(统一码、万国码)是为了解决传统编码的不兼容问题而产生的,它的目标是包含全世界所有语言的字符,为每个字符分配唯一的编码,从根本上解决乱码问题。
Unicode的设计思想
Unicode的核心思想很简单:
给全世界所有的字符分配一个唯一的数字编号(称为码位,Code Point),不管是什么平台、什么语言、什么程序。
Unicode的范围
Unicode使用0到0x10FFFF的码位空间,总共可以表示1,114,112个字符,目前已经分配了超过14万个字符,包含了几乎所有人类语言的字符、符号、表情符号等。
Unicode的码位通常表示为 U+xxxx 的形式,其中xxxx是十六进制的数值:
- U+0041 表示字符 ‘A’
- U+4E2D 表示汉字 ‘中’
- U+1F600 表示笑脸表情 ‘😀’
Unicode平面
Unicode的码位空间分为17个平面(Plane),每个平面包含65536个字符:
- 第0平面(基本多语言平面,BMP):U+0000 ~ U+FFFF,包含了最常用的字符,几乎所有常用汉字都在这个平面
- 第1-16平面(辅助平面):U+10000 ~ U+10FFFF,包含了生僻字、古代文字、表情符号等不常用的字符
UTF编码系列
Unicode只是定义了字符的码位,但是如何将这些码位转换为实际的二进制字节序列进行存储和传输,这就是UTF(Unicode Transformation Format)编码要解决的问题。
常用的UTF编码有三种:UTF-8、UTF-16、UTF-32,各有优缺点。
UTF-32编码
UTF-32是最简单的编码方式:
- 每个字符固定用4个字节(32位)表示
- 直接将码位的数值转换为4字节的二进制
- 是定长编码,处理非常简单
优点:
- 定长编码,字符串处理非常方便,计算长度、随机访问都是O(1)的复杂度
缺点:
- 空间浪费太大,对于英文文本,UTF-32的体积是ASCII的4倍
- 兼容性差,很多系统和软件都不支持
- 实际使用很少
UTF-16编码
UTF-16使用变长编码:
- BMP平面的字符(U+0000 ~ U+FFFF)用2个字节表示
- 辅助平面的字符(U+10000 ~ U+10FFFF)用4个字节表示(称为代理对)
代理对原理: 辅助平面的码位减去0x10000,得到20位的数值,分为高低各10位:
- 高10位加上0xD800,得到高代理(范围0xD800~0xDBFF)
- 低10位加上0xDC00,得到低代理(范围0xDC00~0xDFFF)
这样两个2字节的数值组成一个4字节的代理对,表示一个辅助平面的字符。
优点:
- 空间效率比UTF-32高,大部分常用字符只需要2字节
- 定长(对于BMP平面),处理比较方便
- Java、JavaScript、C#等语言内部的字符串表示就是UTF-16
缺点:
- 仍然有空间浪费,英文文本是ASCII的2倍
- 存在字节序问题,需要BOM标记
- 兼容ASCII但不兼容扩展ASCII
UTF-8编码
UTF-8是目前使用最广泛的Unicode编码,也是互联网的事实标准编码。它是一种变长编码,使用1-4个字节表示不同的字符:
UTF-8编码规则:
| 码位范围 | 字节数 | 编码格式 |
|---|---|---|
| U+0000 ~ U+007F | 1 | 0xxxxxxx |
| U+0080 ~ U+07FF | 2 | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 ~ U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
编码规则说明:
- 单字节字符:最高位为0,低7位就是ASCII码,完全兼容ASCII
- 多字节字符:第一个字节的前n位都是1,第n+1位是0,表示这个字符占n个字节;后续的每个字节都是以10开头
- 这种设计让UTF-8具有自同步性,从任意字节开始都能快速定位到字符的边界
示例:
- ‘A’(U+0041):单字节 → 01000001(和ASCII完全一样)
- ‘中’(U+4E2D):三字节 → 11100100 10111000 10101101
- ‘😀’(U+1F600):四字节 → 11110000 10011111 10011000 10000000
UTF-8的优点:
- 兼容ASCII:英文文本用UTF-8编码和ASCII完全一样,现有ASCII文本不需要修改就能用UTF-8打开
- 空间效率高:对于中文来说,UTF-8每个字符占3字节,和GBK差不多;对于英文来说只占1字节,比UTF-16省一半空间
- 无字节序问题:UTF-8不需要考虑字节序,不需要BOM标记
- 自同步性:编码设计合理,即使部分字节损坏,也不会影响后续的字符解码
- 广泛支持:几乎所有现代系统、编程语言、软件都原生支持UTF-8
UTF-8的缺点:
- 变长编码,字符串处理复杂度高,计算长度、随机访问都是O(n)的复杂度
- 中文占3字节,比GBK的2字节略大
BOM(字节顺序标记)
BOM(Byte Order Mark)是一种特殊的标记,放在文本文件的开头,用来表示文本的编码和字节序。
常见BOM标记
| 编码 | BOM标记 |
|---|---|
| UTF-8 | EF BB BF |
| UTF-16 LE(小端) | FF FE |
| UTF-16 BE(大端) | FE FF |
| UTF-32 LE(小端) | FF FE 00 00 |
| UTF-32 BE(大端) | 00 00 FE FF |
BOM的问题
- UTF-8的BOM是非标准的:UTF-8本身没有字节序问题,不需要BOM,很多系统和软件不支持带BOM的UTF-8文件
- 兼容性问题:很多程序处理带BOM的文件时会出现问题,比如Shell脚本、JSON文件、代码文件等,如果带BOM会导致执行错误
- 隐藏字符:BOM是不可见字符,很多编辑器不会显示,出现问题时很难排查
最佳实践:
- UTF-8文件不要加BOM
- UTF-16和UTF-32如果跨平台使用,建议加BOM,否则要明确指定字节序
- Windows系统的记事本默认会给UTF-8文件加BOM,这是很多乱码问题的根源,尽量不要用记事本编辑代码文件
编码选择建议
- 存储和传输优先选择UTF-8:这是目前的事实标准,兼容性最好,空间效率高
- 程序内部字符串表示:根据语言特性选择,比如Java/JS用UTF-16,Go/Rust用UTF-8,Python3用Unicode字符串
- 中文环境下如果需要兼容旧系统:可以考虑用GBK,但优先选择UTF-8
思考问题
- UTF-8、UTF-16、UTF-32各有什么优缺点?分别适合什么场景?
- 为什么UTF-8是目前使用最广泛的编码?
- BOM是什么?为什么UTF-8文件不建议带BOM?
- 计算一下“中国ABC“这个字符串用UTF-8、UTF-16、GBK编码分别占多少字节?