分组
很多情况下你需要除了是否匹配以外的其他信息,正则表达式经常用于将字符串分解为子串以匹配不同的成分。例如,一个RFC-822头信息可以分解为一个头名和一个值,并通过:来连接,如下:
From:author@example.com
User-Agent:Thunderbird1.5.0.9(X11/20061227)
MIME-Version:1.0
To:editor@example.com
这种情况下可以写一个可以匹配整行的正则表达式,并创建一个分组保存头名,另一个分组保存头信息的值。分组以(和)来标记,这两个字符的意义同数学表达式中的意义相似——将内部的内容当成一个整体,并且可以通过*,+,?或者{m,n}来设定这个分组的重复次数。例如,(ab)*将匹配零或多个ab。
>>>p=re.compile('(ab)*')
>>>printp.match('ababababab').span()
(0,10)
使用(和)来表示的分组也可以获取匹配字符串的开始和结束下标,方法是向group(),start(),end()和span()传递一个参数。分组以0开始,分组0总是存在的,它表示整个正则表达式,所以MatchObject函数都包括0作为默认参数。下面我们将看到如何表示没有匹配长度的分组:
>>>p=re.compile('(a)b')
>>>m=p.match('ab')
>>>m.group()
'ab'
>>>m.group(0)
'ab'
子分组从左到右(从1开始)标号。分组可以嵌套,为确定数字,从左到右数括号中的字符即可。
>>>p=re.compile('(a(b)c)d')
>>>m=p.match('abcd')
>>>m.group(0)
'abcd'
>>>m.group(1)
'abc'
>>>m.group(2)
'b'
可以一次向group()传递多个数字,这里将会返回一个元组,包含相应分组的值。
>>>m.group(2,1,2)
('b','abc','b')
groups()函数返回一个元组,包含所有子分组(从1开始)的字符串。
>>>m.groups()
('abc','b')
正则表达式样式中的前向引用允许前面获取的分组内容必须在当前位置上也可以匹配。例如,如果分组1的内容也可以在当前位置上找到,则\1匹配成功。注意Python的子符串中使用反斜杠来表示特殊含义,因为必须使用去掉特殊意义的字符串形式。
例如,下面的正则表达式可以检测字符串中的重复单词:
>>>p=re.compile(r'(\b\w+)\s+\1')
>>>p.search('Parisinthethespring').group()
'thethe'
如果仅仅在字符串中搜索,前向引用没有多大用处,它们在字符串替换中非常有用。
反捕获和命令分组
精细的正则表达式可能使用很多分组,同时寻找感兴趣的子串和对正则表达式本身进行分组。在复杂的正则表达式中,追踪分组序号很困难,但有两种方法可以解决这个问题,它们都使用了正则表达式扩展的普通语法,下面就先介绍这些语法。
Perl5在正则表达式中增加了几个特性,Python的re模块都支持这些特性。以往选择新的单个按键的元字符或新的以\开始的特殊序号会使得Perl的正则表达式和标准的正则表达式混淆。例如,如果选择&作为新的元字符,旧有的表达式会认定&是一个正常的字符,并且在书写\&或[&]时不再退出。
Perl开发者提出的解决办法是使用(?...)作为扩展语法:括号后加一个?表示语法错误,因为?没有片断去重复,从而引入这个语法不会带来兼容性问题;?后的字符表示使用了哪种扩展,即(?=foo)表示正向查找断言,而(?:foo)表示包含子表达式foo的非捕获分组。
Python在Perl的扩展语法中添加了扩展语法。如果?后的第一个字符是P,表示该扩展只适用于Python。目前有两个这样的扩展:(?P...)定义一个命名分组,(?P=name)是一个对命名分组的后向引用。如果Perl 5的后续版本中添加了使用不同语法的相似特性,re模块将修改以支持这个新语法,并保留Python专有的语法以兼容旧版本。
现在来看常用的扩展语法,先看在复杂正则表达式中的分组。分组从左到右编号,一个复杂的表达式可能包含很多分组,因为追踪正确的编号将变得困难;同时修改这样一个复杂的正则表达式也变得恼人——如果在开始处插入一个新的分组将改变其后的分组编号。
有时你会想使用分组来惧正则表达式的一部分,但对检索分组的内容不感兴趣。可以通过使用一个非捕获分组(?:...)来显式表明这个意思:
>>>m=re.match("([abc])+","abc")
>>>m.groups()
('c',)
>>>m=re.match("(?:[abc])+","abc")
>>>m.groups()
()
除了不能检索匹配分组匹配的内容以外,这个“非捕获分组”同捕获数组的行为完全相同:你可以在其中放入任何东西,可以使用重复元字符(如*)进行重复,以及在其他分组中进行嵌套(可以是捕获分组或非捕获分组)。(?....)在修改一个已存在的样式时相当有用,因为可以在不改变其他分组编号的情况下添加新组。值得一提的是在搜索捕获分组和非捕获分组时没有性能上的差异,任何一个的运行速度都不比另外快。
更显著的特性是命名分组:它们可以通过名字而非仅仅通过编号来指定。
命名分组的语法是Python特有扩展(?P...)中的,这里name指的是分组的名字。命名分组的行为同捕获分组相同,只是添加了名字与分组结合的功能。处理捕获分组的MatchObject完全接受表示编号分组的整数或分组的名称。命名分组同样也是给定序号,所以对分组检索信息可以使用两种方法:
>>>p=re.compile(r'(?P\b\w+\b)')
>>>m=p.search('((((Lotsofpunctuation)))')
>>>m.group('word')
'Lots'
>>>m.group(1)
'Lots'
命名数组容易使用,因为它们使你很容易地记住名字,而没有必要再去记住序号。下面的例子来自imaplib模块:
InternalDate=re.compile(r'INTERNALDATE"'
r'(?P[123][0-9])-(?P[A-Z][a-z][a-z])-'
r'(?P[0-9][0-9][0-9][0-9])'
r'(?P[0-9][0-9]):(?P[0-9][0-9]):(?P[0-9][0-9])'
r'(?P[-+])(?P[0-9][0-9])(?P[0-9][0-9])'
r'"')
显然检索m.group('zonem')比记住序号9要容易多了。
在正则表达式中的后向引用语法,如(...)\1表示分组的序号,这里的变量使用了分组名而非序号。另外一个Python扩展(?P=name)表示分组叫做name的内容在当前位置上应当再次匹配。寻找两个重复单词的正则表达式(\b\w+)\s+\1也可以写成(?P\b\w+)\s+(?P=word):
>>>p=re.compile(r'(?P\b\w+)\s+(?P=word)')
>>>p.search('Parisinthethespring').group()
'thethe'
前向断言
另外一个零宽度断言是“前向断言”,它有正和负两种形式,如下:
(?=...)
(?!...)
为了具体,来看一个正向断言的例子。考虑一个简单的正则表达式样式,它匹配文件名,并将它分割成原名和扩展名,并以.分隔。比如,在news.rc中,news是原名,而rc是该文件的扩展名。
匹配的样式非常简单:
.*[.].*$
注意.需要特别对待,因为它是一个元字符,这里已经把它放在一个字符类别中;另外需要注意的是$,它用来保证所有剩下的部分必须包含在扩展名中。这个正则表示式可以匹配foo.bar、autoexec.bat、sendmail.cf和printers.conf。
现在考虑更复杂一些的问题:如何匹配扩展名不是bat的文件名?下面是一些错误的尝试
.*[.][^b].*$
.*[.]([^b]..|.[^a].|..[^t])$
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
但是现在这样样式已经变得十分复杂了,可读性更差,难于理解。更糟的是,如果问题发生变化,比如你必须排除掉扩展名bar和exe,此样式将会变得更加复杂和难于理解。
负的前向断言将解决所有的困难:.*[.](?!bat$).*$。负的前向断言意味着:如果扩展名在这个位置上不匹配,则尝试样式的其余部分;如果bat$不能匹配,整个正则表达式就匹配失败。字符$是用来保证诸如sample.batch这样以bat开始的扩展名也能够匹配的。
排除多个扩展名也变得十分容易:将这个新扩展名添加到断言内部,作为可选的。下面的正则表达式排除了扩展名bat和exe:
.*[.](?!bat$|exe$).*$