Category: Yurii谈开发

软件开发的人文关怀

几年前,我从温伯格的《技术领导之路》中学到一点:技术人员往往更喜欢和机器打交道,因为他们“认为”自己更适合和机器打交道;但是,优秀的技术人员必须(也必然)具备好的沟通能力。所以,温伯格鼓励各位技术人员多加练习和其他人打交道的能力。温伯格的这个观点我是非常赞成的,好的技术人员一定需要“勇敢”面对他人,不能被“自实现的预言”局限在机器的世界里。

不过我也发现,“技术人员(当然我主要说的是软件开发人员)不适合跟人打交道”的负面影响不止于此,它还成了一种刻板印象(stereotype),进而影响到开发团队之外的人。这个问题其实很严重,它会导致其他人和开发人员沟通时自觉或者不自觉地切换到“和机器沟通”的模式上来,比如单纯依赖邮件而避免见面沟通,比如“你只管这么做出来就好了,别管我用来干什么”。以面向机器的模式来与人沟通,结果往往是完整的项目(而不是狭义的“软件项目”)割裂开来,皆不欢喜。

Continue reading 软件开发的人文关怀

事关“工程师思维”(2)

2011年前我写过《事关“工程师思维”》,在那篇文章里所谈的“工程师思维”,指的是“一种可以把想法实现出来,一步步的变成现实的思维和实践训练”。两年过去了,工作和生活中的许多经历让我认识到,“工程师思维”不仅仅是一种思维和实践的训练,更是从多个方面体现出来的习惯,以下简单说说我的两点理解。

工程师思维的体现之一,是对于工具的执着。工程师不是赤膊上阵的莽夫,要完成自己的任务,必然离不开工具的支持,所以,优秀的工程师,必然从思想上重视工具。
我刚刚接触Linux下的开发时,是在一家当时国内有名的互联网公司。当时自己学了点vi,开始编写php程序。有天,一位同事从旁边走过,他忽然说:你的vim怎么没有颜色呢?然后不由分说,抢过我的键盘噼啪一顿敲,瞬间编辑器就有了格式和颜色(其实就是加上了syntax语法文件)。当时我只是觉得新奇好玩,过了几天,随着程序不断变复杂,才发现开发效率相比之前已经大大提高;后来让我惊奇的是,原来那天帮我弄出颜色来的同事,正是公司的首席科学家;更让我惊奇的是,他负责的项目,基本是不用加班的,而且交付质量都相当出色。
这件事情我印象一直很深,一方面是因为身为首席科学家,愿意“屈尊”为新入门的同事配置工具(后来我才意识到,他所负责的项目之所以都能高质量的准时交付,离不开一个又一个的细节,对工具的执着就是其中之一);另一方面,我也发现很多“工程师”并不那么在乎工具,更愿意坚守一些原始粗糙的工具(“粗糙”与“简单”不同,精心配置的vim当然不算粗糙的工具),你建议他们去换用新工具时,他们往往说“没事,我习惯了,再说也查不了多少”为理由拒绝(我认为这更多是思维上的懒惰)。更不幸的是,我见过的有这样习惯的人,基本都算不上优秀的工程师。两厢对比,我越发深刻地认为,“工程师思维”的一个重要方面就是对工具的执着,甚至既要对自己的工具精益求精,更容不下团队里其他人使用粗糙的工具。

工程师思维的体现之二,是对于流程的执着。工程不同于艺术,不需要天马行空的自如,工程的完成离不开严谨周密的步骤,只有执著于流程的工程师,才能够各种工程中贯彻严谨周密的步骤,从而减少变数,保证工程顺利完成。
许多开发人员都遇到过这样的情况:经常需要解决各种重复或者类似的问题,但是每次都需要花不少力气翻找之前的资料。我也曾遇到过这样的问题,直到有一天,发现某位同事有一个巨大的“脚本库”:无论我问他什么问题,他都可以迅速找出一个脚本,我只需要修改几个参数,就一定可以解决这个问题。
回头去想,这些脚本描述的就是解决某类问题的流程,它标明了具体的操作和步骤,保证任何人都可以照章解决某类问题,这种看似“与人无关、缺乏灵气”的方案,其实正是“工程思维”的体现,最大限度地减少了工程中的不可控因素。于是,我也逐渐积累自己的流程文档,更重要的是,在解决一个个问题的“个案”中,尝试从“流程”的角度观察和思考:这个问题是怎么解决的?前后分为几步?哪些步骤是和其它问题相通的?时间久了,既积累丰富了自己的流程文档,又强化了自己的思考能力。如果因为某些任务看似简单,就不去思考背后的流程,想凭“巧劲”手动解决,遇到真正复杂的问题,就没有化繁为简、系统解决的能力,如果问题重复出现,每次仍然需要手动处理,速度只能停在某个很低的水平。这样的表现,当然和“工程师”不挨边了。

如今流行的某种说法认为,中国人的理工科培训,造就了许多人的“工程师思维”。听起来,“工程师思维”似乎是某种不好的习惯,只知做事,不会思考;但在我看来,事实全然不是这样:“工程”是一门讲究实践、思考、经验总结的学问,而我国现行体制里教育出来的人,往往只擅长照章执行,根本谈不上“工程师”;无论是谁,想要具备真正的工程师思维,都离不开持续、专门的学习、思考和训练。

享受职业素养

按:前段时间与章显洲共同参与了图灵出版公司Clean Coder中文版的翻译,这是我的译者序。

我在招聘时常问的一个问题是:在你过去的工作中,遭遇过哪些印象深刻的困难,最后是怎么解决的?依我的经验,简历写得再漂亮的人,如果这个问题答不好,大都可以直接忽略。为什么会有这种结论?因为我们需要招聘的不是“经历丰富”的人,而是“有职业素养的人”。你遇到的问题可能很容易也可能很难,但我看重的并不是问题的难度,而是解决问题的方式、步骤,以及反思的深度。拿恢复误删数据来说,这可能算非常简单的任务,我更感兴趣的是怎样分析问题,找了怎样的资料,采取了怎样的步骤,此后做了哪些措施来避免这种错误再次出现。在我看来,相比问题本身的难度,解决问题的方式和步骤,以及反思的深度,都体现出一个人的职业素养。

是的,上面我两次提到了“职业素养“。相比起“专业主义”、“职业化”等说法,我更喜欢用它来翻译Professionalism,因为素养强调的并不是天赋的神秘,也不是技艺的高深,而是持续积淀的结晶:一方面,它体现了能力和素质,另一方面,它又强调了持续的积累和养成。甚为职业开发人员,基本技能不够熟练,当然谈不上职业素养;但是能迅速地编写代码,却不关心代码背后的意义,不能迅速判断、解决程序运行中的各种问题,自信满满地为自己交付的程序承担责任,同样是与职业素养绝缘的——许多所谓的”高手“,其实正是缺乏职业素养的典型。

当然,这只是我对于“职业素养”的理解。由个体经验总结的“职业素养”,多有一鳞半爪的嫌疑,所以即便你觉得上面说的有道理,难免感觉只见树木,不见森林。其实真正的”职业素养“绝不限于上述几方面,而是要广阔得多,深刻得多。要想一窥技术人员“职业素养”的全貌,已经有很多现成的资料可以参考,Bob大叔的Clean Coder就是其中的佼佼者。

作为一本技术类书籍,Clean Coder中有相当的内容,是介绍纯技艺的方面,比如测试驱动开发等等,自认已经算“职业开发人员”的人,大概对此并不感冒(不过,我仍然建议你认真看看)。但其它的内容,绝对值得你感冒,比如:什么情况下应该对业务部门说“是”,说“是”意味着什么。如果你没有想过这些问题,或者没有明确的答案,不妨看看Bob大叔是怎么说的:

(说“是”时)你对自己将会做某件事做了清晰的事实陈述,而且还明确说明了完成期限(do with a clean end time)。那不是指别人,而是说的自己。你谈的是自己会去(will)做的一项行动(action),而且,你不是“可能(possibly)”去做,或是“可能做到(might get to it)”,而是“会(will)”做到。

就我所见,技术人员往往太容易说”是“,往往在没有明确目标和期限的情况下,就草率给出了确认的答复,而且并不将其视为自己的一种承诺。屡见不鲜的项目延期,有相当原因就是在这种不负责任的情况下说“是”所致。但是我们想想,似乎没有哪一个正经行业,会把不能完成任务的人视为“有职业素养的人”,软件行业也不能例外。

如果你觉得自己已经足够负责,懂得“是”背后所蕴含的意义和责任,也不过如此,我们不妨更进一步,看看关于说“否”。在第2章,Bob大叔介绍了两个项目搞砸的经过,他并没有像常见的所谓专家那样故作聪明地指出实施过程中出现了哪些问题,导致了失败,而是一针见血地指出:这两个项目之所以会搞砸,因为开发人员没有坚决抵制各种不专业的需求(比如一些无关紧要但成本巨大的需求),抵制各种不专业的行为(比如为了赶工期而降低对程序质量的要求),最终只好喝下自己酿出的苦酒。对此,Bob大叔总结道:

有时候,获取正确决策的唯一途径,便是勇敢无畏地说出“不”字……我们要明白,委屈专业原则以求全,并非问题的解决之道。舍弃这些原则,只会制造出更多的麻烦……

对我来说,这真是振聋发聩的号角。而且,这种思维,这种视角,其实是许多技术人员所不屑或者不愿面对的——最初我也这么认为,但尝试在工作中主动说了几次“不”之后,我逐渐发现:花三分的力气去抵制无理的需求,可以节省十分甚至二十分的开发时间;相反,自欺欺人地说服自己凑合接受了无理需求,往往会非常被动乃至无法脱身,到最后,项目就落得著名的IBM
OS/360操作系统的下场,越挣扎,巨兽就越深地陷入泥潭。

要学习这样的道理,当然也可以参加培训班,听取授课或者阅读讲义,但那未免太显正经而缺乏亲和力。Bob大叔的特别之处在于,他总是可以通过浅显易懂的故事,清晰而敏锐地揭示了问题的核心所在。其中许多故事正是他自己亲身经历的,阅读过程中常会会心一笑,因为遇到了开发人员都懂的妙趣,比如费尽全力也是徒劳,无法让其他人理解“编辑程序的程序”;笑过之后,又会认识到许多道理——无法让其他人理解“编辑程序的程序”并不是真正的原因,真正的原因是:“客户……对功能的设想,其实经不起电脑前真刀真枪的考验……问题在于,东西画在纸上与真正做出来,是不一样的。业务方看到真正的运行情况时就会意识到,自己想要的根本不是这样。一看到已经满足的需求,关于到底要什么,他们就会冒出更好的想法——通常并不是他们当时看到的样子……真正的解决办法,是约定共同认可的验收测试标准,并在开发过程中保持沟通”。至少就我的经验,这一点是说的非常之对的。我曾经尝试在与业务部门确定目标原型之后,要求对方指派一个负责人,在IT部坐班,负责协商、跟进整个开发流程,确认每一点修改,这样既保证最终结果符合业务部门的需求,又提高了开发人员的工作效率,综合来看,成效相当可观。

类似的例子还有很多,在阅读这本书时,我经常会惋惜:如果早一点读到这本书,或许我之前就不会犯这样那样的错误,就能更早更好地积累自己的职业素养,另一方面,能有妙趣横生的书讲述看似枯燥的“职业素养”,对读者来说,又是一种幸运。德国作家托玛斯·曼曾经津津乐道于“斜躺在沙发上整天阅读叔本华”的美妙感觉,那是因为叔本华的文笔优美、流畅,可以把哲学变为惬意的享受。作为同时读过叔本华和Bob大叔的人,我想说,斜躺在沙发上整天阅读Clean Coder,认识和了解开发人员的职业素养,同样是相当惬意的享受。

正则学习问答

最近有幸在开源中国51CTO两家网站作为嘉宾参与了于正则表达式的专题问答。在问答过程中,我收集到学习正则表达式过程中的某些普遍问题,在这里专门花一点篇幅来回答

正则表达式是难学的,这不存在疑义。但是我认为,难点也只在语法方面。正则表达式已经有年头了,它(的语法)诞生于上世纪七十年代。那是个怎样的情景?举个简单的例子吧,Unix下的usrdev等名字,就是那时留传下来的,现在已经有很多人诟病了,usr不是user,dev不是device,难学,也难记。经过这些年的飞速发展,当年的很多问题已经被包装得美轮美奂,如今的用户可能更习惯直接点击“用户目录”、“驱动器”之类的图标,再也不用为那些不规则的简短名字发愁。但是不幸的是,一直以来正则表达式的语法却没有太多的变化,甚至后续增加的功能,也沿袭了之前的语法风格,在编程语言日渐人性化的今天,它自然显得非常难懂了。今天的开发人员可能更习惯Regex.CharRange(‘a’, ‘z’)这样的写法,而不习惯[a-z];遇到(?![a-z])这样的结构就更是抓瞎,除非转为Regex.CheckRight(Regex.CharRange(‘a’, ‘z’))的写法。

不过,换一个角度来看,两者其实是一回事,只是表现形式不同,一个类似要诀,一个类似大白话。如果我们能在头脑里构建出从要诀到大白话的转换,正则表达式就简单了许多,甚至可以说就是模块的拼接。比如支付宝的流水号为18或26位数字,用正则表达式匹配,就是^([0-9]{18}|[0-9]{26})$,或者^[0-9]{18}([0-9]{8})?$。其中的逻辑很简单:^用来锁定开头,$用来锁定结尾,[0-9]匹配数字字符,([0-9]{18}|[0-9]{26})表示两个并列的选项,即数字字符串长度为18位或26位,而[0-9]{18}([0-9]{8})?表示至少需要出现18位的数字字符串,在这之后可能还有一个8位的数字字符串(所以总长度是26位)。一般的正则表达式应用,就是这么简单。

如果你觉得上面说的没错,那么学习正则表达式的难题就只剩下了选择得当的方法。我们学习编程语言时,都强调不能只看书,要动手写程序,甚至最好的办法是把书上的例子亲自输入运行一遍,这样才算真正学会了。但在许多人眼里,正则表达式或许算不上编程语言,所以学习是点到即止,甚至是满足于从网络上抄一些现成的表达式。所以,常见的问题之一是“有没有什么学习的捷径”,很不幸,答案是没有——既然拷贝他人的代码不能学会编程,抄阅现成的表达式、随便翻几篇文档,当然也学不会正则。不过也有幸运的消息,真正学会正则表达式并不需要花太长的时间。

以我的经验,学习正则表达式,真正要做的是深入理解常用功能:字符组、多选分支、匹配模式、环视。可以说,弄明白了这几点,80%的正则问题都可以解决。但是要弄明白这几点,就需要专门的学习:字符组是解决什么问题的,它是怎么使用的?多选分支是解决什么问题的,它是怎么使用的?应当抽一些时间专门学习、思考;这些都弄明白了,再研究解决复杂问题的表达式是怎么构成的。如果你可以每天抽1-2小时专门学习,两周内就会有明显收效,一个月几乎就可以修炼到相当水平。而且,以我的经验,在学习新的编程语言时,不但要把书上的例子都亲自输入运行一遍,更要自己动手去改一改示例代码,看看会出现什么现象,再想想为什么会这样。如果你在学习正则表达式时也做到这一点,必然能够事半功倍。

如果你真正理解了这些常用功能,对它们的价值和使用有清晰的概念,那么另一个麻烦也就迎刃而解了——不同语言的正则表达式不同,如何解决?虽然不同语言中的正则表达式规定各有不同,但背后的思想是统一的,不同的只是表现形式,或者说概念的落地方式。好处在于,编程语言的文档不会详细讲解什么是字符组,什么是多选分支,但会详细告诉你字符组在本语言中是如何表示的,多选分支又是如何表示的(不信你可以在这些文档中搜索character class或者alternation)。所以如果你的脑子足够清楚,即便不确定最终的表达式如何写,也只需要查文档就可以解决。举个例子,匹配空白字符的字符组\s,在Java字符串中要写作\\s,因为\s并不是Java字符串中的一个合法转义序列,所以之前还必须有\来转义\;在PHP中可以直接写作\s,因为PHP处理字符串时会把无法识别的转义序列原封不动地保存下去;在Unix下的某些工具中,必须写作[[:space:]],这是Perl风格的\s在POSIX规范中的表示法。看起来比较麻烦,也仅此而已,因为我们知道,这里需要用到的,就是“匹配空白字符的字符组”。

以上写了这么多,可能有人会说:正则表达式这东西,不登大雅之堂,没必要花那么多精力。或许正是这种观点,形成了“不认真学习正则表达式”思想根源。幸运的是,这个问题其实很好想明白,因为很多事情都是这个道理。比如写文章,我们不要求人人都是作家,但是人人都有可能在需要的时候写出几篇拿得出手的正经文章,“不是作家”并不是“需要时写不出正经文章”的理由。为了能在需要的时候写出正经文章,就必须专门抽出时间来学习和练习写作。正则表达式的学习,其实也是这个道理。

这种说法可以说服一些人,但还有一些人是说服不了的。同时据我观察,那些不能被说服的人,似乎也没有花太多精力在其它“正事”上,反而会不时为正则表达式所困扰。与此相反的是,真正有职业素质的程序员,就像the Productive Programmer中说的那样,会愿意花2小时写出一个正则表达式,为以后节省无穷无尽的时间。当然,以上说的这一切的前提,都是能端正学习正则表达式,或者说学习有价值技能的的态度。做软件的人大都读过布鲁克斯的名文《没有银弹》,所以这里不妨借用他的话说,正则表达式的学习,也不存在银弹。

浅谈编码

《学到不会忘》中我提到,为了写《正则指引》,专门抽了些时间学习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章。