Categories: Yurii谈开发

浅谈编码

本文由Yurii原创,转载请注明来源: Life Sailor

本文链接 浅谈编码


《学到不会忘》中我提到,为了写《正则指引》,专门抽了些时间学习Unicode,也因此明白了很多与编码有关的问题,只是最后没有全部写进《正则指引》中,以免离题。不过,这并不妨碍专门用一篇文章来讲解编码问题。

其实所谓编码问题,不外乎若干概念,弄明白了这些概念,编码问题就可以迎刃而解了,所以这里按照概念来展开讲解。

字符和字符集

字符,就是我们日常使用的各种文字,比如中文的,英文的ABC,日文的,都是字符。手写可以用到的字符几乎是无限的,但在计算机中,必须事先约定好字符的范围,也就是穷举出所有“可以使用”的字符。这个范围,就是通常说的“字符集”(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 ACJK 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章。

Yurii

View Comments

  • 余老师又出精品了。最后一段Unicode 属性稍显突兀。要是能先引入问题和应用场景再介绍就更容易理解啦。

    • “精品”谈不上,写着玩玩,给大家当个参考。
      你的意见很好,我已经补充了一些内容:)

  • 之前觉得[\x4e00-\x9fa5]对中文肯定是不全的,后来只看到iteye上的某个帖子有提过这类问题,还列了几个url...今天看了才知道原来wiki上都有说明...

  • 逻辑清楚、生动有趣,读起来赏心悦目。谢谢楼主的好文章!

  • 在读取使用了BOM的文件时,先读取头两个字节,如果是ff fe,就是little endian,如果是ff fe,就是big endian;

    -------

    笔误?big endian应该是fe ff

  • 谢谢分享。btw:能否对于一些零宽的字符简单介绍下,包括产生的原因,适用场景 :)

  • 还有个问题想请教下,假设我有一段100字节的文字,我用某种编码转换成汉字后,再用同样的编码转换为字节,是否还是 100 呢?谢谢

    (假设 100 个字节末尾不是半字的情况)

    • 如果是100字节,更应该叫“文本”或者“数据”,而不是“文字”吧。
      理论上说转换是可逆的,也就是说回去仍然是100字节,但实际情况不见得如此,编码转换的过程比较复杂,遇到无法转换的字符可能中止或者忽略,这样再转换回去就不能保证100字节了。

Recent Posts

德国生活点滴:歧视比你想象的要复杂(续)

在上一篇文章里,我列举了一些种族歧视现象的亲身经历,引发了许多读者的讨论。但是让我略感遗憾的是,许多人大概没有注意文章的标题,没有觉察到关键是“比想象的要复杂”,所以直接给出了一个简单的论断。 我的本意绝不是强化已有的简单粗疏的刻板印象,而是希望让大家知道,种族歧视这回事,有许多的侧面和细节。了解这些侧面和细节,有助于我们形成更立体的认知。 于是就有了下面这些内容,希望能引发大家的思考。 一 种族歧视是一种最简单粗暴的歧视。 许多人都知道,“歧视”的英文是discriminate,准确的意思是“区别对待”。既然要区别对待,就自然首先必须有办法区分。目力所及,似乎没有人愿意“区别对待”与自己完全同样的人,而总是要先找出一点区别来,再实行区别对待。 所以,种族、口音、家庭出身、经济能力等等各种因素,都可以成为“区别”的指标,由此催生出区别对待。在这些因素当中,种族大概是最容易识别的特征——判断口音需要等对方开口,家庭出身、经济能力等等因素就更是要全面接触才可能了解。唯有种族,具体来说,绝大多数时候是相貌和肤色,是可以远远一眼就望见的。 也恰恰是因为这个原因,种族歧视特别容易引起反感。 这些年来,我得到的一条重要的生活经验是,如果你希望指出对方的问题,但又不纯粹是为了激怒对方,那么最好不要归因为一些木已成舟,对方无法改变的因素,否则对方多半会恼羞成怒。 举个例子,你觉得某人的口语表达还可以更好一点,完全可以直接给出具体的建议。但是如果从“经济不发达地区来的人就是口语差”,或者“个子矮的人就是没自信心来表达”,那几乎一定会制造矛盾。因为“口语表达”是可以改进的,加以锻炼将来肯定更好,而“不发达地区来的人”和“个子矮的人”就像烙印一样,是无法摆脱的。这种话说出来,对方哪怕有意愿改进,也会觉得无奈甚至恼怒。 种族歧视也是这样,“种族”同样是一种烙印,是无法摆脱的。所以当对某些人的判断与种族挂钩的时候,他或她必然感到无奈甚至愤怒。况且老话说“人上一百,形形色色;人上一万,千奇百怪”。哪怕是同一个种族的人,也可能在肤色、相貌之外完全找不到相同点。先入为主地用种族去对其他人下判断,无论是从情感反应上,还是从逻辑上,都是站不住脚的。 (more…)

3 days ago

德国生活点滴:歧视比你想象的要复杂

去年初的时候,小朋友冰球俱乐部来了个新教练Robo。Robo来自加拿大,总是一副很健谈很乐观的样子,而且很喜欢放音乐,把整个训练场搞得热情四射。最关键的是,小朋友们好像都很喜欢他,不但许多动作耐心示范,对每个人的指导也相当到位。而且,他的英语很好,人又很喜欢开玩笑,所以我们交谈很多,他总是跟我说:“你家的小朋友超级酷的,不要给他太大压力,只要他自己运动起来足够自在,能够持续练下去,就是最好的。” 没想到的是,到去年9月份,Robo忽然神秘失踪了,没有任何征兆,也没有任何说明,就此人间蒸发了一般。问其他的教练,也是语焉不详。小朋友训练完,偶尔会失落地跟我说“好久没看到Robo了,不知道他哪里去了。” 3月份的时候,一个偶然的机会,我又见到了Robo,虽然当时时间很紧张,只是打了个照面,但我要他留下了联系方式。 当天晚上我问他:哥们,你怎么忽然就不见了,大家都很想你啊。 过会儿我收到他的回复:我也很想念小孩子们,你儿子很酷……我现在没在那个俱乐部了,因为其他几个教练总是或明或暗地针对我,仅仅因为我的肤色,这是我受不了的。 (more…)

3 days ago

在德国, 全远程+共享空间办公,是什么体验?

注:原文发布于2023年1月16日 到1月份为止,我已经体验了几个月的全远程+共享空间办公了。有不少朋友听说之后很有兴趣,问我到底是什么感觉,所以我简单介绍下个人的体验。 背景 2019年末、2020年初开始在全球流行的Covid-19对远程办公来说,绝对是黑天鹅一般的存在。因为疫情导致的社交隔离措施,极大影响了各大公司的正常运转。 所幸,IT类公司受到的影响比较小,只要求员工“面对屏幕编程”,不必亲临现场。所以,许多IT公司也谨小慎微地开展了远程办公的试验。 从我所知道的结果来看,不少美国公司并不特别喜欢远程办公,比如Google,一旦社交隔离措施有所放松,就忙不迭要求员工回到办公室,盖因为公司认为远程办公严重影响合作效率。 与此相反,不少德国公司反倒是逐渐适应了远程办公的节奏,纷纷降低对员工“到办公室上班”的要求,许多公司甚至可以支持百分百的远程办公。 这里要提到的是,德国公司说的“远程办公”往往是货真价实的“远程”,而不是一些人理解的“家和办公室在同一个城市,只是不用去办公室”而已。 因为德国IT行业缺人严重,而且许多德国公司并没有那么“互联网”,而是依托实业开展业务,所以据我所知,目前不少公司非但没有裁员,反而都在大力招人。 (more…)

3 weeks ago

成年人找工作,不值得那么多愁善感

注:本文发布于2023年2月6日 最近硅谷几大公司都在裁员,看了些报道,被裁的员工真是不好过。损失经济来源不说,有些人还面临身份问题,这可真是屋漏偏逢连夜雨。 我也留意到,不少被裁的人会不停追问自己:为什么我会遇到这样的事情?为什么这样的不幸会降临到我头上?…… 实话说,我挺能理解这种态度。这挫折如此巨大,似乎又来得全无预兆,不由得让人对命运、对人生、对世界产生深重的怀疑。尤其是对已经走入社会,取得一定成就(如果非要抠字眼,那就用“进展”吧)的人来说,更是如此。 但是我更想说,如果被裁员了,当务之急是赶紧找到下一份工作,哪怕只是机械地行动。要知道,成年人找工作,容不下那么多愁善感。 我之所以这么说,是有切身经历为基础的。之前我讲过找德国工作的经历。最开始是信心十足的,因为虽然毕业多年,手艺没丢,基础还在,随时打开leetcode,中等难度题目基本都不在话下,不但能解对,解法也基本接近最优。既然网上都说“刷题就能找到工作”,估计自己应该没大问题。 没想到真的找起工作来,仍然充满了意想不到的挫折。如果不相信,我且举几个例子吧。 (more…)

4 weeks ago

我读《园丁与木匠》

虽然早就听说《园丁与木匠》是关于育儿的好书,但一直没开始读。最近终于翻开这本书,才发现属于“拿起就很难放下”的类型,加班加点读完,收获不少。 关于这本书的价值,已经有许多书评讨论过了,所以我想略过微言大义、长篇大论的叙述,谈谈我印象最深,也是最打动我的三点细节。 第一,儿童的学习方式 小孩子觉得拧螺丝很好玩,想自己动手拧一颗螺丝。于是,他打开了工具箱,对着琳琅满目的工具,他不知所措。一会儿摸摸钳子,一会儿试试扳手……这时候,旁边的父母应当怎么办? 在大多数情况下,父母大概会直接告诉孩子,“亲爱的,你应该用螺丝刀,来,我告诉你”。耐心一点的父母,大概会潜心观察一段孩子的举动,再设法“引导”他到正确的工具上来。在父母眼里,孩子当然不可能一开始就找对正确答案,所以做各种尝试也是情有可原。但是另一方面,也不应该“在错误的路径上摸索太久,浪费时间”,应当“迅速识别出正确的答案”。 无论父母有多少耐心,在他们眼里,孩子找到拧螺丝的工具的过程,都是个“不断接近正确答案”的过程。这个过程越短,孩子就越“聪明”,或者说“学习效率”就越高。 (more…)

4 weeks ago

再见,或许就是再也不见

陈皓(Haoel,网名“左耳朵耗子”)上周六因为突发心梗去世了,享年47岁。 我跟他虽然聊过好些次,但只是微信好友,从未见过面。回看微信记录,当年稀松平常的一声“再见”,已经成了“再也不见”。 许多人在缅怀他,许多文章提到他的时候,会用到“骨灰级程序员”、“技术大牛”这样的称呼。但如果仅仅用这两个词描述他,断然难以解释,为什么他的突然去世,会引发互联网上怀念的狂潮。 所以,我更愿意按照自己的经验,把他描绘为“有坦诚追求,兼具趣味、操守、胸怀的技术人”。恰恰是因为这样的人在这个年代太稀少,而这些品质又让众多人赏识和受益,大家才会如此地怀念他。 这个年代,做技术(仅指狭义的IT)的人很多,愿意分享的人也不在少数,其中不少还可以算世俗意义上的“成功者”。 但是,若仔细去看他们分享的内容,总感觉不够真诚。总感觉作者希望往高深了靠,目的也没有那么纯粹。你若提一些小白问题,迎来的往往是“你怎么连这都不知道?”的反问,或者“要谈这个问题,你先去看几本书再说吧”。话是这么说没错,但无数的初学者也往往因此打了退堂鼓。 但是陈皓的分享不同。我已经不止一次地看到有人提起,他分享——更准确说,是“创作”——的内容质量很高,而且总能做到“深入浅出”。哪怕是小白读者,看完也确实能有收获,如果还有兴趣,更可以跟着文末的链接,顺藤摸瓜探究更广阔的世界。 这让我想起我佩服的一位记者说的:记者写文章的最高境界,就是不表达自己的观点,因为记者的观点应当来自于他的素材。只要把这些素材摆出来,读者读完报道,观点就自然形成了。要做到这一点,需要对素材有足够的信心和把握,外加真诚和坦荡。 能做到这一点的记者,着实不多。陈皓虽然不是记者,他写的技术文章却能让读者得到类似的结论——要知道,技术讨论往往是非常容易擦枪走火的——可见他运用素材和逻辑的功力,以及更重要的,他的真诚和坦荡。 (more…)

4 weeks ago