浅谈编码
在《学到不会忘》中我提到,为了写《正则指引》,专门抽了些时间学习Unicode,也因此明白了很多与编码有关的问题,只是最后没有全部写进《正则指引》中,以免离题。不过,这并不妨碍专门用一篇文章来讲解编码问题。
其实所谓编码问题,不外乎若干概念,弄明白了这些概念,编码问题就可以迎刃而解了,所以这里按照概念来展开讲解。
字符和字符集
字符,就是我们日常使用的各种文字,比如中文的你、我、他,英文的A、B、C,日文的に、ほ、ん、ご,都是字符。手写可以用到的字符几乎是无限的,但在计算机中,必须事先约定好字符的范围,也就是穷举出所有“可以使用”的字符。这个范围,就是通常说的“字符集”(Character Set)。
ISO8859-1是开发中常见的字符集(MySQL默认就采用这种字符集),它支持的语言有英语、德语、法语等,也即包含了英语、德语、法语中的字符。GBK是另一种常见的字符集,它源自GB2312字符集,GB表示“国标”,GB2312即是国家标准,它的另一个名字是CP936(Code Page 936),以前在Linux下播放MP3,如果发现ID3标签乱码,设定为CP936就可以解决。因为制定较早,GB2312只包含6763个汉字,并不足够覆盖日常的使用,所以诞生了GBK,其中的K表示“扩展”。有意思的是,GBK是微软制定的字符集,而不是“国标”,只是曾由国家技术监督局标准化司、电子工业部科技与质量监督司公布为“技术规范指导性文件”。除此以外,港台地区以前使用Big5字符集,曾经在Dos下玩过港台游戏的朋友应该还记得“大五码”这个名字。
与字符相关的另一个概念是字形(glyph),它是字符显示出来的样子,同一个字符可以有好几种写法,每种写法其实对应一种字形。下面举例列出了“高”字的几种字形(资料来自Wiki)。
编码与码值
在计算机内部,所有的数据都是编码保存的,字符也不例外。因此,每一种字符集不但约定了可以使用的文字的范围,而且为每一个字符确定了唯一的代码,称为码值(Code Point,也叫“代码点”)。
在ISO8859-1字符集中,A的码值是41(十六进制),=的码值是3d(十六进制);在GBK字符集中,发的码值是b7 a1(十六进制),罚的码制是b7 a2(十六进制)。因为单个字节只能表示最多256个字符,而中文字符超过256个,所以GBK编码选用2个字节表示单个字符,相应的,其码值也是4位十六进制数值。
我们说的“GBK编码”、“ISO8859-1编码”,其实既指其对应的字符集,又指其对应的码值规定。通常,两者是一体的,但是在Unicode编码中的情况,却不是这样。
Unicode
随着计算机和互联网的发展,各自为战的字符集很快就遇到了问题:如果我需要在一篇文章中同时使用中文和日文字符,该怎么办呢?设定为日文编码(常用的为Shift-JIS、EUC-JP)则不能涵盖中文字符,设定为中文编码则必须放弃日文字符,所以需要一种统一的、可以覆盖各种语言的字符集,于是Unicode字符集应运而生了。
Unicode的最初想法是用2个字节(16位,65536个码值)来表示世界上所有的语言,所以它的字符集称为UCS-2(2 byte Universal Character Set)。用2个字节表示一个字符,就会带来字节序问题:在传输和存储时,到底是先传输高位字节(big endian),还是低位字节(little endian)呢?这个问题Unicode也没有确切的答案,所以设定了BOM(byte order mark,字节序标识)来解决。BOM对应的码值是fe ff,无论fe ff,还是ff fe,在Unicode中都没有实际的意义,所以不会造成干扰。在读取使用了BOM的文件时,先读取头两个字节,如果是ff fe,就是little endian,如果是fe ff,就是big endian;如果文件的开头两个字节既不是fe ff,也不是ff fe,默认采用big endian。如果你用Windows的记事本创建Unicode编码的文件,文件头就会包含little endian的BOM,其它一些文本编辑工具则不会。如果用程序解析包含BOM的XML文件,可能遇到非法字符的错误,必须先截去开头的BOM信息。
最早制定Unicode规范时,大家乐观认为觉得65536个字符就可以覆盖地球上所有语言中的字符了,这种今天看来草率的乐观,导致了不少后果。
第一,因为东亚文字(主要是中、日、韩三种语言的文字)字符非常多,为了节省码值,就将三种语言中字形类似的字符映射到同一码值,这种做法称为UniHan(统一汉字,在Unicode规范中,也称为“东亚文字(East Asian)”),比如中文(包括大陆、香港、台湾)和韩文及日文的“骨”、“直”等字,虽然写出来的字形(glyph)有微小差别,但码值是相同的。这样的好处是节省了码值,而且某些跨语言搜索可以直接进行,比如搜索日文关于“直角”的资料,直接输入“直角”即可。这样的坏处是,不能依靠码值判断到底属于中日韩语言中的哪一种(三种语言中的常用字符大都属于CJK_Unified_Ideography这个书写系统),而且,对于码值相同但字形不同的字符,到底选择哪个字形来显示,还应当参考locale设定(使用过Linux的人大都会记得zh-CN.UTF-8这样的locale设定,它可以影响到”直“、”骨“之类的字形选择)。
网络上有不少资料说,匹配中文字符的正则表达式是[\\\\x4e00-\\\\x9fa5],也有资料说是[\\\\x4e00-\\\\x9fff]。从原理上看,它们都是用字符组表示某个范围,起始码值都是4e 00,结束码值却有不同,这是为什么呢?仔细阅读UniHan规范可知,其实它们的原理都是使用CJK_Unified_Ideography书写系统(Script,这是一种Unicode属性,下面会详细讲到)中的文字,在1992年提交给IRG(International Rapporteur Group)的字符只排到9f a5。在这之后,制定更新版本的Unicode规范时都进行了扩展,新增了字符。不过从根本上说,4e00-9fff是预留给东亚文字的码值范围,所以使用[\\\\x4e00-\\\\x9fff]是更好的选择。具体信息可以参考 UniHan规范。
如果要“完整地”匹配所有的中文(东亚文字),还必须考虑Unicode各版本中增补的CJK统一表意字符,从CJK Unified Ideographs Extension A、CJK Unified Ideographs Extension B一直到最新的CJK Unified Ideographs Extension E,具体细节可以参考Wiki上的说明。
第二,用2个字节表示单个字符并不合适。对ASCII字符来说,用单个字节就可以表示,2个字节造成了大量的浪费;另一方面,65536个字符并不够表示世界上的所有字符,所以Unicode规范进行了扩编,截止本文写作时止,最新的Unicode 6.1.0规范包含110116个字符,所需的字节当然超过2个(16位)。针对这种问题,Unicode字符提供了不同的字符编码方式(Character Encoding Scheme),可以这么理解:字符的码值是一回事,在存储和传输时,具体落实为几个字节,如何表示,又是另一回事,码值的具体表示形式,就由字符编码方式来规定。常见的Unicode字符编码方式有:UTF-8,UTF-16等,其中的UTF是UCS Transformation Format的缩写,明确表示它是一种传输格式,所以我们可以说“Unicode字符集”,也可以说“Unicode编码”,还可以说“UTF-8编码”、“UTF-16编码”,但不能说“UTF-8字符集”、“UTF-16字符集”。
UTF-8是一种变长编码,第一个字节的最高位如果是0,则表示这个字符用单个字节表示,否则,从这一位开始向后数,有多少个连续的1,这个字符就用多少个字节表示。于是,英文字符只需要1个字节就可以表示,而中文字符一般需要3个字节来表示。比如发字,其码值为53 d1,但UTF-8文字编码方式下表示为e5 8f 91。
如果我们拿到一段文本,不知道它到底是GBK编码还是UTF-8编码,就可以依据UTF-8编码的这个特征进行判断。不过我之前试验过一个取巧(但不那么保险)的办法:因为中文里的“的”字出现非常频繁,而当时要判断的文本一般都不短,所以直接查找文本中是否出现了GBK的“的”字或UTF-8的“的”字,也可以判断出来。
UTF-16则是一种定长编码,每个字符都采用2个字节,16位来表示。发的UTF-16编码方式下表示为53 d1。相比UTF-8,它的字符长度固定,本来是一种好处。但是因为Unicode字符集已经超过了65536个字符,所以UTF-16已经没有什么优势了,对超过16位的Unicode字符,UTF-16必须补充另外两个字节来表示,多出来的这两个字节称为代理对(Surrogate Pair)。
Java在诞生时就有”先见之明“地选择了UTF-16作为内部文字编码方式,每个字符在JVM内部都使用16位来表示,所以Java中的char是long类型,也就是16位整数。但是随着Unicode字符集中的字符超过65536个,Java原来的字符串处理API就无能为力了。为弥补这个问题,Java 5.0另外提供了CodePoint相应的方法,比如计算CodePoint个数的codePointCount(),取代之前的length(),以及获取某个CodePoint的codePointAt()方法,取代之前的charAt()方法。另外,在进行跨语言通讯(比如调用Web Service)时,往往必须显式指定输入输出的文字编码方式为UTF-16,否则有可能遭遇乱码。
既然Unicode包含了几乎所有的字符,这些字符的分类管理当然也更复杂。比如,针对某个字符,必须能知道它属于哪种语言;再比如,还需要知道某个字符到底是空白字符,还是标点字符,还是文字字符——ASCII编码中的字符可以分为控制字符、字母字符、标点字符等等,各个分类所包含字符的码值是位于连续区间的,所以直接指定码值范围即可(参加下面的ASCII码表),但是在Unicode中,不同语言的标点字符,其码值必然不是连续的,必须要有办法表示这些分类。要满足这些需求,就必须依靠Unicode属性。
Unicode属性
Unicode不但包含了更多的字符,多种编码方式,还提供了非常有用的功能,即Unicode字符集中的每个字符,都具有好几种属性,它们从不同的方面描述这个字符的某个特征。最常见的属性有:Unicode Property、Unicode Block、Unicode Script,以下分别简要介绍。
Unicode Property的记法类似\\\\p{L}、\\\\p{P},按照字符的功能分类Unicode字符,而每个Unicode字符只能属于一个Unicode Property。 不妨这么理解Unicode Property:它并不按照字符所属的语言来划分Unicode字符,而是按照字符的功能来划分,比如\\\\p{Z}表示任意的空白字符或不可见的分隔符;\\\\p{P}表示任何标点字符,等等。遇到中英文混排、全角半角同时出现的情况,我们就可以用\\\\p{Z}匹配所有的空白字符(不关心到底是全角空格还是半角空格),用\\\\p{P}匹配所有的标点字符(而不用关心逗号到底是中文逗号还是英文逗号),不用费心细节。
Unicode Block则不同于Unicode Property,它按照编码区间划分Unicode字符,每个Unicode Block中的字符编码都是落在同一个连续区间的。因为Unicode编码表中,某种语言的字符通常是落在同一区间的,所以它也可以粗略表示某类语言的字符,比如\\\\p{InHebrew}表示希伯莱语字符,\\\\p{InCJK_Unified_Ideographs}表示兼容CJK(中文、日文、韩文)统一表意字符。如果你细心观察,会发现Unicod Block的名字虽然类似某种语言的名字,但都有“In”(Java风格)或者“Is”(.NET风格)前缀,这表明它其实对应的还是“落在某个区间的Unicode字符”。
Unicode Script按照字符所属的书写系统来划分Unicode字符,比如\\\\p{Greek}表示希腊语字符,\\\\p{Han}表示汉语(中文字符)。它的写法类似Unicode Block,只是名字的开头没有“Is”或者“In”。
以上三种属性互相独立,之间没有层叠关系,可以用下面这幅图简要说明。
在处理字符串时,如果可以用到这几种属性,就会非常方便。如今流行的语言中,大都可以通过内建的正则表达式来获得这几种属性,并进行相应的处理。但是,语言对Unicode属性的支持并没有硬性的标准,所以造成不同语言的支持程度各有不同。一般地说,支持Unicode Property的语言有.NET、Java、PHP、Ruby(限1.9以上版本);支持Unicode Block的语言有.NET、Java;支持Unicode Script的语言有PHP和Ruby(限1.9以上版本)。具体的使用方法,可以参考Regex Tutorial的专题页面,也可以阅读《正则指引》第7章。
建议继续学习:
- 字符编码和中文乱码小叙 (阅读:5574)
- 中文编码杂谈 (阅读:4915)
- Hadoop的map/reduce作业输入非UTF-8编码数据的处理原理 (阅读:4577)
- PHP编码规范 (阅读:4324)
- base64_encode 和 urlencode (阅读:4056)
- UTF-8编码中BOM的检测与删除 (阅读:4005)
- Linux screen窗口中文乱码问题 (阅读:3923)
- Unicode与字符汉字相互转换 (阅读:3852)
- python-django的中文编码总结 (阅读:3759)
- JAVASCRIPT完美实现UTF8页面提交数据到GB2312 (阅读:3564)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:Yurii 来源: 乱象,印迹
- 标签: 编码
- 发布时间:2012-07-19 13:29:30
- [68] 如何拿下简短的域名
- [68] Go Reflect 性能
- [64] Oracle MTS模式下 进程地址与会话信
- [61] 图书馆的世界纪录
- [60] IOS安全–浅谈关于IOS加固的几种方法
- [60] 【社会化设计】自我(self)部分――欢迎区
- [58] android 开发入门
- [53] 视觉调整-设计师 vs. 逻辑
- [48] 读书笔记-壹百度:百度十年千倍的29条法则
- [47] 界面设计速成