首页 > 程序开发 > Web开发 > Python >

Python——正则表达式(3)

2016-03-02

本文译自官方文档: Regular Expression HOWTO 参考文章: Python——正则表达式(1) Python——正则表达式(2) 全文下载: Python正则表达式基础 ===========================================================

4.更强大的功能

到目前为止,我们已经学习了正则表达式的一部分内容。在这一节,我们将介绍一些新的元字符,介绍如何使用组(groups)来检索被匹配的部分文本。

-----------------------------------------------------------------------------------------------------------------------------------------------------

4.1.更多的元字符

一些元字符被称为“零宽断言”,它们不匹配任何字符,只是简单地表示成功或失败。例如,\b表示当前位置位于一个单词的边界,但 \b 并不能改变位置。

这意味着零宽断言不能被重复使用,因为如果它们在一个给定的位置匹配一次,它们显然能够匹配无限次。

I

或操作符,如果A和B都是正则表达式,A|B将匹配任何能够匹配A或者B的字符串。为了让或操作符 | 在多字符的字符串匹配中工作地更加合理,

它的优先级很低,例如Crow | Servo 将会匹配Crow或者Servo,而不是Cro 然后[w] or [S],再跟着ervo。

如果需要匹配字符‘|’,使用 \|,或者将它放在一个字符类中,[|]。

^

匹配一行的开头位置。如果没有设置MULTILINE标志,它只匹配字符串的开头位置。

如果设置了MULTILINE标志,它将匹配字符串中每一行(根据换行符)的开头。

举个例子,如果你只想在一行的开头位置匹配From这个单词,这个正则表达式应该是^From。

>>> print(re.search('^From','From Here to Eternity'))
<_sre.SRE_Match object; span=(0, 4), match=&#39;From&#39;>
>>> print(re.search(&#39;^From&#39;,&#39;Reciting From Memory&#39;))
None

$
匹配一行的结尾,它每当遇到换行符也会进行匹配。看下面例子:

>>> print(re.search(&#39;}$&#39;,&#39;{block}&#39;))
<_sre.SRE_Match object; span=(6, 7), match=&#39;}&#39;>
>>> print(re.search(&#39;}$&#39;,&#39;{block} &#39;))
None
>>> print(re.search(&#39;}$&#39;,&#39;{block}\n&#39;))
<_sre.SRE_Match object; span=(6, 7), match=&#39;}&#39;>
如果要匹配字符&lsquo;$&rsquo;,需要使用 \$,或者将它放进一个字符类中 [$]。

\A
只匹配字符串的开始位置。如果不在MULTILINE模式下,\A 和 ^ 的有一样的效果。在MULTILINE模式下,它们是不一样的:\A 还是只匹配字符串的开头,而 ^ 将匹配字符串每一行的开头,即每一个换行符之后也会进行匹配。

\Z
只匹配字符串的结尾。

\b
单词边界。这是一个零宽断言,只匹配一个单词的开始位置或者结束位置。单词是由字母或者数字构成的序列,所以一个单词由空格或者非字母数字字符作为结束标志。

下述例子只有当class作为单独的一个完整单词的时候才会被匹配,当class是其他单词的一部分时不会被匹配。

>>> p = re.compile(r&#39;\bclass\b&#39;)
>>> print(p.search(&#39;no class at all&#39;))
<_sre.SRE_Match object; span=(3, 8), match=&#39;class &#39;>
>>> print(p.search(&#39;the declassified algorithm&#39;))
None
>>> print(p.search(&#39;one subclass is&#39;))
None
>>> print(p.search(&#39;test:#class$&#39;))
<_sre.SRE_Match object; span=(6, 11), match=&#39;class&#39;>

使用这个特殊的字符,有两点需要注意。

第一,Python字符串跟正则表达式有些字符存在冲突。在Python字符串中,\b 表示退格符(ASCII码值是8),如果你不使用原始字符串,Python会将 \b 解释为退格符,所以这个正则表达式就不会按照你所想的方式去匹配。下例使用与上例相同的正则表达式但省去字符&lsquo;r&rsquo;:

>>> p = re.compile(&#39;\bclass\b&#39;)
>>> print(p.search(&#39;no class at all&#39;))
None
>>> print(p.search(&#39;\b&#39;+&#39;class&#39;+&#39;\b&#39;))
<_sre.SRE_Match object; span=(0, 7), match=&#39;\x08class\x08&#39;>
第二,在字符类中这个断言也没有用,\b 在字符类中就相当于Python中的退格符。

\B
这个零宽断言与\b的意义相反,它匹配非单词边界。

------------------------------------------------------------------------------------------------------------------------------------------------------

4.2.分组

实际上,除了正则表达式是否匹配外,你需要知道更多的信息。对于比较复杂的内容,正则表达式通常使用分组的方式分别对不同内容进行匹配。

比如,一个RFC-822头部的每一行使用分号&lsquo;:&rsquo;使之分为名字和值:

From: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com
这种情况下,可以写一个正则表达式匹配整个头部,然后利用分组功能,使一个组匹配头的名字,另一个组匹配名字对应的值。

在正则表达中使用小括号&lsquo;(&rsquo;和&lsquo;)&rsquo;来划分组,小括号在正则表达式中的意思和在它们数学表达式中的意思一样,它们使得在小括号中的内容成为一个分组,然后你可以使用重复符号重复整个分组(星号 *、加号 + 或者 {m,n})。例如,(ab)* 会匹配0次或者更多次的 ab。
>>> p = re.compile(&#39;(ab)*&#39;)
>>> print(p.match(&#39;ababababab&#39;).span())
(0, 10)
使用小括号&lsquo;(&rsquo;和&lsquo;)&rsquo;表示的子组我们也可以对它们按照层次索引,可以将索引值按照层次传递给group()、start()、end()和span()等这些方法。序号0表示第一个分组,它总是存在的,即整个正则表达式本身,所以匹配对象的这些方法将序号0作为默认参数。

>>> p = re.compile(&#39;(a)b&#39;)
>>> m = p.match(&#39;ab&#39;)
>>> m.group()
&#39;ab&#39;
>>> m.group(0)
&#39;ab&#39;
子分组是从1开始,从左至右编号的。另外,分组也可以嵌套,因此我们可以从左到右统计左括号来统计子组的序号。
>>> p = re.compile(&#39;(a(b)c)d&#39;)
>>> m = p.match(&#39;abcd&#39;)
>>> m.group(0)
&#39;abcd&#39;
>>> m.group(1)
&#39;abc&#39;
>>> m.group(2)
&#39;b&#39;
group()函数也允许多个参数,在这种情况下,它将返回一个元祖,包含指定分组内容:

>>> m.group(2,1,2)
(&#39;b&#39;, &#39;abc&#39;, &#39;b&#39;)
groups()方法返回一个元祖,包含序号从1开始的所有子分组的内容:
>>> m.groups()
(&#39;abc&#39;, &#39;b&#39;)
反向引用指的是你可以在后面的位置引用先前匹配过的内容,比如,\1 引用第一个分组的内容,如果当前位置的内容和分组1的内容一样,则匹配成功。要注意Python中的字符串使用反斜杠加上数字来表示字符串中的任意字符,所以在正则表达式中使用反向引用的时候记得使用原始字符串。

举例如下,下面的正则表达式匹配连续出现两次的单词:
>>> p = re.compile(r&#39;(\b\w+)\s+\1&#39;)
>>> p.search(&#39;Paris in the the spring&#39;).group()
&#39;the the&#39;
像这样搜索字符串的时候并不会经常用到反向引用,因为很少有文本格式会这样来重复字符。但是你很快就会发现在字符串替换的时候它们是非常有用的。

-----------------------------------------------------------------------------------------------------------------------------------------------------
4.3.非捕获组和命名组
精心设计的正则表达式可能会划分很多组,这些组不仅可以匹配相关的子字符串,还可以对正则表达式本身进行分组和结构化。在复杂的正则表达式中,我们很难去跟踪分组的序号,这里有两种方式可以帮助解决这个问题,这两种方式都用了同一种正则表达式扩展语法,所以我们先来看看这个表达式扩展语法。

众所周知,Perl5为标准的正则表达式增加了很多强大的扩展功能,对于这些扩展功能,Perl开发者并不能选择一个新的元字符或者通过反斜杠构造一个新的特殊序列来实现扩展功能,因为这会和标准正则表达式冲突。比如,如果他们选择 & 作为一个新的元字符,那么旧的表达式将会把 & 作为一个特殊的正则字符,但是却没有通过写成 \& 或者 [&] 来去掉它的特殊含义。

最后的解决方法是Perl开发者选择 (?...) 作为扩展语法,问号 ? 紧跟在小括号后面本身是一个语法错误因为前面没有字符可以重复,所以这就解决了兼容问题。紧跟在问号之后的字符指定了哪种扩展功能将被使用,例如,(?=foo) 是一种扩展功能(前向断言),(?:foo)是另一种扩展功能(一个包含子字符串 foo 的非捕获组)。

Python支持Perl的一些扩展语法,并且还增加了一个扩展语法。如果在问号 ? 之后的字符是 P 的话,可以肯定这是一个Python的扩展语法。

现在我们已经知道了扩展语法,所以让我再回过头来看看这些扩展语法在复杂的正则表达式中是如何工作的。


有时你想要用一个分组去指定正则表达式的一部分,但是并不关心分组匹配的内容。你可以通过非捕获组来实现这个功能:(?:…),这里省略号 … 可以换成任何正则表达式:

>>> m = re.match(&#39;([abc])+&#39;,&#39;abc&#39;)
>>> m.groups()
(&#39;c&#39;,)
>>> m = re.match(&#39;(?:[abc])+&#39;,&#39;abc&#39;)
>>> m.groups()
()
非捕获组除了不能获取任何内容外,在其他方面和捕获组一样,你可以在里面放任何正则表达式,使用重复功能的元字符,或者将它嵌套到其他分组(捕获的或者非捕获的)。(?:…)非捕获组在修改已存在的模式的时候是非常有用的,因为添加非捕获组并不影响其他捕获分组的序号。值得一提的是,非捕获分组和捕获分组在搜索速度上没有任何区别。

另一个比较重要的功能是命名组,分组可以通过这个功能指定一个名称,而不是序号。

命名组的语法是Python特有的扩展语法:(?P…)。在这个正则表达式中,name显然指的是分组的名称。命名组和捕获组的行为是一样的,只是它可以通过一个名称来访问。匹配对象的所有方法不仅可以处理那些由数字引用的捕获组,还可以处理通过字符串引用的命名组。命名组仍然存在序号,所以你可以通过名称或者序号两种方式获取内容:

>>> p = re.compile(r&#39;(?P\b\w+\b)&#39;)
>>> m = p.search(&#39;((((Lots of punctuation)))&#39;)
>>> m.group(&#39;word&#39;)
&#39;Lots&#39;
>>> m.group(1)
&#39;Lots&#39;

命名组通过名称访问分组可以让你不用去记住分组的序号,从而使处理变得更加简单。下面看一个imaplib模块中的正则表达式的例子:

>>> InternalDate = re.compile(r&#39;INTERNALDATE "&#39;
			  r&#39;(?P[ 123][0-9])-(?P[A-Z][a-z][a-z])-&#39;
			  r&#39;(?P[0-9][0-9][0-9][0-9])&#39;
			  r&#39;(?P[0-9][0-9]):(?P[0-9][0-9]):(?P[0-9][0-9])&#39;
			  r&#39;(?P[-+])(?P[0-9][0-9])(?P[0-9][0-9])&#39;
			  r&#39;"&#39;)

这可以很方便地通过m.group(&lsquo;zonem&rsquo;)来获取内容,而不需要记住分组序号9。

在正则表达式中,反向引用的语法是类似于这样的:(…)\1,通过数字去引用分组。在命名组中,我们可以很自然地想到通过名称去引用前面的分组。这是另一个Python的扩展:(?P=name)表示当前位置的内容是前面名称为name的分组的内容。匹配连续出现两次的单词的正则表达式 (\b\w+)\s+\1 也可以写作(?P\b\w+)\s+(?P=word)
>>> p = re.compil(r&#39;(?P\b\w+)\s+(?P=word)&#39;)
>>> p.search(&#39;Paris in the the spring&#39;).group()
&#39;the the&#39;

------------------------------------------------------------------------------------------------------------------------------------------------------

4.4.前向断言
前向断言是另一个零宽断言。前向断言包括前向肯定断言和前向否定断言两种形式,如下所述:

(?=…)
前向肯定断言。如果当前包含的正则表达式在当前位置匹配成功,则代表成功,否则失败。如果该部分正则表达式被匹配引擎尝试过,就不会继续进行匹配了。剩下的模式在此断言开始的地方继续尝试。

(?!...)
前向否定断言。这跟前向肯定断言相反,如果不匹配则表示成功,匹配则失败。

为了更具体,让我们通过一个案例来看看前向断言的作用。考虑写一个简单的模式来匹配一个文件名,文件名通过一个点号&lsquo;.&rsquo;分割成名称和扩展名两部分,比如,new.rc,news是文件的名称,而rc是文件的扩展名。

匹配文件名的模式非常简单,如下:

.*[.].*$
注意点号&lsquo;.&rsquo;是一个元字符,所以文件名中间的点需要将它放入一个字符类以去掉它的特殊功能。同时注意末尾的结束符$,它用来保证字符串的剩余部分都包含在扩展名内。这个正则表达式会匹配foo.bar、autoexec.bat、sendmail.cf和printers.conf。

现在,考虑这样一个稍微复杂的情况,如果你想匹配扩展名不是bat的文件名,我们可以看一下你有可能写错的尝试:
.*[.][^b].*$
这种尝试想要匹配扩展名的第一个字母不是b的文件名,从而将扩展名为bat的文件排除。但是要知道,foo.bar的扩展名bar的第一个字母也是b,这种方法会将它一并排除。

所以你在上述解决方法的基础上修改如下,这个表达式变得有点复杂:
.*[.]([^b]..|.[^a].|..[^t])$
扩展名的第一个字母不是b,或者扩展名的第二个字母不是a,或者扩展名的第三个字母不是t,符合这些规则的文件名将会被匹配。这样的话就会匹配foo.bar,而只将autoexec.bat排除。但是它只能匹配扩展名包含3个字母的文件名,而扩展名包含2个字母的,如sendmail.cf将会被排除。所以我们继续修复这个正则表达式:
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
在这个正则表达式中,扩展名的第二个和第三个字母都用问号 ? 变成可选的,这样就可以匹配少于3个字母的扩展名了,比如sendmail.cf。

但是,目前的这个正则表达式变得非常复杂,非常难于阅读和理解。更糟糕的是,如果你的需求改变了,你想要将扩展名为bat和exe的文件名排除,那么这个正则表达式就会变得异常复杂。

这时,前向否定断言就可以很简单地解决这些问题。
.*[.](?!bat$).*$
前向否定断言的含义是这样的:如果表达式bat在当前位置不匹配,则尝试剩余的模式;而如果bat$匹配了,则整个正则表达式将失败。(?!bat$)末尾的结束符&lsquo;$&rsquo;保证可以正常匹配像sample.batch这样的扩展名是以bat开头的文件名。


所以,现在将另一个文件的扩展名排除在外就变得很简单,只要将它以或选的方式加入到断言中即可。比如,下面的正则表达式可以将扩展名为bat和exe的文件名排除:

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

相关文章
最新文章
热点推荐