字符集与字符编码 (charset & encoding)

时间:2021-10-20 03:02:27

乱码是个大坑,相信每个人都遇过,而且是个绕不过去的坑。我理解每个程序员都应该写一篇编码相关的博文,梳理自己对这一块的理解,下面是我反复理解多次之后的学习小结。

1、从记事本的不同编码说起:

打开记事本,输入“我我”,保存为ansi编码(其实是gb2312,这也是默认编码)。再分别另存为unicode(其实是utf-16 little endian)、unicodeBigEndian(其实是utf-16 big endian)、utf8,用UltraEdit打开,切换到二进制模式,内容如下:

编码 内容
ansi CE D2 | CE D2
unicode FF FE | 11 62 | 11 62
unicode big endian FE FF | 62 11 | 62 11
utf-8 EF BB BF | E6 88 91 | E6 88 91

可以看出,“我”在gb2312里的编码是CE D2,在utf-16 little endian里是11 62,在utf-8里是E6 88 91。FF FE是文件头,用来标识这个文件是unicode little endian格式的,同理,EF BB BF标识文件是utf-8格式。所谓的endian,是字节顺序,不同的操作系统,可能小字节在前、也可能大字节在前,既然不一样,就用一个文件头(学名叫BOM,byte order mark,字节顺序标识符)来标识。具体方法是:找一个在unicode里不存在的字符(FF FE)来表示,EF BB BF是FF FE在utf-8里的编码。各种编码的BOM详见下表:

bytes encoding
EF BB BF utf-8
FF FE utf-16 little endian
FE FF utf-16 big endian
FF FE 00 00 utf-32 little endian
00 00 FE FF utf-32 big endian

2、乱码的各种现象

文本保存在文件里都是字节byte,那这些byte数组是怎么显示为具体的字符呢?以ansi格式的“我”为例,CE D2显示在界面上的步骤是这样的:windows首先将CE D2转换成它内部使用的编码格式unicode,然后按照unicode编码去字体文件中查找字体图像,最后将图像画在窗口的指定位置上。如下:

  1. 字符首先以某种编码保存在文件中。
  2. windows将文件中的编码映射为unicode编码。
  3. windows根据unicode编码去字体文件中查找字体图像,并画在窗口上。

这3步中的每一步错了,都体现为一种典型的乱码。

  1. 错误1:如果弄错了编码格式,比如将“我”的utf-16编码11 62错存成了gb2312的文件,就会出现乱码。字符集与字符编码 (charset & encoding)
  2. 错误2:如果映射到unicode出错,例如出现了unicode里未定义的字符编码,windows就会使用缺省字符,通常是?。比如“我”的gb2312编码CE D2在unicode里尚未使用,所以映射不到任何一个有效的unicode编码。字符集与字符编码 (charset & encoding)
  3. 错误3:如果映射到了unicode编码,但在字体文件中找不到对应的字符,windows就会显示字体文件中的缺省图像:空白或方格。字符集与字符编码 (charset & encoding)

3、各种编码ascii、ansi(gb2312/big5/...)、unicode(utf8/16/32/...)

1) ascii用1个字节(共255个)表示所有英文字符。缺点很明显,就是不够用。对于欧洲那些表音的字母类,就已经捉襟见肘了,再遇到中日韩的表义字符动辄上万,就更不够了。于是ansi想出来填坑,但结果是反而把事情搞麻烦了。其实如果一开始就想到字符不够用的问题,再直接整出个全球统一字符集unicode,就不会有这个大坑了。所以说白了,乱码问题是个历史原因造成的问题(话说软件里那么大坑,哪些不是历史原因造成的!Y2K也是一例,但是谁也没那个远见,所以只能迂回前进了)。

2) 然后,各国都发现ascii不够用,于是各自造出自己的编码来满足需要,中国造出gb2312、*造出big5大五码、日本造出shift_jis、其他阿拉伯国家、印度也是类似的。先是IBM弄了代码页(CodePage)、再是微软继承了这一套再加了些自己的定义,整成了ansi,包括所有地区的代码页。通过这些代码页,windows可以实现各地区编码与unicode之间的相互转换。(注:代码页的具体文件为:c:\windows\system32\*.nls文件)代码页是为了兼容已有的程序而存在的,如果我们能强制所有程序都转到unicode,那各地区的ansi编码也就没有存在的必要了,说白了只是发展过程中的中间产物。

3) 最后是unicode。

3.1) 先说说字符集与字体编码的区别。对大多数字符集来说,字符集里的编码 = 字符编码。比如“我”字,在gb2312中的编码是CE D2,在big5中是A7 DA。唯独在Unicode里例外,Unicode使用4个字节/32位(实际只用了31位,最高位必须为0)唯一表示全世界所有可能的字符。unicode的容量为231-1 ≈ 21亿个,截止到2005年只用了10万个,所以应该足够用了。但每个字符4字节,太浪费空间了,于是就产生了各种编码方式,常见的有utf-8、utf-16、utf-32等。这里,unicode是字符集,utf-8/16/32是字符编码。同样是“我”字,在unicode里是00 00 62 11,在utf-8里是E6 88 91,在utf-16里是62 11,在utf-32里才是00 00 62 11。

3.2) unicode到utf8/16/32的转换方式就不详述了。无非又是一些位操作,对理解这个问题的主干无益。有兴趣的可以参看本文最后的引用。

4、对比一下各编码的存储效率

  1. 存储英文时:utf-8 = ansi > utf-16 > utf-32
  2. 存储中文时:ansi ≈ utf-16 > utf-8 > utf-32

当然,这里说的是大多数的情况,中英文混杂、多语言混杂、小语种文本都需要具体讨论。

5、参考

  1. 伐木丁丁鸟鸣嘤嘤:《浅谈文字编码和Unicode(上)》和他做的小工具CodeView:快速查看文本编码
  2. 阮一峰:《字符编码笔记:ASCII,Unicode和UTF-8
  3. 中韩翻译网 金圣镇:《字体编辑用中日韩汉字Unicode编码表