4.9. 正则表达式 101

正则表达式是一种强大的(并且非常标准化的)方法,通过使用复杂的字符模式,可以对文本进行搜索、替换和分解。如果你曾经在其它的语言中(象Perl)使用过正则表达式,应该可以跳过本节,只要读一下关于 re 模块 的小结,对可用的函数和它们的参数有个大概的了解就行了。

字符串拥有一些方法,可以进行搜索(indexfind,和 count),替换(replace),和分解(split),但它们只限于一些最简单的情况。搜索方法可以查找单个,固定编码的子串,并且它们总是大小写敏感的;为了对一个字符串 s 执行大小写不敏感搜索,你必须调用 s.lower()s.upper() ,并确保要搜索的字符串有着相匹配的大小写。replacesplit 方法也有着同样的限制。如果可能,你应该使用它们(它们的执行速度快并易于阅读),但对于更复杂的情况,你不得不转移到正则表达式上来。

例 4.19. 在字符串的末尾进行匹配

这些例子是受到在我每天的工作中所遇到的现实问题的启发而写出来的。我需要从一个老系统中导出街道的地址,在将它们导入到一个新系统之前,进行清理和标准化处理。(瞧,我不只是拼凑出这个东西,它实际上有用。)

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')               1
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')               2
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.') 3
'100 NORTH BROAD RD.'
>>> import re                              4
>>> re.sub('ROAD$', 'RD.', s)              5 6
'100 NORTH BROAD RD.'
1

我的目标是对街道地址进行标准化处理,将 'ROAD' 变成象 'RD.' 一样的缩写。乍一看,我以为相当简单,只要使用字符串方法 replace 就行了。毕竟所有的数据都已经是大写的了,所以大小写不匹配不会是一个问题。并且要搜索的字符串,'ROAD',是一个常量。而在这个迷惑人的简单例子中,s.replace 的确是可以工作的。

2

不幸的是,生活总不是一帆风顺的,我很快就意识到了。这儿的问题是,在地址中 'ROAD' 出现了两次,一次是作为街道名称 'BROAD' 的一部分,一次是作为路本身的单词。replace 方法看到这两次的出现,盲目地将两个都进行了替换;同时,我发现地址受到了破坏。

3

为了解决地址含有超过一个 'ROAD' 子串的问题,我们可以采用象这样的方法:仅搜索和替换在地址最后 4 个字符中的 'ROAD's[-4:]),不管字符串的其它部分(s[:-4])。但你可以看出这已经变得不实用了。例如,这个模式依赖于我们要替换的字符串的长度(如果我们用 'ST.' 来替换 'STREET',我们得需要使用 s[:-6]s[-6:].replace(...))。你愿意在六个月期间回来修改并调试它吗?我知道我不愿意。

4

是该转到正则表达式的时候了。在Python中,所有与正则表达式相关的功能都包含在 re 模块中。

5

看一下第一个参数:'ROAD$'。这是一个非常简单的正则表达式,它仅匹配出现在一个字符串末尾的 'ROAD'$ 表示“字符串的末尾”。(有一个与之对应的字符, ^ 符号,它表示“字符串的开始”。)

6

使用 re.sub 函数,我们对字符串 s 进行搜索,对满足正则表达式 'ROAD$' 的,用 'RD.' 进行替换。这样将在字符串 s 的末尾对 ROAD 进行匹配,但决不会对属于单词 BROAD 一部分的 ROAD 进行匹配,因为它是在 s 的中间。

例 4.20. 匹配整个单词

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)     1
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)  2
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)  3
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)  4
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s) 5
'100 BROAD RD. APT 3'
1

继续我的清理地址的故事,我很快发现前一个在地址的末尾匹配 'ROAD' 的例子不太好,因为不是所有的地址都包含一个街道的指示词;有一些仅以街道的名称结束。大部分时间,我不会遇上它,但如果街道的名称是 'BROAD',那么这个正则表达式将匹配字符串末尾的 'ROAD',而这个 'ROAD' 是单词 'BROAD' 的一部分。而这可不是我想要的。

2

真正想要的是,当 'ROAD' 在字符串的末尾,并且它是整个单词,而不是某些长单词的一部分时,对它进行匹配。为了在一个正则表达式中表达出来这个意思,你要使用 \b,它的意思是说“单词的边界必须就在这儿”。在Python中,由于在一个字符串中, '\' 必须进行转义,从而使理解变得困难。(有时把它叫做“反斜线麻烦”,而这就是为什么正则表达式在Perl中比在Python中容易的原因之一。不好的一面,Perl混淆了正则表达式与其它的语法,所以如果你有一个bug,可能很难分辨出是语法中的错误,还是你的正则表达式中的错误。)

3

为了解决反斜线麻烦,你可以使用所谓的“原始字符串”,即在 '...' 前加字母 r 的前缀。这样就告诉Python在这个字符串中没有需要转义的。'\t' 是一个制表符,但 r'\t' 实际是反斜线字符 \ 跟着字母 t。我建议当处理正则表达式时,总是使用原始字符串,否则很快就会搞乱的(并且正则表达式很快就被自已给搞乱了)。

4

唉! 不幸的是,我很快发现更多同我的逻辑相反的例子。在本例中,街道地址包含了作为整个单词的 'ROAD' ,但它不在末尾,因为在街道指示词后面有房间号。由于 'ROAD' 不正好在字符串的末尾,没有匹配上,所以对 re.sub 的整个调用最后没有进行任何的替换,而我们得到了初始的字符串,它不是我们想要的。

5

为了解决这个问题,我删掉了 $ 符,而添加了另一个 \b。现在正则表达式读起来为“对 'ROAD' 进行匹配,当它在字符串中任何地方是整个单词时”,不管在是末尾,开始,或是在中间的什么地方。

这只是正则表达式能够实现功能的冰山之一角。它们相当强大,有专门讲述它们的整本书。它们不是解决所有问题的正确方法。你应该充分地学习它们,了解何时使用它们是合适的,何时比起要解决的问题来说,它们将只会引起更多的麻烦。

 

有一些人,当遇到了一个问题,会想“我懂,我将使用正则表达式。”现在他们有了两个问题。

 
--Jamie Zawinski,在 comp.lang.emacs  

进一步阅读