Python 正则表达式 Howto(7)

非捕捉组和命名组

有些经过精心设计的正则表达式可能会使用许多的组,这些组即会捕捉需要的子串,也会将这些子串结构化并放在这些组中。在某些复杂的表达式中,跟踪这些组变得很困难。有两个功能可以帮助解决这个问题,他们都是用了一个公共的正则表达式扩展语法,我们先来看看这个表达式扩展语法。

Perl 5 为标准的正则表达式语法增加了很多额外的功能。Python re模块支持他们中的大部分。为了不使Perl的正则表达式不跟标准的正则表达式冲突, 如果我们选择一个新的特殊字符或者是一个写的以反斜杠开始的序列来表示这些新的功能,还是比较有难度的。如果选择 & 作为一个新的匹配字符, 但是在标准的正则表达式中& 是一个常规字符并不需要转义成\& 或者 [&] 。

Perl开发者的方案是使用 (?...) 作为扩展语法。 ? 紧跟在小括号后面再表针的正则表达式中是语法错误,原因是 ? 不会有任何的东西需要重复,所以这不会引入兼容性问题或者是二义性问题。紧跟在 ? 后面的字符集合表示什么样的扩展语法会被使用。所以 (?=foo)表示一类匹配 (前向断言) 而 (?:foo) 表示另一类匹配 (一个包含子串 foo 的非捕获组).

Python 针对Perl的扩展语法又增加了一个扩展。如果紧跟在问号后面的字符是P,那么可以肯定这是一个Python 的扩展。现咱有两个扩展, (?P<name>...) 定义了一个命名组, (?P=name) 是对那个命名组的一个反向引用. 如果将来Perl使用不同的语法添加相似的功能,RE 模块会支持新语法,同时为了兼容以前的代码而保留Python 的特殊语法。

既然我们已经浏览了一般意义上的扩展语法,我们可以返回来看看这些在复杂正则表达式中使用这些语法。由于组是按照从左到右的顺序计数,同时一个复杂的正则表达式又含有相当数量的组,追踪这些组变得非常困难。修改这样的正则表达式更是让人痛苦不堪。加入你要在开始出插入一个新的正则表达式,你得把所有的组的序号都得改了。

有时你只是需要一个组来收集部分正则表达式,你并不对组的是否匹配感兴趣,这时你可以使用一种非捕获的组(?:...), 这里的... 你可以替换成任何正则表达式。

>>> 

>>> m= re.match("([abc])+","abc")
>>> m.groups()
('c',)
>>> m= re.match("(?:[abc])+","abc")
>>> m.groups()
()

除了你不能从组中获得匹配的内容之外,非捕获组跟正常的组没有什么区别。你可以在里面放任何你相放的东西,添加重复字符,比如说*, 或者跟其他的组进行嵌套(捕获的或者是非捕获的),当修改一个现有的模式的时候, (?:...) 是非常有用的。原因是在不改变其他已经计数的组的前提下,你可以增加新的组。值得一提的是,在搜索捕获或者非捕获组的时候,性能上没有任何差别,不会存在一个比另外一快的情况。

我们来看一个更加重要的功能:命名组。跟普通组使用数字访问匹配内容不同的是, 命名组使用一个有意义的名字来访问。

命名组的语法是Python独有的: (?P<name>...). 这里名字显然指的是组的名字, 命名组除了有一个名字标识之外,跟其他的捕获组是一样的。匹配对象方法可以接收数字或者名字作为参数去访问匹配内容。命名组仍让可以使用数字访问,所以有两种方式可以访问匹配的内容:

>>> 

>>> p= re.compile(r'(?P<word>\b\w+\b)')
>>> m= p.search('(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

命名组非常好用,原因是你可以使用好记的名字而不是一些不知所云的数字来标识这个组。举个例子(来自于imaplib 模块):

InternalDate= re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
        r'(?P<year>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')

显然,使用 m.group('zonem')I访问匹配内容比使用数字9更简单,易懂。

像 (...)\1 这样的反向引用语法会使用组的组号来访问。由于我们有了命名组,显然也有一个对应的变体使用名字而不是数字。这个变体就是 (?P=name) 。其含义是名字为name的组在这个点上应该在匹配一次。寻找重名单词的正则表达式除了使用(\b\w+)\s+\1 之外,还可以使用: (?P<word>\b\w+)\s+(?P=word):

>>> 

>>> p= re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
>>> p.search('Paris in the the spring').group()
'the the'

前向断言(lookahead

另外一个零宽断言是前向断言,前向断言有积极跟消极两种形式,如下:

(?=...)

积极前向断言。如果含有的正则表达式,这里以 ...,表示,在当前的点成功匹配,代表匹配成功,否则匹配失败。但是一旦正则表达式被引擎试过了,引擎再也不会再前进了,剩下的模式在此断言开始的地方尝试。

(?!...)

消极前向断言。跟积极前向相反。不匹配表示成功。

为了使之更加易懂,我们来看个前向断言非常有用的情况。考虑一个简单的模式,这个模式的作用是匹配一个文件名字,用.将其分割成基本名字以及扩展名。例如,在 news.rc中, news 是基本的名字 rc 是扩展名。这个模式非常简单:

.*[.].*$

请注意. 是特殊字符,应该放在[]中。$确保字符串剩余的部分必须包含在扩展名中。这个正则表达式可以匹配: foo.bar ,autoexec.bat , sendmail.cf , printers.conf.

现在考虑一种更加复杂的情况,如果你想匹配一些扩展名不是bat 的文件应该怎么写呢?先来看不正确的尝试:.*[.][^b].*$ 

为了除掉bat,第一次尝试需要扩展名的首字母不是 b.错了,不能匹配 foo.bar.

.*[.]([^b]..|.[^a].|..[^t])$

为了修复第一个尝试的错误,这次这个表达式变得非常难看.第一个字符不是b, 第二个字符不是a, 第三个字符不是c; 这个可以接受 foo.bar 并且拒绝 autoexec.bat, 但是扩展部分需要三个字母。比如他不能接受 sendmail.cf. 继续尝试:

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

在第三次尝试中,第二个以及第三个字母都是可选的,这样可以匹配短的扩展名。比如 sendmail.cf.

这个模式已经非常复杂了,失去了扩展性可读性。更严重的是如果需求改变了,如果你想同时干掉 bat 和 exe扩展名, 这个模式那就更麻烦了。

一个消极前向断言可以消除你的困惑。

.*[.](?!bat$).*$ 这个消极前向断言的意思是: 如果表达式 bat 在当前的点不匹配, 尝试剩下的部分; 如果 bat$ 匹配, 整个匹配失败. 尾部的 $ 是必须的可以用来保证 像之列的串 sample.batch,这个串虽然以 bat开始,但是还是被允许的。

如果要干掉另外的文件扩展名也同样容易:我们只是简单将需要干掉的加进去就可以了。下面的模式会干掉 bat 或者是 exe:

.*[.](?!bat$|exe$).*$

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值