http://tommwq.tech/blog/2020/11/20/232
1 字符集和字符编码
字符集(charset)和字符编码(character encoding)是两个含义相近的概念。在历史上一段时间里,二者实际上指代同一个东西,就是为字符建立的数字编号。字符集建立了字符到数字的1-1映射。对于每个字符,都有唯一的数字与之相对应。映射的作用域是字符集能够识别的全部字符,值域是这些字符的数字编号。字符编码则是在字符集的基础上更进一步,规定了在计算机中如何表示这些数字编号。比如我们可以把字符“A”映射成65,这就是一个字符集(虽然只包含1个字符)。现在考虑我们如何在计算机中表示数字65。假设我们使用C语言,我们可以用char、short、int、long这4种数据类型记录数字65,因此产生了这个小小字符集的4种不同的字符编码。
char A_c = 0x41;
short A_s = 0x0041;
int A_i = 0x00000041;
long A_l = 0x0000000000000041;
一些字符集在编制的时候,就规定了数字编号的表示方式。对于这种情况,字符集和字符编码是等同的。
最初每个计算机厂商都设计了自己的字符编码。为了让不同厂商的系统可以相互通信,统一字符编码的需求产生了。最开始统一的是表示拉丁字母的ASCII码。拉丁字母数量较少,因此ASCII采用单字节编码。单字节可以表示256个不同的字符,这对于象形文字是远远不够的。因此产生了双字节字符集DBCS(Double-Byte Character Set)。DBCS用两个字节记录字符的编号,最多支持65536个字符。如果只考虑常用字符,对于大部分文字,DBCS已经足够用了。
厂商希望将软件销售到不同的地区,因此希望软件能够支持多种文字。这个就要求建立一个支持多种文字的字符集。国际标准化组织ISO和Unicode联盟分别着手进行这项工作。ISO制作的字符集叫做UCS(Universal Character Set),通过标准ISO 10646进行规范。Unicode联盟编制的字符集叫做Unicode。随着工作的进行,两个组织认识到建立统一字符集的重要性,并着手合并工作成果,这就是今天大家看到的Unicode(严格来说是Unicode 2.0及以后版本)。如今UCS和Unicode可以认为是等同的。ISO为UCS定义了两种字符编码,分别使用2字节和4字节记录字符编号,因此称为UCS-2和UCS-4。Unicode联盟为Unicode定义了3种字符编码,分别是UTF-8、UTF-16和UTF-32。UTF-16和UTF-32可以看做和UCS-2、UCS-4是等同的。UTF-8则采用变长编码,编码长度可以是1、2、3或4字节。变长编码的优点有2个:可以兼容ASCII,对于包含拉丁字符较多的文本,编码的总长度较小。当然定长编码也有自己的优势:解析简单,使用方便。
编码长度 | 字符范围 | UTF-8编码 |
---|---|---|
1 | 0x000000~0x00007f | 0xxxxxxx |
2 | 0x000080~0x0007ff | 110xxxxx 10xxxxxx |
3 | 0x000800~0x00ffff | 1110xxxx 10xxxxxx 10xxxxxx |
4 | 0x010000~0x10ffff | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
对于多字节定长编码,需要考虑字节序的问题。因此UTF-16和UTF-32又被分为UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE。为了确定字节序,Unicode引入了BOM(Byte Order Mask)。BOM是编码首部的几个字节,是可选的。
UTF-8 | 0xef 0xbb 0xbf |
UTF-16LE | 0xff 0xfe |
UTF-16BE | 0xfe 0xff |
UTF-32LE | 0xff 0xfe 0x00 0x00 |
UTF-32BE | 0x00 0x00 0xfe 0xff |
2 常见字符集/字符编码
国内常用的字符集/字符编码有:
- GB2312。GB/T 2312-1980字符编码收录常用汉字6700多个,采用DBCS。
- GBK。GB/T 2312的扩展,包含2万多个汉字,DBCS。
- Big5。台湾地区采用的中文字符集,收录1.3万个汉字,DBCS。
- GB 18030-2005。收录汉字7万个,变长编码。
- GB/T 13000。等同ISO/IEC 10646或Unicode。
- ISO-8859-1/latin1。在ASCII基础上增加了对部分欧洲文字的支持。
- UTF-8。Unicode字符集的一种变长编码方式。
- UTF-16。采用双字节编码的Unicode字符。
- UTF-32。采用四字节编码的Unicode字符。
- ANSI。Windows的记事本程序提供了一个叫做ANSI的编码。实际上它是本地DBCS的别名。在中文Windows系统中,ANSI即GBK。
3 乱码和生僻字
3.1 锟斤拷
这是网站经常出现的乱码。unicode有一个特殊符号0xfffd,用于替换无法识别的字符。假设一个HTML页面采用GBK编码。Web应用在读取HTML时,却以unicode进行解码,其中有些无法解析的字符就会被0xfffd替换。当传输给客户端时,如果采用了UTF-8编码,0xfffd将编码为0xef 0xbf 0xbd。而“锟斤拷”三个子的GBK编码是 0xef 0xbf 0xbd 0xef 0xbf 0xbd。
3.2 PUA(Private User Areas)和一字双码
为了提供兼容和扩展,字符集通常会预留一部分编号,供使用者自行约定用途。这部分编号叫做PUA(Private User Areas)。在字符集的后续版本中,保证不会使用PUA中的编号。GBK和Unicode都有PUA。假设在字符集版本1中,缺少了字符X。使用者在PUA中为X分配了编号CHARSET_1_PUA(X)。在字符集版本2中,增加了字符X,并分配了编号CHARSET_2(X),就会出现一个字符拥有两个编码的情况。如果一个计算机系统使用字符集版本1,另一个使用字符集版本2,它们在通信的时候,就会出现无法识别字符X的情况。
为了推进电子政务建设,2003年公安部委托北大方正公司建立生僻字库,叫做方正人口信息字库(包括字体和输入法)。为了处理当时GBK和Unicode尚未支持的生僻字,方正人口信息字库使用了大量的PUA编码,包括4700多个Unicode PUA编码和1400多个GBK PUA编码。其中一些字符被添加到后续的Unicode版本中,因此出现了一字双码的情况。在新系统上生成的,包含一字双码字符的数据,在老的系统上无法识别。这里面最著名的,当属“䶮”字。