考虑到字符编码问题是一个有趣, 在开发中经常会遇到并且稍微有点麻烦和棘手的问题, 但是在网络上却很难找到将这个说的比较细致和成体系的文章, 所以我写了这篇笔记, 期间会尽量的查阅资料和参考文档, 将关于计算机编码的问题捣鼓清楚.
在笔记的后半部分, 我也会针对Python3的Unicode做一些说明, 并且当说到Unicode的时候, 我会尝试说明一些关于UTF-8 with BOM在Linux/Unix内核中的一些矛盾.
关于我搜寻和参考的资料, 都会贴在文章的最后.
这个世界上没有纯文本, 如果你想要读出它, 就要知道它的编码.
基本概念
首先我们要说明一些关于字符编码的基本概念, 有了这些概念的理解阅读后面的内容才不费劲~. 由于我默认将文章的受众定位成了有计算机科学基础的学生, 所以最基本的概念就直接一笔带过了.
位
就是我们熟知的比特(Bit), 也是二进制位, 因此有0和1两个值. Bit是Binary digit
的缩写.
是计算机用来表示信息的最小单位.
字节, 字, 字长
将一连串的比特组合在一起, 就构成了位串. 由于一个位的表示能力有限, 所以我们更多的情况下使用的是特定长度的位串来表示信息. 为什么说是特定长度呢? 标准, 标准! 我们制定几种不同长度的位串, 来表示不同的信息, 比如说有:
半字节(nibble, 使用4个比特.)
字节(byte, 使用8个比特, 是现代个人计算机的最小的存取和寻址单位)
字(WORD, 视具体操作系统位数决定字节数* 下面补充说明)
双字(DWORD, 视具体操作系统位数决定字节数)
四字(QWORD, 视具体操作系统的位数决定字节数)
补充一点, 关于字节的位数, 一般说来都是8个bit, 但是这也只是一个标准制定的而已, 其实过去也有非8位的字节标准. 在一些严谨的计算机文献中, 会使用八位组(Octet)来代替字节.
刚刚就说了, 一个位的表示能力有限, 所以我们想到使用字节, 而对于最小单位的字节来说, 仍然不能充分发挥计算机的运算能力, 所以对于计算机来说, 更高效处理的单位长度其实是字的长度, 也就是字长. 这也就是上面说的视具体操作系统位数决定的. 其实这样说不精确, 甚至说不是很对. 因为字长实际上是由CPU对外的总线宽度决定的, 它决定了CPU一次处理的数据的实际比特位.
所以, 64位的CPU可以使用32位的操作系统, 但是这样不会发挥出它应有的性能.
字符集
字符集, 其实就是一堆字符的集合, 包括各种文字, 数字, 字母, 音标, 标点符号, 图形等等. 将这些字符进行编号放在一起形成的序列, 就是字符集 (Charset) .
常见的字符集有
ASCII
ISO8859-? 系列
GB(GB2312, GBK, GB18030)系列
BIG5
Unicode
只有字符集没有什么卵用.
编码&解码
这个很好理解了,就是将信息从一种格式转换成为另外一种格式.
字符编码
编码, 刚刚说了, 就是格式之间的转换, 而计算机中的字符编码, 就是将字符转换(或者说映射)成为二进制数的过程. 这样, 我们就可以在计算机中方便的表示, 存储, 处理, 以及在网络中传输字符信息了.
这也是一种抽象.
代码页
代码页 (code page), 真的是一个很迷的概念.主要是来源于IBM的字符数据表示体系结构. 经常是被理解成是字符集的别称, 但这是不对的. 维基百科给出的概念是:
In computing, a code page is a table of values that describes the character set used for encoding a particular set of characters, usually combined with a number of control characters.
大体翻译过来就是说是一个描述字符集的值表, 用于编码一组特定的字符(包含一些控制字符). 这么看来还真的就是字符集的意思.
但是仔细看一下, 它是在描述字符集, 就是说根据具体的情况, 代码页可能会对字符集做一定的扩展, 在早期计算机还没有出现图形界面的时候, IBM称呼BIOS所支持的字符集位代码页, 而由于该代码页是烧录在显卡上的, 所以也被称为是OEM代码页.
正因为此, 每个厂商有自己的代码页, 所以代码页也叫做"内码表". 当我们想要知道一个二进制字节是什么字符的时候, 就要去根据系统设置的代码页去查表. 所以, 只有在操作系统的层面上, 我们才说代码页这个概念.
字符编码模型
这是一个极其重要的概念了, 模型的设计直接反应编码系统的组成结构和相关性. 例如, 在ASCII为代表的简单字符编码模型中, 刚刚所说的字符集和字符编码就是一个玩意, 没有什么区分.
而在我们后面将要隆重介绍的Unicode和UCS(通用字符集)中, 这个模型将会稍显复杂, 因为他会考虑很多内容.
UCS其实就是ISO/IEC 10646标准, 全称是通用字符集. 和Unicode字符集保持同步和一致. 实际上这个标准就是Unicode联盟和ISO/IEC共同制定的. 因此UCS的具体实现就是Unicode的具体实现, 即UTF-8, UTF-16, UTF-32.
在Unicode技术报告#17中, 描述了Unicode的字符编码模型. 报告中阐述和引入了巨多名词和概念, 我们慢慢展开.
先来说一下字符编码模型考虑的内容
有哪些字符
字符编号是什么
如何将编号映射成逻辑序列, 即码元序列
如何将码元序列转换成物理层面的字节流
在一些特殊或者复杂环境中, 如何将字节序列进行适应性的处理
仔细思考一下, 就不难发现, 现代字符编码模型和过去的简单字符编码模型不同的地方就是它更关注: 通用, 不同编码方式 这两点. 类似我们的解耦, 现代字符编码模型将字符集和字符编码方式进行了解耦, 使得更容易扩展, 更加通用, 一套字符集, 我们可以不同的编码方式进行处理. 也就是字符集和字符编码实现是一对多的形式.
根据上面所说的考虑内容, Unicode字符编码模型是这样分层(分级别)的:
级别一 抽象字符表ACR(Abstract Character Repertoire):明确字符的范围(即确定支持哪些字符)
级别二 编号字符集CCS(Coded Character Set):用数字编号表示字符(即用数字给抽象字符表ACR中的字符进行编号)
级别三 字符编码方式CEF(Character Encoding Form):将字符编号编码为逻辑上的码元序列(即逻辑字符编码)
级别四 字符编码模式CES(Character Encoding Scheme):将逻辑上的码元序列映射为物理上的字节序列(即物理字符编码)
说一下, 这里有些文章会说是5层, 但是根据UTR中的说法, Unicode字符编码模型应该是四个级别, 所谓第五个层次TES, 应该只能算作是一个重要的概念.
报告的原文(第7版)摘几段:
The four levels of the Unicode Character Encoding Model can be summarized as:...
In addition to the four individual levels, there are two other useful concepts:…TES
However, four levels need to be defined to adequately cover the distinctions required for the Unicode character encoding model.
无论怎么看, TES都不是算是模型的层次.
现在我们慢慢展开说一下这四个等级.
1. 抽象字符表 ACR
在这里, 我们通过定义抽象字符的无序集合来确定字符的范围. 字符表可以是封闭的, 也就是在制定时就决定了, 典型的例子是ASCII以及ISO8859系列, 而Unicode为了达到通用的目的, 提出设计开放的字符表, 也就是说, 我们可以随时添加新的字符到表中.
这里我们所说的抽象字符是不具有的字形的, 所以才说是抽象的.
稍稍总结一下ACR的三个特点: 无序, 封闭和开放, 字符不具有具体的字形.
2. 编号字符集 CCS
一个编号字符集定义成是从上面的抽象字符集合到非负整数集合的映射. 这些整数不一定要连续, 我们把定义了整数的抽象字符的这个位置(想象一个表格的单元格)称为码点(code point)(暂时存个疑问), 所以这样就形成了一个编号空间(Code space, 也可以叫做码点空间). 一个存在上限的有多种方式描述的非负整数范围. 例如, 我们可以通过一对非负整数: GB2312的汉字编号空间就是94 x 94. 也可以直接使用一个非负整数: ISO-8859-1的256, 或者也可以使用字符的存储单元尺寸, 比如ISO-8859-1的范围是2^8 = 256.
回到上面的码点这个概念, 并不是说码点的数量和抽象字符的数量(编号)是一致的, 这是因为在我们的CCS中, 还存在了非字符码点和保留码点. 不仅如此, 多个码点可能还对应这同一个字符, 比如: \u51c9
与\uf979
的这两个码点是同一个字符“凉”(注意, 这并不是汉字liáng, 这是一个字符.). 更多的例子可以在这里查询到: CJK兼容-F900-F921. 还有, 例如上面的注音符号, 他就是由多个码点表示的, 由基本符号的a加上注音符号.
最后稍微注意, 这里我们说的是编号, 而非编码. 这是两个截然不同的概念. 其实这一步和计算机丁点关系都没有, 因为没有涉及到下面说的字符编码方式和字符编码模式.
3. 字符编码方式 CEF
从这一步开始, 就开始进入到计算机的表示了. 我们知道常见的数据类型也就是单,双,四字节, 分别能够表示256, 65536, 4294967296个码位. 那么现在这一级别需要考虑的就是如何将无限扩展的(上面说了Unicode是开放的ACR, 也许现在就有新的emoji表情被添加到字符表中), 不仅如此, 过去诞生且正在使用的那些字符编码我们是抛弃不用(显然不可能), 还是向下兼容, 如果是兼容的话, 是完全兼容, 还是部分兼容 ?
这些问题的解决方案 就是我们的CEF了. CEF将字符集中字符的码点值转换成有限长度的编码值, 这个编码值就是之前提到的码元序列, 码元(code unit)就是这个序列的单位名称. 这里的转换也还没有这么具体, 只不过是逻辑上的方式.
对于ASCII这样的简单字符编码模型, 字符编码其实就是字符编号, 而它的编码方式就是简单的直接映射. 对于Unicode这样的现代复杂字符编码模型, 字符编号和字符编码并不一定相等, 而映射方式也不一定是直接的.
到这里你就更加清楚了, CEF其实就是字符编码标准的实现方式. 例如: UTF(Unicode/UCS Transformation Format)-8, UTF-16, UTF-32就是Unicode的编码方式.
上面的说法似曾相识吧! 很多博客, 网站都是这么说的. 其实也勉勉强强可以这么理解.
4. 字符编码模式 CES
终于到了这一级别, 在这一个级别, 就会将物理层面上的具体实现纳入考虑. 包括对各种不同的硬件平台与操作系统设计上的差异考虑. 在这一层, 码元序列就会转换成为字节序列.
由于太具体了, 所以我们就把这一部分的内容丢到后面吧.