上一章,我们通过Word中的“使用通配符”模式,粗略见识了正则表达式的使用方法。然而通配符并不等于正则表达式,遇到复杂的情况,通配符就力不从心了。所以从本章开始,我们来看“正宗”的正则表达式。
“工欲善其事,必先利其器”,学习正则表达式也是如此。尽管正则表达式的思想和规则是基本确定的,应用起来却有许多讲究(比如,在Java、C++、Python等不同的编程语言中,同一个表达式的具体写法是不同的,在Word、Excel等软件中也是这样)。所以,学习正则表达式最好采用“中立而规范”的工具——这有点像学习摄影,开始应该学习的是构图、用光,而不是尼康、佳能或索尼相机的特性。
本书中,我们采用Firefox的插件Regular Expression Tester(意思是“正则表达式测试工具”)来学习和讲解正则表达式,选择它的好处在于:不需要搭建编程语言环境(许多时候我们并不需要在编程语言中应用正则表达式);在Windows/Unix/Mac上都可以使用;并且支持大多数通用的正则表达式功能。如果你没有接触过它,也不用担心,下面我们介绍它的安装和使用。
Regular Expression Tester是Firefox(火狐)浏览器的一个插件,因此,如果你没有安装Firefox浏览器,在使用这个插件之前,必须首先安装Firefox浏览器:
进入网址http://www.mozillaonline.com/ ,选择自己需要的版本,点击下载,之后运行下载的程序,完成安装。
图2-1,下载Firefox浏览器
然后,启动Firefox浏览器,选择工具—扩展[xdx1] (在某些版本的Firefox中,这里也可能是“附加组件”),在“获取附加组件”中输入“regular expression tester”,即可搜索到该插件,请点击右侧的“添加至Firefox”按钮,稍后Firefox会提醒尚未验证该插件的作者,没有关系,请选择“立即安装”。安装完毕重启Firefox就可以使用“regular expression tester”插件了。
图2-2,安装Regular Expression Tester
在Firefox的菜单栏选择“工具——扩展”,下拉菜单中选择Regular Expression Tester,就可以看到Regular Expression Tester的主界面。 我们可以看到,这个工具分为三大块,最上面的Regular Expression文本框用于输入正则表达式,中间的Search Text文本框用于输入待匹配的文本,下面的Result则显示匹配(还包括替换,将来我们会看到)的结果,左下角显示正则表达式操作进行的时间,右下角显示匹配发生的次数。
图2-3,Regular Expression Tester的界面
在Regular Expression和Search Text之间,还有几个选项:Case sensitive、Global、Multiline,它们对应到非常实用的功能,后面我们会详细介绍它们的意义和用途,但现在请只勾选Global选项。
图2-4,现在只需要勾选Global
先测试上一章中Word中查找手机号码的例子,把手机号输入Search Text文本框中,每行一个号码,然后在Regular Expression文本框中输入表达式『1310000[0123456789][0123456789][0123456789][0123456789].doc』 ,Result中迅速出现了结果(以黄色高亮标明),表达式匹配成功。
图2-5,『1310000[0123456789][0123456789][0123456789][0123456789].doc』的匹配
如果你仔细观察,会发现Result中每一行的末尾都有一个特殊的符号 ——没错,正则表达式处理的是字符,而每一行末尾的回车/换行符,虽然“看不见”,也是一个字符,为了展示文本的“真实面貌”,Regular Expression Tester用这个符号标识。或许你现在觉得有些别扭,但是请相信我,下面你将会发现这样的好处。
接下来,我们测试上一章中的另一个表达式『1310000????.doc』,你一定记得,在勾选了“使用通配符”之后,它就能“大致”匹配1310000号段的手机号码(虽然不一定准确)。现在我们用Regular Expression Tester重复验证一次。
图2-6,『1310000????.doc』匹配出错
出错了!匹配结果栏显示“The entered string is not a regular expression(输入的字符串不是正则表达式)”!问号作为通配符是可以匹配一个任意字符的,那么问题究竟出在哪里呢?
其实,我们已经提到过原因:通配符不等于正则表达式!通配符中*表示“任意字符串”,?表示任意字符,但这只是通配符中的规定,不是正则表达式中的规定!
在通配符的世界里,〖*〗表示“任意字符串” ,〖?〗表示“任意字符”(这里的“任意长度字符串”和“单个任意字符”都是为了保持与之前一致,你看是否有全部修改的必要) ,那么,正则表达式中如何表示这两种意义呢?要弄清这个问题,我们先看看正则表达式中的『*』和『?』表示什么意义。 『*』和『?』在正则表达式中都叫做“量词”(英文名叫quantifier),它用来“度量”字符串的长度,也就是字符能够出现的次数,其中『*』表示字符出现次数可以从0到无穷多(也就是“任意长度”了),而『?』表示字符出现0次或者1次。所以,正则表达式『6*』能匹配的,就是空字符串””,或者是”6”、”66”、”66666”……这样的字符串,而正则表达式『6?』能匹配的,只能是空字符串””,或者是单个字符构成的字符串” 6”。
图2-7,『6*』的匹配
注意:regular expression tester”目前无法显示空字符串,所以无法演示匹配空字符串的情况;本例中输入”666666”,你可以自己尝试,无论字符串中包含多少个6,都能匹配。
图2-8,『6?』只能匹配字符串”6”,而不能匹配”66”,”666”
请注意,量词只关心字符出现的次数,与字符没有任何关系:『7*』能匹配空字符串””,”7”,”77”,”777”…,『8*』能匹配空字符串””,”8”,”88”,”888”…。要模拟通配符*,还必须规定每个字符可以是“任意字符”,所以需要有办法表示“任意字符”。好在正则表达式中存在一个特殊符号,也就是普通的点号(英文句号)『.』,它表示“任意字符”,把“任意字符”和“任意长度”组合起来,就得到了“任意字符串”,也就是通配符*的意义了(这里能否麻烦你做个图,我希望是类似一个直角坐标系的,一个原点,两个带单方向箭头的轴,纵向标明“任意字符”,横向标明“任意长度”,图的名称就是“任意字符串”)。
图2-9,『.*』的匹配
现在你多半已经想到了,通配符*表示“任意字符串”,对应的正则表达式是『.*』,那么通配符?表示“任意字符”,恰好就对应了正则表达式中的『.』。因此,在表达式『1310000[0123456789][0123456789][0123456789][0123456789].doc』中,点号『.』其实只是恰好匹配了字符串中的”.”而已,如果此处出现其它字符,也可以匹配。
图2-10,表达式中的『.』“恰好”匹配了【.】
现在我们已经知道了,通配符*和?的功能,在正则表达式中要如何实现。
前面我们提到了,通配符*、?具有特殊含义,所以不能出现在文件名中。同样具有特殊含义的正则表达式字符『.』、『*』和『?』,是否应该禁止出现在正则表达式要处理的普通文本中?这显然不可能——这几个符号都是很常见的,如果使用正则表达式就不容许它们出现,正则表达式所能处理的文本就非常有限。可是,文本中的*、?等等“特殊符号”要如何来准确匹配呢?
图2-11,正则表达式中的『.』
『.』能匹配任意字符,怎么准确匹配字符串中的”.”? 其实,所谓“特殊符号”就是“意义不同于字符本身的字符”:字符『6』不是“特殊字符”,因为它只能表示字符”6”;相反,字符『*』表示“字符出现次数可以从0到无穷多”,点号『.』表示“任意字符”。在正则表达式中,这些具有特殊意义的“特殊字符”有个统一的名字“元字符(meta-character)”——它的意思是“具有特殊意义的字符”,不管你看它顺不顺眼,记住这个名字就好了。 其实,“元字符”本身的匹配非常简单,只有一条规则:转义。如果某个字符在正则表达式中具有特殊的意义(上面我们提到了『.』和『*』,将来还会遇到更复杂的情况),要表示这个字符本身,就在前面添加反斜线 \ 即可。
图2-12,用『\.』匹配点号
元字符经过转义,就失去了特殊意义,变成了普通字符,所以正则表达式『.\*』的意思就是“先匹配一个任意字符,再匹配一个星号*”。
图2-13,用『.\*』的匹配
如果你觉得转义的概念有些麻烦,不妨这样想:转义只是为了避免混淆的一种形式变化,转义序列\*“看起来”像两个字符构成的字符串,然而从概念上来说,它只表示一个普通字符*。我们要记住的是概念,而不是形式。
编程中的转义比较特别,值得拿出来讲一讲。
在一般的编程语言中,正则表达式都是以字符串(String)构造的。而对于字符串这种数据类型,多数编程语言都规定了一些转义字符序列来表示特殊字符(注意,这里说的是编程语言中的特殊字符,而不是正则表达式中的“元字符”),比如〖\n〗表示换行符,〖\t〗表示制表符。因此,在编程语言中使用正则表达式时,转义是一个麻烦的问题,但是,我们又必须弄清楚这个问题——网络上时常看到有人提问“这个正则表达式里到底需要几个反斜线?”。
转义的问题其实非常简单:在编程语言中,作为字符串出现的正则表达式,必须经过“字符串的转义”,“正则表达式的转义”才能真正生效。
举例来说,我们的正则表达式里需要出现『\*』,也就是“带转义(去掉了特殊意义)的〖*〗字符”。因为正则表达式是以字符串形式表现的,如果直接在字符串里写 \* ,许多编程语言就会报错:因为它只认识 \t 、 \n 之类的转义序列,不认识 \* 。
解决的办法是在 \* 之前添加反斜线 \ ,写成 \\* 。这样,编程语言在处理字符串时,会首先将 \\ “翻译”成 \ ,正则表达式得到的就是 \* 了。
以上说的情况比较简单,可是,如果我们需要在正则表达式中使用反斜线字符『\』呢?这个字符首先必须以转义形式写出,也就是说,在正则表达式中必须写成『\\』;但每个反斜线,在字符串里又需要转义,所以正则表达式中的一个『\』,在字符串里就必须写成【\\\\】!
不过,也有些语言不会报错——它们遇到“未定义”的转义字符,会直接忽略反斜线的转义功能,将它视为普通字符序列,Python,PHP都是如此(注:JS是否如此尚未验证)。
我的建议是,在对元字符转义时,心里一定要明白应该写几个反斜线;同时,推荐你在使用Python和PHP时,不要省略一个反斜线字符。
在本章,我们真正踏入了正则表达式的殿堂。安装好Regular Expression Tester,就搭建好了学习正则表达式的舞台。
我们也知道了,通配符不同于正则表达式,在正则表达式中,*表示字符出现次数可以从零到无穷多,?表示表示字符出现零次或者一次,它们都是量词,属于元字符。通配符*就等于正则表达式『.*』,通配符?就等于正则表达式『.』。
| 通配符 | 正则表达式 |
| ? | . |
| * | .* |
最后要记得的是,如果需要取消元字符的特殊意义,必须使用反斜线进行转义,在一些编程语言中,可能需要两个反斜线字符——我们推荐这么做!
From Life Sailor, post 正则表达式傻瓜书 第二章:元字符
之前我写了一篇《坚持了两年之后,小朋友突然不想去打冰球了…》,本来是无心之作,没想到收到了很多留言,我自己也获益不少。 本来,我以为解决了小朋友的问题,此事就这样过去了。没想到的是,暑假过后,冰球训练重开,他又老调重弹:“我不去了,我不想打冰球了……”。 这可叫我如何是好?听到他嘟嘟囔囔说这一切的时候,我心里百感交集。 成年人的生活里总是有忙不完的事情,对应的,也希望一切井井有条、按部就班。因此,这样“意外”的变数,总是第一时间让人心生无奈和烦恼:天哪,怎么会这样呢?为什么会这样呢? 不过,基于之前的经验,借鉴大家的留言,这次我显然更有心理准备一些,起码不会慌乱。 之前我写过,如果父母多阅读一些高质量的育儿专著,有助于把自己的期望水平“降”到合适的程度,就不会那么焦虑甚至抓狂。 (more…)
认识Michael很偶然,但我也很幸运,因为我见证了一个“打冰球的好孩子”的成长。 最早认识Michael是在冰球队的夏季体能训练上。那时候这群孩子还只有六岁左右,每次训练都是家长送来,在旁边观看陪伴,再接回家。但是,我很快发现有个孩子不一样,家长送他来就回家,他靠自己换好全身装备,训练完自己洗澡更衣,再由家长接回去。看起来,他好像完全没有其他孩子那种“害怕独处”的感觉。 于是我问他:“小朋友,你这么勇敢,你叫什么名字呀?” 他说:Michael。 我尝试复述他的名字,好几遍都不成功,因为我总听成“米歇”,最后他耐着性子慢慢说,我仔细听才发现最后还有个音节,嘴要更扁一点,舌头往上垫,才可以念出来,类似“米歇-厄尔”。其实这个名字写出来大家都认识,英文里读作“迈克尔”,无奈德语的发音规则很严格,字母i不会像英文那样有两种读音,结尾的el又一定要发音,所以就成了“米歇-埃尔”。 (more…)
偶然刷到一篇文章,说的是“贵族家长”群体给小朋友安排的活动:冰球、马术…… 我有点诧异,原来“冰球”也被贴上了“身份”的标签。想想自家小朋友的情况:赶上打折花了400多欧元买的全套护具,80元买的二手冰球包,每个月60欧元的俱乐部费用……想了想,似乎很难和“贵族”联系起来。 只不过,他已经坚持打冰球到了第四年,我们的生活确实有不小的变化。写下来,既是对自己有个交代,也可以作为“贵族运动”的现身说法。因为在我看来,如果非要说它是“贵族”运动,也只能“贵”在高(时间)投入、高产出而已。细细想来,我们的生活,已经被冰球深深的影响了。 (more…)
一 很多人关心,我们父子给M写了道歉信之后,对方是否有回应。 答案是:到目前为止,还没有任何回应。不过比较特殊的是,写完信之后德国小学就开始放秋假,学生不用去学校,既然见不到,也就不可能收到任何回应。 老实说,我觉得对方父母是有点反应过度的。这些年我的一条深刻经验是,如果出现分歧、矛盾,越早、在越低的层面直接面对,就越容易解决。许多小的矛盾之所以越闹越大甚至无法收场,往往都是经过了很多演绎、传话,而没有在一开始就开诚布公地面对。 试想,如果自己的孩子收到写着“我要杀了你”的信件,哪怕一开始很惊慌甚至愤怒,但仔细想一想,毕竟还有很多信息是未知的——比如对方是谁,平时言行如何,为何要写这样的信…… 更好的办法或许是先去直接寻求这些问题的答案,而不是直接把信交给家长委员会,走“公事公办”的路子。 我当然承认,“公事公办”无可厚非,对方家长也有这样的权利——所谓权利,就是“有资格做对方不喜欢的事情,人家还拿你没办法”。既然有这样的权利,就需要尊重。 所以,“严于律己,宽于待人”的确是与人相处的重要法则:我不会选择这么做,但我能理解和尊重你这么做的权利。 也有人问,那将来你遇到M的父母,会不会紧张? 答案是:不会。 (more…)
一 收到S老师邮件的时候,我刚刚胆战心惊地做完第一次德语技术分享,还在享受着同事们的鼓励。猛然间就收到一封邮件:“您的孩子在学校参与了一起性质严重的事件,您必须来学校面谈,请从以下时间段中选择……” 什么?“性质严重的事件”?我揉了揉眼睛,确认自己没有看错。再把这段文字贴到谷歌翻译里,确认自己没有理解错。 我没有看错,也没有理解错,就是“性质严重的事件”。好吧,既然“性质严重”,那谈话肯定是越早越好,最早的日期是第三天。我紧赶慢赶,回信确认了最早可能的谈话时间,虽然德国人通常都不期待能这么快收到回复。 去接他回来的路上,我发现他一切正常,完全看不出任何异样。于是,我也没有表现出任何异样,只是依照惯例,问他当天发生了什么,在学校开心不开心。 得到肯定的答复之后,我心生疑惑,看起来和“性质严重”完全不搭边。那会是什么事情呢? 我又问他,有没有和同学吵架、打架,是不是被人欺负了不敢说。但是,答案全都是“没有”。 我满心怀疑,又按捺不住,直接问:“既然一切都挺好,为什么S老师给我发信,说让我来学校跟她谈话呢?”我担心“性质严重”会吓到他,故意隐去了这个词。 他的满面春风在那瞬间凝固了,喃喃低语道:“好吧,原来是那件事,我还以为她不会跟你说。” (more…)
在2024年之前,我从来没想过自己有一天还可以加入乐团,甚至参加音乐会演奏。我只是个普通中年人,在之前文章里说过,上世纪八十年代随大流弹了十年手风琴,考过六级(当时最高八级)之后就彻底放弃了。直到二十多年后,在上海工作时才重新开始弹琴,当时有幸跟夏老师学了两年,打开了感官,懂得了音乐的世界远远比考级要广阔和美妙。再往后,就是自己看Youtube学习了一些乐理知识。因为德国几乎每个城市都有很多音乐学校,2023年末,我给本市的音乐学校写信,询问是否可以参加手风琴课程。通过回信我才知道,原来不只是“每个城市都有很多音乐学校”,而且“每个城市都有很多乐团”,哪怕是手风琴乐团。就这样,阴差阳错的,2024年初,经过简单的试奏,我加入了本市的手风琴乐团。虽然我是乐团新人,仍然有很多要学习的,但是一年下来,确实有不少感受。如果读者朋友也对音乐感兴趣,或者想让孩子学习音乐,也许我的感受可以提供一些参考。 (more…)