人人都会AI编程

15.3 贪婪与非贪婪匹配、捕获分组与非捕获分组

更新时间:2026-07-12

理解了正则的基本语法和 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>...) 让正则更容易读懂。

掌握了这些,你对正则的灵活度和控制力会明显提升一个台阶。