字符编码概论

大约2年前对字符编码有过较深入的研究,后来零零散散的放到了《字符编码研究》中,不过由于粘贴比较仓促,思路不清晰并且不便于阅读。隔了一段时间,也忘记了一些内容。虽然网上已经有很多关于字符编码的文章了,但这些介绍字符编码的文章存在一个普遍的问题:没有结合具体的工具。而编码问题往往肆虐在各种设计不完善、概念混乱的软件中。故准备将整理旧文章,结合一些软件谈一谈字符集与字符编码。

了解 字符编码是编写程序不得不经历的关卡,毕竟所有的源代码都是由文本字符堆砌而成的。最初计算机的世界里只有英文,使用7bit就足以将所有的英文字符与控制字符编码,但由于计算机是8bit的,所以ASCII标准使用8bit编码,最高位置0.比如字母a的编码为01100001,八进制数为61,详细的编码方案点击此处查看。在这种编码方案下,一个字符从存储到显示、复制、传输都是统一的,然而计算机的发明者们显然没有想到原属于军事领域的机器会改变世界,也压根没有考虑太多兼容性。当计算机开始涌入其他国家之后,为了方便本地区的人使用,一系列扩充计算机编码的需求提上日程。公司(比如微软)会制定编码方案,国家标准局(GB)也会制定编码方案,一些计算机的学会机构(Unicode联盟)也会制定编码方案,字符编码的混乱世界由此拉开帷幕。

1.字符集与字符编码

在ASCII畅行的时代,字符集与字符编码并无区分。因为计算机磁盘中存储的内容与内存中读取的一致,与屏幕上显示的一致,与网络中传输的一致。表示字符a的’01100001’无论在哪里都是一样的。但是当locale编码出现后,情况开始转变。对ASCII的扩充导致了无法再用单一的一个字节来表示字符,比如GBK中使用两个字节来表示编码,那么存储到文件中时,哪一个字节放在首位呢?有了选择,也就会有不同的标准。关于哪一个自己放在首位,产生了Little Endian与Big Endian两种编码方案,此处我们暂压不题。另外,连续的多字节字符,如何划分他们的边界呢?总之,字符集无法全部使用单一字节,造成了字符集与字符编码两种概念的分道扬镳。具体说来,字符集完成字符与某种数制数对对应的问题,而字符编码则完成这种数制的数在磁盘上的存储问题。以Unicode体系为例,凡是人类语言中出现的字符,在Unicode中都有且仅有唯一的数与其对应;当准备将这种字符存储在磁盘上时,代表此字符的数可以选择不同的编码方式存储在磁盘中,比如选择UTF8编码方式或者UTF-16编码方式。但在这种分层概念清晰地浮出水面之前,世界上已经有很多字符集或者编码方案了。

2.中文字符集与字符编码

处于中国,当谈中文字符编码。在ASCII基础上,GB先后发布了GB2312(1981年)、GB13000(1993年)、GB18030-2000(2000年)、GB18030-2005(2005年),每次编码标准都兼容上一次的,并且所有的编码标准都兼容ASCII。

GB2312

全称《信息交换用汉字编码字符集·基本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。GB2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB2312。GB2312标准共收录6763个汉字,同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。GB2312中对所收汉字进行了“分区”处理,每区含有94个汉字/符号。这种表示方式也称为区位码。01-09区为特殊符号;16-55区为一级汉字,按拼音排序;56-87区为二级汉字,按部首/笔画排序;10-15区及88-94区则未有编码。举例来说,“啊”字是GB2312之中的第一个汉字,它的区位码就是1601。

但仅有区位码还不够,还需要将汉字存储在计算机中。在使用GB2312的程序通常采用EUC储存方法,以便兼容于ASCII。浏览器编码表上的“GB2312”,通常都是指“EUC-CN”表示法。每个汉字及符号以两个字节来表示。第一个字节称为“高位字节”,第二个字节称为“低位字节”。“高位字节”使用了0xA1-0xF7(把01-87区的区号加上0xA0),“低位字节”使用了0xA1-0xFE(把01-94加上0xA0)。而ASCII中的编码范围为0x01-0x7F,所以EUC-CN编码中是可以分辨出单个字节的ASCII字符的。由于一级汉字从16区起始,汉字区的“高位字节”的范围是0xB0-0xF7,“低位字节”的范围是0xA1-0xFE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。例如“啊”字在大多数程序中,会以两个字节,0xB0(第一个字节)0xA1(第二个字节)储存。(与区位码对比:0xB0=0xA0+16,0xA1=0xA0+1)。为直观显示,截取1区,2区,16区,17区的汉字编码图如下:

1区
1区

2区
2区

3区
3区

4区
4区

这里是GB2312的EUC-CN编码表,备份链接参照这里

这时候有了内码交换码的概念。内码指的是“将资讯编码后,透过某种方式储存在特定记忆装置时,装置内部的编码形式”;在交换文件前,文件提供者先将由内码形式储存的文件转换成交换码形式再做交换。在接收文件后,文件接收者再由交换码转成内码。所以可以认为区位码是一种交换码,而字符编码是内码。为了方便起见,许多系统的内码则直接使用交换码,如ASCII广为各种系统所使用。这有点类似我们后文中将要提到的字符集与字符编码,但与字符集不同之处在于,交换码会兼顾输入法层面。比如区位码就可以直接做成输入法,五笔输入法也可以认为是一种交换码,字符集的作用就简单多了。

微软的GBK

微软为了向世界推广windows,提出了针对不同国家代码页(Code Page)的概念,并使用GB2312制订了CP936。到1993年,Unicode1.1版本推出,收录中国大陆、台湾、日本及韩国通用字符集的汉字,总共有20902个。大陆制订了等同于Unicode 1.1版本的”GB 13000.1-93″,微软收录了GB 13000中的所有字符对GB2312进行了扩展(当然也是对CP936的扩展),补充了GB2312中没有的字符比如”啰”、”镕”等,称为GBK(也就是CP936的扩展),可见GBK是兼容GB2312的而非GB13000。而有趣的是,原始GB13000一直未被业界采用,后续国家标准GB18030技术上兼容GBK而非GB13000。GB2312、GBK都属于双字节字符集(DBCS)

虽然GBK与CP936相同,但后来可能会发生细微的变化使两种编码集产生差异,我们不在此列举他们,只简单的认为GBK就是微软在中国地区的代码页。在实际使用windows的时候,如果我们用记事本打开从其他区域windows拷贝过来的文件,很可能出现乱码,仅仅能正确显示ASCII中的字符,当将文本文件从一台电脑复制到另外一台电脑后,文件在磁盘中保存的二进制代码并未变化,如果文本文件非Unicode编码,记事本会依照windows中设定的系统区域的语言去调用相应的代码页解析二进制代码,同样的’11001110 11010010’在CP936下是’我’,但在韩文的EUC-KR(代码页为949)编码下就变成了’乖’。设置系统区域属性的位置:控制面板->区域和语言->管理->更改系统区域设置,对话框如下:

系统区域设置
系统区域设置

其他中文字符编码

GB2312是一种简体中文的编码,与之同时产生的还有一种针对繁体中文的编码,它是由台湾财团法人信息工业策进会为五大中文套装软件所设计的中文共通内码,所以称为Big5(大五碼)。大五码也有着丰富的历史,感兴趣的可参考维基百科

在GBK之后,大陆又制定了GB18030等编码(标准文件在此处编码字符集在此处),成为取代GBK1.0的正式国家标准,该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。微软也为GB18030定义了代码页CP54936。但是由于GB18030有一部分4字节编码,用于编码从CJK扩展A引入的6582个汉字,而Windows的代码页只支持单字节和双字节编码,所以这个codepage是无法真正使用的。由于实际中GB18030出现较少,在此略去不题。

3.Unicode

Unicode终于姗姗来迟。Unicode联盟创建的目的就是为了统一世界上出现的所有字符。内码与交换码呈现了山雨欲来风满楼之势,字符集与字符编码清晰界限在unicode的世界中呼之欲出。与GB2312、GBK编码方案不同之处在于,unicode自诞生之时就从未认为世界上的字符是静态的、有限的,准确而言,Unicode本身就考虑了字符会不断增长这个事实,那么在自然世界里什么是不断增长的呢——自然数,这个整数可能很大。于是Unicode使用自然数来表示字符,每一个字符与一个无符整数对应。Unicode上按照区域、语言给出了几乎世界所有的字符集(当然字符集也在不断扩充中),有Basic Latin (ASCII),也有CJK Unified Ideographs (Han)(注:CJK 是中文Chinese、Japanese、Korean三国文字的缩写。它能够支持这三种文字)。可莫要认为为Unicode规定对应的整数是一件简单的事情,制定者需要了解字符的变迁历史、不同国别相似字符的异同,实际上这是一件相当耗费精力并且庞大的工程。

UTF-8编码

UTF-8编码是可变长编码,因为Unicode中大部分常见字符都在前面,所以能够节省存储空间。它是一种前缀码,可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容。UTF-8的设计有以下的多字符组串行的特质:单字节字符的最高有效比特永远为0;多字节串行中的首个字符组的几个最高有效比特决定了串行的长度。最高有效位为110的是2字节串行,而1110的是三字节串行,如此类推;多字节串行中其余的字节中的首两个最高有效比特为10。表示如下:

UCS-4编码 UTF-8字节流
U+00000000 – U+0000007F 0xxxxxxx
U+00000080 – U+000007FF 110xxxxx 10xxxxxx
U+00000800 – U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+00010000 – U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U+00200000 – U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U+04000000 – U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

重音文字、希腊字母或西里尔字母等使用2字节来存储,而常用的汉字就要使用3字节。另外,在一UTF-8编码的文件中会前缀上EF,BB,BF,称作是有BOM格式的编码,但是大部分文件是不需要这三个字节的,称作无BOM格式的编码。

UTF-16与UTF-32

Unicode还有一额不可变长的编码方案。UTF-16使用固定长度的2个字节,能够表示的字符个数为65535;UTF-32使用固定长度的4个字节,能够表示的字符个数少于1114111。从Unicode字符集映射到这两种编码方案的过程比较复杂,可参考维基百科了解。映射的方案导致了大尾序(big endian)和小尾序(little endian)两种存储形式。只需明白两种尾序表现,是在编码时连续成对的字节顺序相反。以Macintosh制作或存储的文字使用大尾序格式,以Microsoft或Linux制作或存储的文字使用小尾序格式(顺便说下,windows平台与UNIX/Linux平台在处理键盘回车键输入的符号上也有不同,windows采用回车+换行CR/LF表示下一行, 而UNIX/Linux采用换行符LF表示)。为了弄清楚UTF-16文件的大小尾序,在UTF-16文件的开首,都会放置一个U+FEFF字符作为Byte Order Mark(UTF-16LE以FF FE代表,UTF-16BE以FE FF代表),以显示这个文本文件是以UTF-16编码,其中U+FEFF字符在UNICODE中代表的意义是ZERO WIDTH NO-BREAK SPACE,顾名思义,它是个没有宽度也没有断字的空白。UTF-32编码由于占据的空间比较大,在实际中出现较少。

Unicode小结

可以认为Unicode走的是理想主义路线,各种编码方案走的是现实主义路线,目前已经形成标准的UTF-8,UTF-16,UTF-32三种编码方案都无法对所有的Unicode字符进行编码。Unicode提供了一个页面可以查询中文字符的UTF-8,UTF-16,UTF-32的编码,

http://www.unicode.org/cgi-bin/GetUnihanData.pl

小试牛刀-看看文本文件

windows的记事本程序可以导出4中格式编码的文件,我们补充一个”UTF-8 BOM格式编码“,在文件中输入“[回车]a”,另存为这几种编码,使用二进制查看:

编码类型(记事本中的另存为) 二进制 Size 编码集
ANSI CE D2 0D 0A 61 5 GBK(由windows的区域属性决定)
Unicode FF FE 11 62 0D 00 0A 00 61 00 10 UTF-16 Little Endian
Unicode big endian FE FF 62 11 00 0D 00 0A 00 61 10 UTF-16 Big Endian
UTF-8 E6 88 91 0D 0A 61 6 UTF-8 no BOM
UTF-8 BOM格式编码(补充) EF BB BF E6 88 91 0D 0A 61 9 UTF-8 has BOM

很多信息就一目了然了。可以发现,微软在对概念的界定上相当不厚道。可以认为ANSI编码的文件在交换的时候容易出问题,而其他几种文件编码格式则较稳定。在大多数情况下,现在的程序都能够正确识别文本所使用的编码,一部分是由于文件编码前已经标记了,比如带有BOM的UTF-8编码文件;一部分是由于程序能够统计文本中字符的特征,以判断正确的编码方式,会有判断错误的情况,比如新建一个文本,输入”写”,保存之后,记事本默认会以ANSI编码方式保存,但是在打开的时候,由于文件中的字符过少,操作系统会误判断成UTF-8无BOM编码格式,所以就会出现乱码”д”。另外,当我们复制到本地一个文本文件打开的时候出现乱码时,很有可能这个文件使用了ANSI编码方式,所以我们只需要手动尝试其他地区的编码方案即可,在Notepad++,chrome中修改编码方式的位置如下:

Chrome设置网页的编码方式
Chrome设置网页的编码方式

Notepad++设置文本的编码方式
Notepad++设置文本的编码方式

Notepad++中的【转为…】可以将文件转换成其他形式的编码存储.

4.本文小结

实际上,字符编码的问题还远远未到能够做小结的时候,只不过本文已近4k字,多了不便阅读,故准备告一段落。字符编码整体的框架已经厘清,以后会逐步展开在不同软件中体现出来的字符编码问题。欲知Visual Studio、Mysql、正则表达式、Java、HTML中字符编码的故事,且听后文分解。

参考资料

 

Tagged with: , , , , , , , , , ,

发表评论

邮箱地址不会被公开。 必填项已用*标注

*