日期:2014-5-19 浏览次数:21294次

理解字符编码
在论坛上不断看到有人受乱码问题困扰。于是整理了一些资料,希望可以帮助大家理解清楚“字符编码”这个不算复杂却很搞人的问题。
本文内容与SQLServer中文处理一文部分内容有重复,但重点不同,可结合着一起看。

下文中,我会用Python(V3.x)和SQLServer(2005)列举一些示例。
Python3对Unicode的支持非常好,清晰地划分字节流和字符串两种不同类型也极大地简化了对字符编码问题的理解,可以交互式执行这一点更是让简单测试一些语句方便到家了。(这正是选用Python列举示例的理由)
原本对Python不了解的朋友可参看Python入门二三事一文。


字节流(byte stream) VS 字符串(string)

要理解字符编码,首先要区分字节(流)与字符(串)这两类不同的事物。计算机内部只存储和处理字节,字符只是人类理解的概念。
在Python中,字节流和字符串分别对应bytes和str类型:(变量名b和s只是巧合,^___^|||)
Python code

# 示例-1
>>> b = b'Unicode\xe5\xad\x97\xe7\xac\xa6\xe4\xb8\xb2'
>>> type(b)
<class 'bytes'>
>>> s = 'Unicode字符串'
>>> type(s)
<class 'str'>


在SQLServer中,字节流和字符串分别对应varbinary/binary和varchar/nvarchar/char/nchar。

为了便于人类与计算机的沟通,人们需要在字节与字符之间建立对应关系,这即是字符编码(Character Encoding)。一种字符编码可以支持(即定义了对应关系)的所有字符称为一个字符集(Charset)。如ASCII编码支持的所有字符被称为ASCII字符集。这两个概念关系如此紧密,以至于可以视之为等价,区别仅在于前者是从对应关系角度命名,后者则是从所有字符集合的角度命名。事实上,在维基百科上这两个关键词指向的是同一个页面。

所谓乱码,或者是字符转换为字节(编码)与字节转换为字符(解码)的过程使用了不同的字符编码,或者是尝试将不是文本的字节流(可能是数据错误,也可能就是二进制数据)解码为字符。理解了这两点,乱码这个概念就没有存在的必要了。

ASCII与扩展ASCII的编码方案

最早形成标准并且至今仍有广泛影响力的字符编码莫过于ASCII。该字符集只使用单字节的低7位,即0x00到0x7f的范围。该字符集中包含了控制字符(现在可能只有其中几个是常用的)、数字、大小写英文字母和英文标点符号。ASCII是一个设计良好的字符编码规则,如充分利用码元、数字和字母的排布是连续的并且有着直观的对应、大小写字母可以通过一个bit的改变而相互转化,等等。其主要局限在于无法支持非英语的字符。ASCII只定义了128个字符,这对于拉丁语系来说尚且太少,更不必说包含成千上万字符的CJK语言和希腊、阿拉伯等世界各国语言。

第一种解决方案是扩展ASCII,把最高位为1的字节(即0x80到0xff的范围)使用上,从而还可以与ASCII保持兼容。Windows和IBM引入的代码页正是这种解决方案,例如,cp1252支持西欧国家的各种字符,cp936和cp950支持中文字符(分别用于简体中文和繁体中文系统)。中文系统中常见的字符编码如国标码(包括gb2312、gbk、gb18030,应用于中国大陆和新加坡)和大五码(big5,应用于台港澳)也是类似的原理。
事实上,许多不同的编码名称之间可能是等价或近似等价的,只是编码的制订方不同而已。例如,西欧语言的cp1252和latin1非常相近,中文的cp936和cp950分别近似等价于gbk和big5。
维基百科的字符编码条目列举了各种常见的字符编码。Python官方文档中还用表格列举了各种编码规则不同名称的对应关系。
对于大陆地区常用的中文编码,简单来说:gb2312支持常用的简体汉字,gbk/cp936支持常用的简体和繁体汉字,gb18030支持更多的汉字和部分少数民族字符,几者之间的关系为:ASCII < gb2312 < gbk/cp936 < gb18030(其中<表示兼容的关系)。虽然gbk/cp936也支持繁体汉字,但不要与big5/cp950混为一谈,二者是不同的编码方案,只是支持的字符有交集而已:
Python code

# 示例-2
>>> s = '中華'
>>> s.encode('gbk')
b'\xd6\xd0\xc8A'
>>> s.encode('big5')
b'\xa4\xa4\xb5\xd8'


想要了解更多相关细节,可以分别查看各种编码的维基条目,或者google相关的文章。

Unicode

前一种扩展ASCII的方案的主要缺点是各种编码各自为政,这便导致不同系统之间字符集可能无法兼容,一个常见的问题便是在一台电脑上保存的文本文件复制到另一台不同代码页设置的电脑上会显示乱码。例如字节流0xa1a2,在latin1、gbk、big5不同的编码方案中对应的就是不同字符:
Python code

# 示例-3
>>> b = b'\xa1\xa2'
>>> b.decode('latin1')
'¡¢'
>>> b.decode('gbk')
'、'
>>> b.decode('big5')
'﹜'


于是人们想要统一,由此产生的第二种方案便是Unicode,一个类似于巴别塔(Babel)的计划(不同的是Unicode成功了)。关于Unicode组织与国际标准化组织的ISO-10646工作组分别不约而同地制订统一字符编码方案、后来发现彼此的存在从而开始协作和共享、最终却依然各自发布标准的迭事可以参看Unicode维基条目。二者的编码方案是兼容的,差异主要是实现方式。我们接下来只谈如何理解Unicode。

Unicode可以分为编码方案和实现方式两个层面来看。

编码方案定义了字符与码位(Code point,可理解为一个整数值)的对应关系。Unicode定义了1114112个码位(当然并非所有码位都对应有字符,有些是保留为特殊用途,有些是暂时尚未定义,有些是预留为私有空间),划分为17个字符平面,编号0-16,每个平面包含65536个码位,其中编号为0的平面最为常用,称为基本多文种平面(Basic Multilingual Plane, BMP)。Unicode码位的表示方式是“U+”加上十六进制的码位编号,BMP的码位编号为4位的十六进制数,范围是U+0000到U+FFFF,BMP之外的其它平面的码位,编号为5到6位的十六进制数。
平时我们提到Unicode字符,绝大多数情况指的都是BMP的字符。其中,U+0000到U+007F的范围与ASCII字符完全对应,U+4E00到U+9FA5的范围定义了常用的中文字符(这些字符也都在GBK字符集中)。如在SQLServer上借助自然数辅助表可轻松查看常用Unicode字符的码位:
SQL code

# 示例-4
SELECT
    [码位(dec)] = n,
    [码位(hex)] =