October 2010


今天我的同事老赵 @jeffz_cn 问我,有没有办法用正则表达式匹配“不包含某个字符串”的文本,正好,我在写作的《正则表达式傻瓜书》中也提到了这类问题,就把这一节放出来,给大家参考,也希望大家多提建议(尤其是配图方面)。

正则表达式的与或非

我们都知道,写正则表达式有点像搭积木,复杂的功能总可以拆分开来,由不同的元素(也就是子表达式)对应,再用合适的关系将它们组合起来,就可以完成。在这一节,我们讲解常见的与或非关系的表达。

“与”是最简单的关系,它表示若干个元素必须同时相继出现,比如匹配单词cat,其实就是要求字符c、字符a和字符t必须同时连续出现。

正则表达式表达“与”关系非常简单,直接连续写出相继出现的元素就可以,我们可以想象,在各个元素之间,存在看不见的连接操作符·,比如上面匹配单词cat的正则表达式,就是『cat』,我们可以将它想象为『c·a·t』。

“与”关系也不限于字符之间,任何子表达式都可以用它来连接,如果我们把上面单词中的a替换为字符组『[au]』,表达式就变为『c[au]t』,你可以想象为『c·[au]·t』。

“或”是正则表达式灵活性的重要体现,我们可以规定某个位置的文本的“多种可能”,比如要匹配cat或是cut,在正则表达式看来,就是“字符c,然后是a或u,然后是t”。

如果“或”的多种可能都是单个字符(一般要求ASCII字符,中文字符等多字节字符的情况,可以参考本书专门论述的章节,此处仅以ASCII字符为例),就可以用字符组来表达“或”的关系,比如上面的cat或者cut的情况,正则表达式写做『c[au]t』,其原理如下:

更复杂的情况是“或”的多种可能,并非都是单个字符,有些可能是多个字符。比如,我们可以看一个更复杂的例子,不仅要匹配cut,还要匹配c开头、t结尾的单词chart、conduct和court。也就是说,在开头的c,结尾的t之间“可能”出现的是:uharonducour。

遇到这种情况,就不应使用字符组,而应当使用多选分支『(…|…)』,将各个“可能选项”列在多选分支中。于是,正则表达式变为『c(u|har|onduc|our)t』,其原理如下:

关于多选分支,还有两点要补充:

多选分支也可用于“每个选择都是单个字符”的情况,比如『c[au]t』写成『c(a|u)t』是没错的,但字符组的效率要远高于多选分支,所以,在这种情况下,推荐使用字符组『c[au]t』;

默认的多选分支『(…|…)』使用的括号是会捕获文本的,也就是说,括号内的表达式真正匹配成功的文本会记录下来,匹配完成之后可以提取出来,具体到上面的例子,就是我们有办法在匹配完成后“提取”出u或har或onduc或our。但许多时候,我们需要的只是整个表达式的匹配,而不关心“匹配时到底选择的哪种可能情况”,在这种情况下,我们稍加修改,使用“不捕获文本的括号”,可以提高效率。不捕获文本的写法也很简单,只是在开扩号之后加上字符『?:』,也就是『(?:…|…)』,具体到上面的例子,就应该写成『c(?:u|har|onduc|our)t』。这样做虽然繁琐点,但效率有保障,阅读起来也不困难,我推荐养成这种习惯,只要用到了括号,就想想是否真的要捕获括号内表达式匹配的文本,如果不需要,就是用不捕获文本的括号。

“非”看起来简单,其实是最复杂的,以下分几种情况讨论。

首先讨论针对字符的“非”:不容许出现某个或某几个字符。这是最简单的情况,直接用排除型字符组就可以对付,仍然用上面的例子,如果要匹配的单词是c开头、t结尾,中间有一个字符,但不能是u(也就是说,整个单词不能是cut),直接用『c[^u]t』就可以了,若中间的字符不能是a或u(也就是说,整个单词不能是cat或cut),则表达式改为『c[^au]t』。

如果你认真读过关于排除型字符组的章节,肯定会知道,这个表达式能匹配的只是cot之类的单词,因为中间的排除型字符组『[^au]』必须匹配一个字符。可是,如果我们还想要匹配chart、conduct和court,怎么办?最简单的想法是去掉排除型字符组的长度限制,改成『c[^au]+t』——不幸的是,这样行不通,因为这个表达式的意思是:c和t之间,是由多于一个“除a或u之外的字符“构成的,而chart、conduct和court,都包含a或u。

我们回头仔细看看这个“非”的逻辑,我们发现,其实我们要否定的是“单个出现的a或u”,而不仅仅是“出现的a或u”,所以才出现这样的问题,要解决这个问题,就应当把意思准确表达出来,变成“在结尾的t之前,不容许只出现一个a或u”。想到这一步,我们就可以用否定顺序环视『(?!…)』来解决了,它表示“在这个位置向右,不容许出现子表达式能够匹配的文本,我们把子表达式规定为『[au]t\b』(最后的『\b』很重要,它出现在t之后,保证t是单词的结尾子母)。

有了这点限制,匹配a和t之间文本的表达式就随意很多了,我们可以用匹配单词字符的简记法『\w』表示,于是整个表达式就变成了『c(?![au]t\b)\w+t』。请注意,这里出现的并不是排除型字符组『[^au]』,而是普通的字符组『[au]』,因为否定顺序环视『(?!…)』本身已经表示了“否定”的功能。

如果我们再进一步,“整个匹配文本中都不能出现字符串cat”,要怎么办呢?许多人的思路就是借鉴处理“或”关系的思路:既然字符组对应单个字符的情况,多选分支对应多个字符的情况,那么在否定时也是这样。可惜,正则表达式并没有提供与多选分支对应的“否定”结构,那么,应该怎么办呢?

解决的办法还是得依靠否定顺序环视——“整个匹配文本中都不能出现字符串cat”,换句话说,就是“在文本中的任意位置,向右,都不能出现该字符串”。因此,我们用两个锚点『^』和『$』,分别匹配整个字符串的开头和结尾位置,再用否定顺序环视『(?!cat)』表达“不能出现字符串cat”。

即便知道了原理,也不见得能写对正则表达式,比如『^(?!cat).+$』就是不正确的,因为它只限定了在文本的开头(也就是『^』)右边不能出现cat,而我们真正要做的是,在文本的每一个位置右边,都不能出现cat,所以应该改成『^((?!cat).)+$』;但这还说不上完美,根据前面提到的关于括号捕获的知识,因为此处并不需要括号捕获的文本,所以最好使用非捕获型括号『(?:…)』,最终我们得到的表达式就是『^(?:(?!cat).)+$』。

提高自己的效率,做到事半功倍,是我们都希望达到的目标。如何达到这个目标呢?根据我的经验,不断反思、总结自己做过的事情,是很有成效的办法。翻译,也是这样,下面是我自己总结提炼的翻译步骤,给有兴趣的朋友作参考。

第一步,通读

通读很重要,却被许多译者忽视,他们往往认为,自己已经了解原文的“意思”,可以直接下笔,遇到问题“见招拆招”即可,翻译前通读原文,完全是浪费时间。
但是,事实似乎并非如此。我们翻译文章,要做的并不是“代替作者写文章”,而是“解释/传达作者的文字”。而文字本身是内涵丰富的,除去“意思”本身,还有用词、结构和风格等诸多方面。比如用词,原作者往往可能用一些双关语、多义词,在不同场合表达不同的意思,译者则应当尽力找到“对应”的双关语、多义词,这是个苦差,因此就更需要译者有大局观,知道原文中该词出现在哪些场合,都表示什么意思,才好取舍;再比如结构,原文中很可能有前后关联的典故/故事,有时甚至横跨几个章节,如果没有通读原文,翻译时就容易遗失原文的逻辑结构;风格也是如此,好的翻译讲究贴合原文,这种“贴合”,当然也包括风格的贴合,原文是轻快的,就不能翻译成沉重的;原文是严肃的,就不能翻译成平淡的。
当然,通读只是提供了观察鉴别这些“意思之外”的方面的机会,并不能保证译者能“准确把握”这些方面。但是不通读,却是绝不可能“把握”的。要想提高自己的通读效率,可以参考郝明义先生翻译的《如何阅读一本书》。

第二步,翻译

通读完成,对原文的布局有了把握,就可以动笔了。我们常说的“翻译”,指的就是这种狭义的翻译。
这一步的要点之一是按部就班。所谓“按部就班”,指的是拟定合理的计划,按章法推进。翻译是一项非常消耗脑力的活动,长的文章和书籍,不可能一次完成。一次做的太狠,不但精神疲惫、效率下降,也会影响下次翻译,导致译文水准波动(这是可以看出来的)。相反,如果能保持在状态比较好、精力较为充沛的情况下翻译,不但效率有保障,译文水准也可以保持统一。
这一步的另一大要点是,既要细致入微,又要有大局观,合理取舍。细致入微,指的是译文要“准确”对应原文,而有大局观,指的是不能“只见树木,不见森林”,光顾着眼前的翻译准确“对应”了,而不考虑前后文。比如原文里反复强调“vision”,有时指“愿景”,有时指“视野”,还有时来自圣经,就必须翻译为“默示”;这些情况,都应当在通读时考虑到,翻译时尽力保留这种联系,避免割裂。
这一步用到的主要是各种词典,我推荐陆谷孙先生主编的《英汉大词典》。

第三步,校对

我没有把“校对”放在最后,而放在了狭义的“翻译”之后,是因为这个阶段非常微妙:你刚刚把原文翻译完毕,对原文还有比较深的印象,现在又相对放松,有译文可以参考,可以比较迅速地浏览。
此时的校对主要是看看译文有没有译错、偏离原文的地方,因为有些词和句子即便译错了,也无伤大雅,光凭译文“看不出来”有错。在这个阶段,凭着对原文的印象加以校对,往往能发现不少此类错误。

第四步,理顺

现在基本可以脱离原文,把译文单独拿出来看了。这时候我们要做的,仿佛是修改一篇中文作文。词与词、句与句、段与段之间,逻辑是否通顺,承接是否恰当,意识是否连贯?这都是我们需要考虑的问题。比如说“尽管有这些代码,还是要好好看看”,单独看时,“这些”可以解释为多,也可以解释为少,都行得通,但译者在理顺时应当搞清楚,原文到底要强调的是多还是少?弄清这一点,才能把文脉梳通。
此外,因为不同语言的行文存在差异,所以有时我们需要把一段话拆开,有时又需要把几句话合并起来。总的来说,在这一步,不但要理顺字词,也要理顺字词背后的意思。这样的译文,读起来才不感到生涩。
关于字词和意思的通顺,已经有许多文章和教材论述了,大家如果有兴趣,可以参考民国中学教材《文心》和《国文百八课》(三联书店最近出版了整套《中学图书馆文库》,不妨都看看)。

第五步,润色

前面我们讲过,好的翻译讲究“贴合原文”,要想贴合,就少不了“润色”这一步。
具体来讲,“润色”分为三种,第一种是字词的润色,比如适当加入一些介词,打通关键环节,减少译文的生硬;第二种是风格的润色,用更符合原文风格的词语和句子替换掉“直接对应”的译文,当然前提是要保证意思不发生偏差(而不是意思“绝对不变”);另一种是文章意思的润色,照顾读者,比如把美国人人皆知的J.F.K. 翻译为“肯尼迪总统”,把“尤里乌斯凯撒”翻译成“凯撒大帝”,另外,也需要添加一些注释,比如“面积和内布拉斯加州差不多”,可以加注“内布拉斯加州,面积大概相当于湖北省大小”,华氏温度、磅重等等西方常用而中国不常用的数据,也建议译者多走一步。
想要做好“润色”这一步,推荐阅读陈望道先生的《修辞学发凡》。

一般来说,经过这五步,翻译就基本完成了;这些步骤看似繁复,熟练之后,却可以大大提高效率,而且每一阶段都有每一阶段的要点,避免精力分配错乱,在个别细节上“眉毛胡子一把抓”的问题,顾此失彼进退失据,能较好地从整体上保证译文的水平。