这一章我们讲分组,分组在正则表达式中是一个很有用的工具。它主要有以下的作用:重复,缩小逻辑运算的范围,内容提取,反向引用,通过命名读取内容等。如果你之前没有学习过分组,这么一看这些作用说明,肯定没法做到顾名思义,甚至会觉得有点迷茫。放心,就像我之前说过,很多东西你只要一旦明白,就会觉得非常的简单,如果你读过我之前正则表达式的其他章节,你肯定深有体会,分组也一样,接下来我们会详细的谈论分组。
6.分组
分组主要是用圆括号包裹起来的语法,里面可以包含正则匹配的语法,同时还有一些分组特有的语法。主要有:捕获组,可用于提取内容(...),非捕获组,不提取内容(?:...),命名捕获组(?P<name>...),引用命名组(?P=name),引用组通过序号例如/1,/2。
6.1重复以及内容提取((...))
在前面讲的语法的时候,我们提到了重复,我们可以通过*,+,?,{m,n}这样的语法来重复前面的字符,但是我们需要重复的字符不是一个而是多个呢?这时候就需要用到分组双括号(),比如(abc)+,这个代表abc可以重复一次或者多次,从而可以匹配abcabcabc这样的字符串。如果在()中开始位置加入?:表示不提取内容。看一下下面的例子:
python
import re
a = "abcabc23abcabc12ac1123abc31123abc"
b = re.findall(r"(abc)+\d+", a)
b1 = re.findall(r"(?:abc)+\d+", a)
c = re.search(r"(abc)+\d+", a)
c1 = re.search(r"((abc)+(\d+))", a)
print("a= {}, b = {}, b1 = {}, c = {}, c.group() = {}, c.group(1) = {} \
c1.group(0) = {}, c1.group(1) = {}, c1.group(2) = {}, c1.group(3) = {}".
format(a, b, b1, c, c.group(), c.group(1), c1.group(0),
c1.group(1), c1.group(2), c1.group(3)))
# 输出结果:a= abcabc23abcabc12ac1123abc31123abc,
# b = ['abc', 'abc', 'abc'], b1 = ['abcabc23', 'abcabc12', 'abc31123'],
# c = <re.Match object; span=(0, 8), match='abcabc23'>,
# c.group() = abcabc23, c.group(1) = abc c1.group(0) = abcabc23,
# c1.group(1) = abcabc23, c1.group(2) = abc, c1.group(3) = 23
b中我们用圆括号包裹了abc,然后在括号后面跟了一个+,表示abc至少重复一次,后面跟了\d+,表示数字至少出现一次。我们看b的结果,有点奇怪,怎么都是abc?这是因为findall这个函数只是提取了括号里面的内容。对比一下b1,结果就是比较符合预期。b1在原括号中开始位置加入了?:表示不提取括号内容。b1结果的第一个成员abcabc23,其中abc至少出现了两次,我们看b的结果中,都是abc,原因是()只提取重复的最后一次,所以只有abc,这个abc指的是abcabc23中的第二个abc。
c我们用了一个全新的函数search,这个函数在字符串中找到一个匹配就停止了。打印c,发现c是一个Match对象,这个对象最常用的函数就是group,默认group()返回匹配整个正则的串,其实和group(0)效果是一样的。这个正则表达式中有多少个分组(),后面就可以跟最大数字是这个分组的个数。c1有三个分组,那么group(1)代表第一个分组,group(2)代表第二个分组,group(3)代表第三个分组。分组是从左到右计算,第一个左括号(算第一个,第二个左括号(算第二个,就算括号之间嵌套也没关系。c1的group(0)和group(1)结果是一样的,原因是1把整个正则表达式都包含进来了,所以结果也是匹配整个正则的值,group(1)和group(2)分别是包含了字母和数字。
6.2限制逻辑运算作用域
逻辑运算我们还没有讲到,我们用|表示或,我们看一下下面的代码:
python
import re
a = "abc2342adc1123abc31123abc"
b = re.findall(r"abc\d{1,9}|adc\d{1,9}|abc3abc\d{1,9}",a)
c = re.findall(r"(?:abc|adc|abc)\d{1,9}", a)
print("a= {}, b = {}, c = {}".
format(a, b, c))
# 输出结果:a= abc2342adc1123abc31123abc,
# b = ['abc2342', 'adc1123', 'abc31123'],
# c = ['abc2342', 'adc1123', 'abc31123']
b是用我们之前的方式,我们为了用逻辑或,需要将每种可能就完整的写下来,这样显得繁琐,因为每个或的正则都有共通的\d{1,9},我们可以全部提取到外面,这就需要用到分组圆括号,这样这个或的逻辑运算只是被限制在圆括号内,不会影响括号外的正则,如c1所示。因为findall只捕获圆括号中的内容,所以可以看到我们用?:来阻止捕获内容,这样findall可以得到完整的结果。
6.3命名捕获组((?P<name>...))
可以(?P<name>...)这样的形式来给捕获组命名,注意P是大些的P,这样我们可以在group参数里面通过名字来引用,这样做的好处是比没有意义的数字好记。看一下下面的代码:
python
import re
a = "abcabc23abcabc12ac1123abc31123abc"
b = re.search(r"(?P<word>(?P<letter>abc)+(?P<number>\d+))", a)
print("b = {}, b.group(1) = {}, b.group(2) = {}, b.group(3) = {} \
b.group(word) = {}, b.group(letter) = {}, b.group(number) = {}".
format(b, b.group(1), b.group(2), b.group(3),
b.group("word"), b.group("letter"), b.group("number")))
# 输出结果:b = <re.Match object; span=(0, 8), match='abcabc23'>,
# b.group(1) = abcabc23, b.group(2) = abc, b.group(3) = 23
# b.group(word) = abcabc23, b.group(letter) = abc, b.group(number) = 23
b中我们将三个分组都做了命名,最外层的分组命名为word,这包含了剩下的两个分组。另外两个分组分别命名为letter和number。打印的结果来看,group中输入名字和输入分组的位置,都可以得到相同的结果,只是命名之后更清晰明白,增加了可读性。
6.4分组引用((?P=name), \number, number是分组的位置)
上一节我们讲了分组,我们可以在正则表达式中引用这个分组,可以通过命名引用?P=name),也可以通过分组位置引用,用\加上分组位置。看一下下面的代码:
python
import re
a = "abc12abcabc123abc"
b = re.search(r"(?P<letter>abc)\d+(?P=letter)", a)
b1 = re.search(r"(abc)\d+\1", a)
print("b = {}, b.group() = {}, b.group(1) = {} \
b1.group() = {}, b1.group(1) = {}".
format(b, b.group(), b.group(1),
b1.group(), b1.group(1)))
# 输出结果:b = <re.Match object; span=(0, 8), match='abc12abc'>,
# b.group() = abc12abc, b.group(1) = abc
# b1.group() = abc12abc, b1.group(1) = abc
b首先命名了一个分组为letter,这个分组包含了字母abc,然后跟了\d+表示至少有一个数字,然后后引用了这个分组,b1是用\1实现了同样的作用。我们可以看到结果是一样的。这里有一点要注意(?P=letter)这只是引用分组,但是它本身并不是分组,也就是说b的正则表达式中只有一个分组,所以如果你b.group(2)会提示没有这个分组。如果你要把它当作一个分组,需要再加一对圆括号,如下面的代码所示:
python
import re
a = "abc12abcabc123abc"
b = re.search(r"(?P<letter>abc)\d+((?P=letter))", a)
b1 = re.search(r"(abc)\d+(\1)", a)
print("b = {}, b.group() = {}, b.group(1) = {}, b.group(2) = {} \
b1.group() = {}, b1.group(1) = {}, b.group(2) = {}".
format(b, b.group(), b.group(1), b.group(2),
b1.group(), b1.group(1), b1.group(2)))
# 输出结果:b = <re.Match object; span=(0, 8), match='abc12abc'>,
# b.group() = abc12abc, b.group(1) = abc, b.group(2) = abc
# b1.group() = abc12abc, b1.group(1) = abc, b.group(2) = abc
b和b1都多加了一个括号,都有了两个分组,这样就可以通过group(2)来获取分组的内容了。
讲到这里,我们的分组就讲完了。分组在重复多个字符还是很有用的,另外分组还可以命名并且被引用,这增加了程序的可读性,下一章我们会介绍修饰符,包含全局和分组修饰符。