软件开发中的字符编码问题的思考
字符编码问题从上层到底层是个非常大的系统工程,但是从普通程序员的角度,只需要了解其核心和主干的知识。
TL;DR
计算机底层处理和存储的是 01 构成的比特流,而我们所用的文本和字符将会被编码为这样的比特流。
因为历史原因,出现了不同的标准,定义了不同的编码方案,对能容纳的字符集合也不一样。Unicode 是现在的国际标准,理论上(将)包含地球上所有的字符,并且还将继续扩展(例如支持 emoji)
Unicode 是标准,UTF-8,UTF-16,UTF-32 是实现。(这句话并不准确,Unicode 是一个字符编码系统,分为多个层次,UTF-8 是某个层次的实现[^1]CEF:Character Encoding Form)
UTF-8 是现在事实上的标准实现,主流的网页都使用 UTF-8
字符与编码
计算机的世界是 0 和 1 构成的, 所有的字符如'A','0','道'最终都将表示为 0-1 的序列来进行处理、存储和传输。
例如"Hello World"使用 ASCII 编码将会表示为
这个从字符到到二进制串(比特流)的过程就是编码(encoding), 反之则是解码(decoding)。
在实际中,情况要复杂很多,因为 ASCII 只有 7 位参与的编码(第一位是 0),最多只能表示 128 个字符,这在现实中显然不够,光汉字就有 6000 多个了,更何况还有其他语言也有大量的字符。也因此各个组织和国家制定了许多编码标准,例如我国的 GB2312, GBK 等等。 这些标准相互不完全兼容,带来很多问题,常见的就是乱码问题,例如 GBK 编码的文件用 UTF-8 打开就是一团乱码。为了国际上的沟通便利,也为了降低程序员的开发维护成本,Unicode 出现了,它最开始是个字符集(charset),把人类现在使用的符号都纳入其中,并且规定了唯一字符编号(称为码点,code point)。Unicode 现在已经是国际通行的标准了。
Unicode 只定义了字符集和唯一字符编号,但是没有定义编码方式(即如何用二进制表示)。这个又涉及了很多考虑因素,例如单 byte 最多只能编码 256 个字符,定长的 n 字节可以编码(2^(8n))个字符,但是这会带来不必要浪费,在处理、存储、通信的各个环节都会增大开销。因此比较好的编码方案是平均码长比较小的方案,通常都是变长(长度变化, variable-length)的:
UTF-32 使用定长的 32 bits 来编码,每个字符有四个字节,这非常简单但是对空间是巨大的浪费
UTF-16 和 UTF-8 是变长编码,UTF-8 会使用 1-4 个字节进行编码,是互联网上应用最广泛的 unicode 编码方式。UTF-8 还对 ASCII 编码是二进制兼容的。
关于 Unicode 和 UTF-8 的关系,知乎网友hu zhi的回答非常精妙,如果学过通信的应该很容易 get:
用通信理论的思路可以理解为:unicode 是信源编码,对字符集数字化;utf8 是信道编码,为更好的存储和传输。
小结: unicode 提供了一个统一的字符集和字符代码(码点),解决了字符集不统一的问题,提供了跨语言跨平台的方案; utf8 提供了压缩的编码; UTF-8 编码对 Unicode 进行了进一步的编码,有效地节省了传输的带宽和存储空间。
UTF-8
UTF-8 的编码规则二条:
对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
对于 n 字节的符号(n > 1),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
@Note: UTF-8 通过第一个字节有多少个 1 指定了该字符有多少个字节,从而实现了变长
@Note:在 UTF-8,中日韩字符(Unicode 0x4E00 - 0x9FFF)是三个字节的。少数罕见字符可能例外。
值得一提的是,在具体的二进制表示中还有字节序的问题,也就是通常说的大端小端的问题(little-endian & big-endian)。Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),也称为 BOM(Byte Order Mark),用FEFF
表示。这正好是两个字节,而且 FF 比 FE 大 1。如果一个文本文件的头两个字节是FE FF
,就表示该文件采用大头方式;如果头两个字节是FF FE
,就表示该文件采用小头方式。
最佳实践
在实际开发过程中,至少面对着三种字符编码问题:
在程序外部存储(IO),用什么编码
在程序内部处理的时候,用什么编码
在内部处理(在内存)和外部存储(磁盘)上,考虑转化
对于 1,外部 IO,通用性第一,因此 UTF-8 是最好的选择。UTF-8 被浏览器和编辑器广泛支持。UTF-8 用来作为持久化和交换的编码是更优的选择。 btw,UTF-8 与 ASCII 完全兼容,也是个优点,可以节省空间。这也适用于前后端交换数据的情况。
对于 2,程序的内部处理,这种情况比较复杂了。和具体的业务约束有关系,也和所用的平台、语言和框架有关系。Unix 世界中默认的系统编码是 UTF-8, 而 Windows 默认 UTF-16, 其历史遗留问题给开发者带来了很多误解。就语言来说,像 python 和 java 原生支持 unicode, 就比较舒服。而对于 C/C++,要稍微痛苦一些,char 只代表一个字节的内容,std::string 可以理解为字节的容器,并没有编码方式的属性,字节实际被解析为什么字符还是取决于系统的编解码方式。在这种情况下,直接用 UTF-8 可能在中英文混用的时候无法得到正确的字符串长度。因为有的编码是多字节,甚至是变长字节的。如果使用 std::string 存储多字节编码的字符串,则
.length()
和 分割方法可能都无法如预期工作。例如:
因为在 UTF-8 编码中"你好 "占用 3*2 个字节, 因此 u8"我爱C++"
占用 9 个字节,所以 string.lenght()的结果是 9。
Note: 程序中处理的不是字符,是字节流。 从现在的视角来看,char 类不再表示字符类型,表示"字节"更加恰当。
C++中可以使用第三方库提供的带有编码信息的字符串类。例如 Qt 实现了自己的 QString 方法,内部用存储的是 unicode,并且提供了一些编码转换的工具类。在 Qt 的程序中,内部统一用 QString 就不会遇到编码问题。第三方库 ICU-project 提供了跨平台的 Unicode 和全球化的支持。
utf8everywhere 更是主张在程序内部也使用 utf-8 的编码,并且在其网站首页列了长文说明这种理由。
对于 3, 如果内外部不一致的话,一定要做好编码方式的转换,C++11 以后提供了<codecvt>可以进行字符串编码的转换。
推荐学习资料
阮一峰的博客,字符编码笔记:ASCII,Unicode 和 UTF-8 , 这篇讲得详略得当,把 Unicode 和 UTF-8 的关系讲得比较清楚。
强烈推荐知乎专栏文章:刨根究底字符编码, 非常系统地介绍了字符编码的标准、历史和实现细节
对于中文编码感兴趣的,可以读一下中文编码杂谈
C++ 中的中文编码,里面有段代码实现了中文从 unicode -> uft8 的转换。
参考资料
UTF-8 的实现标准 UTF-8, a transformation format of ISO 10646
https://utf8everywhere.org/
版权声明: 本文为 InfoQ 作者【行者孙】的原创文章。
原文链接:【http://xie.infoq.cn/article/89ec412378f180a01b18fdd07】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论