Python中re模块之分组匹配与断言(第3节)


1、分组匹配

Python中进行匹配操作时,在正则表达式中加入括号“()”,可以对匹配到的字符串进行分组,目的是分离我们匹配到的字符串。使用group()方法可以获取匹配的所有内容,也可以获取匹配结果中指定分组的内容,另外,还可以通过groups()方法把所有匹配出来的分组加入到元组中。例如:

动手练一练:

import re

a1 = r"(\w+)\s+(\d+)"  # 分离姓名和电话号码
b1 = "Maria 123456789"

result = re.match(a1, b1)

name = result.group(1)
phone = result.group(2)

print(f"名字: {name}")  # 输出 名字: Maria
print(f"电话号码: {phone}")  # 输出 电话号码: 123456789
print(result.group())  # 输出 Maria 123456789
print(result.groups())  # 输出 ('Maria', '123456789')

c1 = re.findall(a1, b1)
print(c1)  # 输出 [('Maria', '123456789')]

执行以上代码,输出结果为:

名字: Maria
电话号码: 123456789
Maria 123456789
('Maria', '123456789')
[('Maria', '123456789')]

上面的例子中,“(\w+)”代表匹配一个或多个字母、数字或下划线,表示姓名。“\s+”代表匹配一个或多个空格字符,表示姓名和电话号码之间的间隔。“(\d+)”代表匹配一个或多个数字,表示电话号码。使用group(1)来获取第一个括号匹配的结果,使用group(2)来获取第二个括号匹配的结果。这里需要注意的是,re.match()函数和re.search()函数会返回一个对象,必须通过group()函数提取数据,而re.findall()函数会直接返回一个包含所有分组结果的列表。

正则表达式中分组匹配的本质是,将匹配的内容里一部分抠出来,如果抠出的内容不止一个,就放到元组里面。例如:

动手练一练:

import re

#分组匹配
a = """
1张三,今年15岁。
2李四,今年16岁。
3王五,今年17岁。
4赵六,今年18岁。
"""

#选择每行逗号前面的字符串,不包括逗号
b = re.compile(r"^(.*),", re.M) # “^”匹配字符串的开头,“re.M”实现多行匹配
result = b.findall(a)
print(result)

#选择每行逗号前面的字符串,包括逗号并分组存储
b = re.compile(r"^(.*)(,)", re.M)
result = b.findall(a)
print(result)

执行以上代码,输出结果为:

['1张三', '2李四', '3王五', '4赵六']
[('1张三', ','), ('2李四', ','), ('3王五', ','), ('4赵六', ',')]

上面的例子中,正则表达式中的re.M参数表示将字符串视为多行,从而“^”匹配每一行的行首,“$”匹配每一行的行尾。

2、贪婪与非贪婪匹配

贪婪匹配:在Python中,正则表达式默认使用贪婪匹配模式,即尽可能多地匹配字符串。例如:

动手练一练:

import re

s = "acbab"
p = re.compile(r"a.*b")
result = p.findall(s)
print(result)  # 输出: ['acbab']

执行以上代码,输出结果为:

['acbab']

上面的例子中,“a.*b”将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索“acbab”的话,它会匹配整个字符串“acbab”,这被称为贪婪匹配

非贪婪匹配:也就是尽可能少地匹配字符串。在前面的教程中,我们已经介绍了正则表达式提供的限定符包括“*”、“+”、“?”、“{n}”、“{n,}”和“{n, m}”,只要在这些限定符的后面加上问号“?”,就可以进行非贪婪匹配。例如:

动手练一练:

import re

s = "acbab"
p = re.compile(r"a.*?b")
result = p.findall(s)
print(result)  # 输出: ['acb', 'ab']

执行以上代码,输出结果为:

['acb', 'ab']

上面的例子中,星号“*”后面加上问号“?”就变为非贪婪模式,此时会匹配尽可能少的字符。在“a.*?b”中,可以匹配最短的,以a开始,以b结束的字符串。如果用它来搜索“acbab”的话,它会匹配“acb”(第一到第三个字符)和“ab”(第四到第五个字符),因为它会匹配尽可能少的字符,所以不会匹配“acbab”。

下表列出了非贪婪匹配的常见语法:

用法\语法 功能
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或一次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

3、零宽断言

在程序开发中使用正则表达式做匹配的时候,我们希望匹配一个字符串,这个字符串的前面或后面是一个特定的内容,但我们又不想要前面或后面的这个特定的内容,这个时候就要借助正则表达式中的零宽断言零宽断言,简单来说就是用于指定一个位置,这个位置应该满足一定的条件,但是这个位置又不纳入匹配的结果,所以叫“零宽”。

举个简单的例子,比如我们打开任意一个网页,在网页的空白部分右键单击鼠标,然后从弹出的菜单中选择“查看网页源代码”,在html网页源代码中,都有“<title>标题内容</title>”这样的标题标签,用前面介绍的正则表达式方法,我们只能确定源码中的<title>和</title>是固定不变的。因此,如果想获取页面title标签中的“标题内容”,最简单的方法只能这样写代码:<title>.*</title>,而这个方法匹配出来的是一个完整的“<title>标题内容</title>”标签,并不是单纯的“标题内容”。想解决这个问题,就需要用到零宽断言的方法。

用法\语法 功能
?=exp 简称正向先行断言,它断言此位置的后面能匹配表达式“exp”,但不包含此位置,例如“a regular expression”这个字符串,要想匹配“regular”中的“re”,但不能匹配“expression”中的“re”,可以用”re(?=gular)”,该表达式限定了“re”右边的位置,这个位置之后是“gular”,但并不想要“gular”这些字符。
?<=exp 简称正向后发断言,它断言此位置的前面能匹配表达式“exp”,例如“regex represents regular expression”这个字符串里面有4个单词,要想匹配某个单词里面的“re”,但不匹配某个单词开头的“re”,可以用“(?<=\w)re”,该表达式规定在“re”前面必须有单词字符。
?!exp 简称反向先行断言,它断言此位置的后面不能匹配表达式“exp”,例如“regular expression”这个字符串,要想匹配除“regular”之外的“re”,可以用“re(?!g)”,该表达式限定了“re”右边的位置,这个位置后面不能有字母“g”,这里的感叹号“!”就是表示非,不需要的意思。
?<!exp 简称反向后发断言,它断言此位置的前面不能匹配表达式“exp”,例如“regex represents regular expression”这个字符串,要想匹配某个单词开头的“re”,可以用“(?<!\w)re”。该表达式规定在“re”前面不能有单词字符。感叹号“!”表示非,不需要的意思,当然也可以用”\bre”来匹配。

如果不用零宽断言,普通的正则表达式,仅仅能获取到有规律的字符串,而不能获取无规律的字符串。例如:

动手练一练:

import re

a = "<title>标题内容</title>"

#匹配<title>标签里面的“标题内容”
b = re.compile(r"(?<=<title>).*(?=</title>)")
result = b.search(a)
print(result.group()) # 输出 标题内容

执行以上代码,输出结果为:

标题内容

上面的例子中,我们想要的是“标题内容”,它没有规律,但是它前边肯定会有<title>标签,后边肯定会有</title>标签,这就足够了。“标题内容”前肯定会出现<title>,就用正向后发断言,表达式:“(?<=<title>)”。“标题内容”后边肯定会出现</title>,就用正向先行断言,表达式:“(?=</title>)”。

下面是一些简单的示例:

动手练一练:

import re

# 前面必须是“abc”才能匹配后面写的内容
print(re.search(r"(?<=abc)\d+", "abc123xyz").group())  # 输出: 123

# 后面必须是“xyz”才能匹配前面写的内容
print(re.search(r"(\d+(?=xyz))", "abc123xyz").group())  # 输出: 123

# 前面必须是“abc”,后面必须是“xyz”才能匹配之间写的内容
print(re.search(r"(?<=abc)\d+(?=xyz)", "abc123xyz").group())  # 输出: 123

# 前面不是“xyz”才能匹配后面写的内容
print(re.search(r"(?<!xyz)\d+", "abc123xyz").group())  # 输出: 123

# 前面不是“xyz”才能匹配后面写的内容,利用“?”限制贪婪
print(re.search(r"(?<!xyz)\d+?", "abc123xyz").group())  # 输出: 1

# 后面不是“abc”才能匹配后面写的内容
print(re.search(r"\d+(?!abc)", "abc123xyz").group())  # 输出: 123

执行以上代码,输出结果为:

123
123
123
123
1
123