理解了正则的基本语法和 re 模块的方法后,有两个概念会在实际使用中反复出现,而且容易踩坑:贪婪匹配和分组捕获。这一节就把它们彻底讲清楚。
贪婪与非贪婪匹配
默认情况下,正则中的量词(*、+、?、{m,n})都是贪婪的,意思是它们会尽可能多地匹配字符。而非贪婪(也叫懒惰)匹配则相反,会尽可能少地匹配。
- 贪婪匹配(默认行为)
以 .* 为例:它会一直吃到字符串末尾,然后根据需要“往回吐”一点,以满足后面的模式。
示例:
import re
text = "<h1>标题</h1><p>正文</p>"
pattern = r"<.*>"
print(re.findall(pattern, text)) # 输出:['<h1>标题</h1><p>正文</p>']
这里 <.*> 从第一个 < 开始,一路匹配到最后一个 >,把中间的标签和内容全吃掉了,这通常不是我们想要的。
- 非贪婪匹配(在量词后加
?)
同样的模式,改为 .*? 就成了非贪婪,它会尽量少匹配,遇到第一个满足条件的就停。
pattern = r"<.*?>"
print(re.findall(pattern, text)) # 输出:['<h1>', '</h1>', '<p>', '</p>']
现在就能正确匹配到每一个标签了。
- 常用非贪婪量词
在 *、+、?、{m,n} 后面加上 ? 即可:
*?:重复任意次,但尽可能少+?:重复至少一次,但尽可能少??:重复 0 次或 1 次,但尽可能少{m,n}?:重复 m 到 n 次,但尽可能少
- 实际使用原则
当你需要匹配“最短的、符合模式的子串”时,就用非贪婪模式。比如提取 HTML 标签属性、解析引号内的字符串等。
但也要注意:非贪婪可能导致效率变低,因为引擎需要在每个位置都尝试一次。不过在现代应用场景下,可读性远大于微小的性能差异,该用就用。
捕获分组与非捕获分组
分组 () 在正则中有两个作用:把一个子表达式当作整体进行运算(例如应用量词或选择分支),以及把匹配到的内容捕获出来。但有时候我们只需要分组的功能,不想单独捕获,这时就用到了非捕获分组。
- 捕获分组
(pattern)
被括号包起来的部分会被捕获为一个组,可以通过 group()、groups() 或 findall 获取。
import re
text = "张三,电话:13800138000,邮箱:zhangsan@example.com"
pattern = r"电话:(\d{11}),邮箱:([\w.]+@[\w.]+)"
m = re.search(pattern, text)
print(m.group(1)) # 输出:13800138000
print(m.group(2)) # 输出:zhangsan@example.com
如果使用 re.findall,且模式中包含捕获分组,那么返回的将是分组捕获内容的列表(若只有一个分组,则是这个分组内容的列表;多个分组则是元组列表)。
print(re.findall(pattern, text)) # 输出:[('13800138000', 'zhangsan@example.com')]
- 非捕获分组
(?:pattern)
在左括号后加上 ?:,这个分组就不作为捕获结果,它只用于组织正则结构,不影响后续引用和 findall 的输出结果。
# 只想匹配电话号码,但不单独捕获区号
text = "010-12345678"
pattern = r"(?:0\d{2,3})-(\d{7,8})" # 区号部分非捕获
m = re.search(pattern, text)
print(m.group(1)) # 输出:12345678(区号没有被捕获,所以只有一个组)
在实际中,当你有很多分组,但只关心其中某几个时,可以把不关心的部分标记为非捕获,让代码更清晰,也避免 groups() 返回一堆没用的空值。
- 命名分组
(?P<name>pattern)
还可以给分组取一个有意义的名称,方便提取和代码维护。
pattern = r"电话:(?P<phone>\d{11}),邮箱:(?P<email>[\w.]+@[\w.]+)"
m = re.search(pattern, text)
print(m.group('phone')) # 输出:13800138000
print(m.group('email')) # 输出:zhangsan@example.com
在复杂的正则中,命名分组能大幅提高可读性,推荐使用。
- 反向引用
捕获分组还可以在后面用 \1、\2 等引用之前匹配到的内容,用于匹配重复出现的模式,比如成对的 HTML 标签。
pattern = r"<(h[1-6])>(.*?)</\1>"
text = "<h2>标题</h2><h3>子标题</h3>"
print(re.findall(pattern, text)) # [('h2', '标题'), ('h3', '子标题')]
如果用了命名分组,可以用 (?P=name) 进行引用。
一句话总结
- 想匹配“最短”内容?在
*、+等量词后加?,开启非贪婪。 - 想提取特定部分?用捕获分组
();只想改变优先级但不想单独抓取?用非捕获分组(?:)。 - 分组多、逻辑复杂时,用命名分组
(?P<name>...)让正则更容易读懂。
掌握了这些,你对正则的灵活度和控制力会明显提升一个台阶。