2018年3月12日星期一

字符编码/字符集详解

一.引言


工作中,经常会遇到乱码,字符集转换等问题,尤其在python处理汉字,以及MySQL生僻字处理的时候。本文详细解释下字符集以及字符编码的用途,关于python处理中文,以及MySQL中的字符集相关,后续再聊。

我们知道,计算机看到的都是二进制,文本文件中的0/1,怎么转换成人类可读的文字呢?这就涉及到编码以及解码,简单来说将0/1进行结构化解读,通过字符集-编码-解码的对应关系,进行正确地存储和读取。
那问题来了,编解码到底是怎么个过程,字符集是什么?有哪些字符集,它们之间哪些兼容哪些不兼容?下文尝试解释清楚。

二.名词解释


  • bit
    • 一个二进制位,可以取值0或者1。两个bit可以组成二进制的00,01,10,11. Nbit可以表达2N次方个可能的值。
  • 字节
    • 一个字节包含8 bit,所以二进制表达范围可以从00000000~11111111。一共可以表达256种不同的值。字节是计算机存储的基本单位。
  • 字符
    • 指计算机中可以使用的独立的数字,符号,各国家文字,emoji表情等等。每个英文字母就是一个单字节字符。而一个中文的字符,可能占2个或三个甚至4个字节,所占字节数的不同是因为字符编码不同导致。每个字符都需要有对应的编解码规则才能被计算机处理。在对字符进行编码时,每个字符对应的编码位置称为码位(code point)。
  • 字符集
    • 是多个字符的集合,表达了字符与字符同一个字符集内的字符采用同一套编解码规则,而由于其采用的编解码规则的不同,导致每个字符集所能表示的字符的数量也有不同,每个字符集表达的能力是有范围限制的。 常见字符集包括ASCII, GBK, GB2312, UTF-8等。
  • 大端/小端(big endian/little endian)
    • 关乎计算机如何处理内存中的多个字节。只针对多字节字符时需要考虑(UTF-8下不需要考虑,原因见下文)。例如“汉”字的unicode编码是6C49,写入文件时到底是6C还是49写在前面决定了读取编码的正确姿势。6C在前就是big endian , 49在前就是little endian。
    • 大小端的存在是因为各组织的设备处理字符时采用了不同的方式。例如IBM服务器默认使用小端方式,而Motorola/SUN等使用大端方式。当不同设备之间进行文件传输或者网络传输时,需要让对方明确知道自己的字节顺序,否则必然出错。
    • 八卦一下这种叫法的出处:《格列佛游记》中人们常常争论"吃鸡蛋时先敲碎鸡蛋的大端还是小端",这些争论甚至会引发战争~

三.常见字符集及其字符编码

1.ASCII字符集


       ASCII(American Standard Code for Information Interchange 美国信息交换标准代码)码使用一个字节中的7位来表示一个字符,所以这个字符集能表达128个字符,其编码方式ASCII码表如下:

该字符集最古老也最受限,基本只能满足早期英语国家的基本需求。
      我们注意到一个字节是8位,而ASCII只定义了其中7位的含义,最高位加入后能够表达的128-255号的字符被不同国家地区进行了不同的字符映射,比如,144在阿拉伯人的ASCII码中是گ,而在俄罗斯的ASCII码中是ђ。这给解码造成了大麻烦。这个麻烦的解决引入了内码表(即编码的翻译表)进行解决,悲催的是,使用者需要清楚不同时候切换正确的内码表。

2.GB2312, BIG5, GBK, GB18030字符集

ASCII字符集处理不了中文,为了处理汉字,我国开始发布简体中文汉字国家标准。现有出现了GB2312, GBK, GB18030编码。而以前见到过的BIG5是台湾地区单独发布的繁体字字符集。
  • GB2312
    • 1981年5月1日发布的简体中文汉字编码国家标准。GB2312对汉字采用双字节编码,收录7445个图形字符,其中包括6763个汉字。
  • BIG5
    • 台湾地区繁体中文标准字符集,采用双字节编码,共收录13053个中文字,1984年实施。
  • GBK
    • 1995年12月发布的汉字编码国家标准,是对GB2312编码的扩充,对汉字采用双字节编码。GBK字符集共收录21003个汉字,包含国家标准GB13000-1中的全部中日韩汉字,和BIG5编码中的所有汉字。
  • GB18030
    • 2000年3月17日发布的汉字编码国家标准,是对GBK编码的扩充,覆盖中文、日文、朝鲜语和中国少数民族文字,其中收录27484个汉字。GB18030字符集采用单字节、双字节和四字节三种方式对字符编码。兼容GBK和GB2312字符集。
可以看到,字符集表达范围为 GB2312 < GBK < GB18030,他们之间互相兼容,但是BIG5与他们三个完全不兼容。

3.Unicode字符集以及UTF-8/UTF-16字符编码

GB码是中国国家发布的字符编码,而非国际标准。Unicode,通用字符集(Universal Multiple-Octet Coded Character SetUCS),是国际标准字符集,它将世界各种语言的每个字符定义一个唯一的数字表达,以满足跨语言、跨平台的文本信息转换。Unicode字符集与ASCII编码兼容,但是和GB码并不兼容。
看一下下面的字符编码表达示例,直观感受下,包括普通英文字母,汉字以及emoji表情:


字符GB2312GBKGB18030BIG5UNICODE
X5858585858
BABABABABABA6C49
87D687D6F8A656CD
FE9F4DAE
😊9439FD361F60A

Unicode的一个显著特征,是强制定义了世界上每个已知字符的数字编码值,这个值甚至可能是随机指定的,而且不会给出任何解释。具体的规则Unicode字符平面映射
再次强调!Unicode本身只是一个用来映射字符和数字的标准。它对支持字符的数量没有限制,也不要求字符必须占两个、三个或者其它任意数量的字节。
细细探索下Unicode 的编码,目前分为17组编排,0x0000-0x10FFFF,每组称为一个平面,可以表示 65536 个字符。第一个平面中的字符是常用字符,称为 基本多语言平面(Basic Multilingual Plane, BMP),其他平面称为辅助平面。目前只用了少数平面。wikipedia中的图表引用至此。


那么问题来了,Unicode字符是怎样被编码成内存中的字节呢?目前常见的Unicode字符编码包括UTF-8以及UTF-16。我们分别看下这两种字符编码如何能够表达上述平面的信息。

3.1. UTF-8(Unicode Transformation Format-8 bits)

UTF-8将0x0000-0x10FFFF的编码空间划分编号为1,2,3,4 的四段,这四段中的字符占用的空间分别是 1 个字节,2 个字节,3 个字节,4 个字节。如下图所示:
  • 0x00-0x7F 单字节编码,即兼容ASCII。编码格式:0XXXXXXX
  • 0x080-0x7FF 两个字节编码。编码格式: 110XXXXX 10XXXXX
  • 0x0800-0xFFFF 三个字节编码。编码格式:  1110XXXX 10XXXXXX 10XXXXXX
  • 0x010000-0x10FFFF 四个字节编码,编码格式:  11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
可以看到UTF-8对ASCII编码完全兼容!

为什么UTF-8字符编码没有大小端之分?
这里隐含着另外一个疑惑,既然每个字符可能是1-4个字节,为什么UTF-8全名里面包含了8 bits,这到底是什么鬼?
      本质上,UTF-8的编码方式是采用8bit即一个字节作为编码单位,解码算法的做法一次还是读取一个8bit(即一个字节)。读完一个读下一个,直接到计算出一个完整字符的信息。
      而UTF-16或者UTF-32,则使用16bit或者32bit作为编码单位,解码算法要处理UTF-16,则一次读取两个字节,这两个字节CPU处理的时候,就需要区分字节顺序了!
      既然UTF-8是基于字节进行编码,而大小端是针对多字节的字节顺序处理,那大小端在UTF-8下肯定就没有意义了!

3.2. UTF-16

UTF-16相对UTF-8更复杂一点,具体编码方式可以查阅wikipedia,不做展开。
UTF-16字符编码同样也是一种变长编码方案,使用2个字节或者4个字节来表示一个字符。对于常用字符使用2个字节表示(65536个字符),其他采用4个字节。

可以简单比较下UTF-8和UTF-16,前者对ascii完全兼容,单字节就够。英语世界网络世界使用utf-8,基本没有迁移成本,而且相比utf-16节省了一半的字节消耗,优势明显。utf-8也是目前最普及的Unicode字符编码方式!

3.3. 关于BOM(Byte Order Mark)


BOM是一个Unicode字符,U+FEFF,可以用在一个文本流的开头,来标记

  1. 字节顺序,是大端还是小端
  2. 使用哪种Unicode字符编码,例如是UTF-8还是UTF-16或者其他什么

      BOM是可选的而非必须的,如果要用,必须在文本的开头。BOM一般使用在文本信息交换的场景下。
      BOM的编码如果出现在文本中间,会被解释为"zero-width non-breaking space",效果上等同于忽略了。
      上文已经解释了UTF-8下无大小端的区分,这时候如果还有BOM,那基本就是标记文本使用方应该用UTF-8进行字符解码。但实际上并没有必要。

4.字符集定义方法

上文我们可以明确的知道所有的字符串,要正确的进行解读,需要有一个正确的编码+解码方式,那当我们拿到一份文本的时候,如何才知道其正确的解码方式呢?
有一些标准做法如下:
1) Email
使用Content-Type: text/plain; charset="UTF-8"作为表单的header

2) Web
最优的做法是在http header里面指定Content-Type: text/plain; charset="UTF-8",但是实际操作上web server可能处理大量的不同字符集的页面。实际的做法:
在每个HTML页面代码前面部分加上类似如下的代码:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
有人或许会疑惑HTML页面本身还没确认用什么编码解读,怎么依赖其内容了,还好上面这部分是基于ASCII字母,基本上所有编码都能读懂,在遇到生僻字之前,就基本能确认解码应该用怎样的字符集了!

如果web浏览器在http header和HTML meta标签中都没找到字符集设置怎么处理?答案是浏览器比如IE会自行根据内容猜字符集。如你所料肯定不会非常靠谱的,所以不要纠结了,用规范的方式准没错!


参考文献
  • https://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Data/endian.html
  • https://zh.wikipedia.org/wiki/Unicode%E5%AD%97%E7%AC%A6%E5%B9%B3%E9%9D%A2%E6%98%A0%E5%B0%84#.E5.9F.BA.E6.9C.AC.E5.A4.9A.E6.96.87.E7.A7.8D.E5.B9.B3.E9.9D.A2
  • https://en.wikipedia.org/wiki/Byte_order_mark
  • https://www.zhihu.com/question/20167122
  • http://www.qqxiuzi.cn/bianma/zifuji.php
  • http://stackoverflow.com/questions/701624/difference-between-big-endian-and-little-endian-byte-order
  • https://en.wikipedia.org/wiki/UTF-8
  • https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/





没有评论:

发表评论