写点什么

软件开发中的字符编码问题的思考

作者:行者孙
  • 2021 年 12 月 14 日
  • 本文字数:2916 字

    阅读完需:约 10 分钟

字符编码问题从上层到底层是个非常大的系统工程,但是从普通程序员的角度,只需要了解其核心和主干的知识。

TL;DR

  1. 计算机底层处理和存储的是 01 构成的比特流,而我们所用的文本和字符将会被编码为这样的比特流。

  2. 因为历史原因,出现了不同的标准,定义了不同的编码方案,对能容纳的字符集合也不一样。Unicode 是现在的国际标准,理论上(将)包含地球上所有的字符,并且还将继续扩展(例如支持 emoji)

  3. Unicode 是标准,UTF-8,UTF-16,UTF-32 是实现。(这句话并不准确,Unicode 是一个字符编码系统,分为多个层次,UTF-8 是某个层次的实现[^1]CEF:Character Encoding Form

  4. UTF-8 是现在事实上的标准实现,主流的网页都使用 UTF-8

字符与编码

计算机的世界是 0 和 1 构成的, 所有的字符如'A','0','道'最终都将表示为 0-1 的序列来进行处理、存储和传输。


例如"Hello World"使用 ASCII 编码将会表示为


01001000 - H01100101 - e01101100 - l01101100 - l01101111 - o00100000 - ' '01010111 - W01101111 - o01110010 - r01101100 - l01100100 - d
复制代码


这个从字符到到二进制串(比特流)的过程就是编码(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 的编码规则二条:


  1. 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。

  2. 对于 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,就表示该文件采用小头方式。


最佳实践

在实际开发过程中,至少面对着三种字符编码问题:


  1. 在程序外部存储(IO),用什么编码

  2. 在程序内部处理的时候,用什么编码

  3. 在内部处理(在内存)和外部存储(磁盘)上,考虑转化


  • 对于 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() 和 分割方法可能都无法如预期工作。例如:


#include <iostream>#include <string>using namespace std;
int main() { const string love_cpp = u8"你好C++"; cout << love_cpp.length() << endl;// 9}
复制代码


因为在 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>可以进行字符串编码的转换。

推荐学习资料

  1. 阮一峰的博客,字符编码笔记:ASCII,Unicode 和 UTF-8 , 这篇讲得详略得当,把 Unicode 和 UTF-8 的关系讲得比较清楚。

  2. 强烈推荐知乎专栏文章:刨根究底字符编码, 非常系统地介绍了字符编码的标准、历史和实现细节

  3. 对于中文编码感兴趣的,可以读一下中文编码杂谈

  4. C++ 中的中文编码,里面有段代码实现了中文从 unicode -> uft8 的转换。

参考资料

  1. The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)

  2. What Every Programmer Absolutely, Positively Needs To Know About Encodings And Character Sets To Work With Text

  3. UTF-8 的实现标准 UTF-8, a transformation format of ISO 10646

  4. 汉字的unicode 编码和对应的utf-8

  5. UTF-8 and Unicode FAQ for Unix/Linux

  6. https://utf8everywhere.org/

发布于: 41 分钟前阅读数: 5
用户头像

行者孙

关注

Nothing replaces hard work 2018.09.17 加入

充满好奇心,终身学习者。 博客:https://01io.tech

评论

发布
暂无评论
软件开发中的字符编码问题的思考