什么是捕获组
用圆括号 (...) 包裹的部分形成一个捕获组。括号有两个作用:
- 分组:让括号内的模式作为一个整体,可以对整体应用量词或选择
- 捕获:记录括号内匹配到的子字符串,以便后续提取或引用
捕获组(Capturing Group)
用
(...) 定义。引擎匹配时,会记录括号内匹配到的子字符串。每个捕获组按从左到右的顺序编号(从1开始)。可以在匹配后通过 .group(n)(Python)或 match[n](JavaScript)获取。命名捕获组(Named Capturing Group)
用
(?P<name>...)(Python)或 (?<name>...)(JavaScript/PCRE)定义,给捕获组起一个名字。通过名字而非编号访问,代码可读性更好。非捕获组(Non-Capturing Group)
用
(?:...) 定义。括号只起分组作用(用于量词或选择),不记录捕获内容。比捕获组性能略好,代码意图更清晰。反向引用(Backreference)
在正则内部用
\1、\2 等引用前面已捕获的内容,要求该位置的内容必须与捕获组捕获的内容完全相同。在替换字符串中也可以用反向引用重组文本。捕获组基础
/(\d{4})-(\d{2})-(\d{2})/
2026-03-26
group(1)="2026",group(2)="03",group(3)="26"
import re # 捕获组的基本使用 m = re.search(r'(\d{4})-(\d{2})-(\d{2})', 'today is 2026-03-26') m.group(0) # '2026-03-26' 整个匹配(等价于 m.group()) m.group(1) # '2026' 第1个捕获组(年) m.group(2) # '03' 第2个捕获组(月) m.group(3) # '26' 第3个捕获组(日) m.groups() # ('2026', '03', '26') 所有捕获组的元组 # 一次提取多个组 year, month, day = m.groups() print(f'{year}年{month}月{day}日') # '2026年03月26日'
命名捕获组
当有多个捕获组时,用数字编号容易混乱,命名组让代码更清晰:
# Python:(?P<name>...) m = re.search( r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', 'today is 2026-03-26' ) m.group('year') # '2026' m.group('month') # '03' m.group('day') # '26' m.groupdict() # {'year': '2026', 'month': '03', 'day': '26'} // JavaScript(ES2018+):(?<name>...) const m = '2026-03-26'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/) m.groups // { year: '2026', month: '03', day: '26' } m.groups.year // '2026'
findall 与捕获组的关系
Python 的 findall 在有捕获组时,返回捕获组内容而非整个匹配——这是常见的困惑来源:
# 无捕获组:返回整个匹配 re.findall(r'\d{4}-\d{2}-\d{2}', '2026-03-26 和 2025-12-31') # → ['2026-03-26', '2025-12-31'] # 有1个捕获组:返回该组的内容 re.findall(r'(\d{4})-\d{2}-\d{2}', '2026-03-26 和 2025-12-31') # → ['2026', '2025'] (只有年份!) # 有多个捕获组:返回元组 re.findall(r'(\d{4})-(\d{2})-(\d{2})', '2026-03-26 和 2025-12-31') # → [('2026', '03', '26'), ('2025', '12', '31')] # 如果想用捕获组提取部分但 findall 返回整体 # 方案1:用非捕获组 (?:...) 包裹不需要的部分 re.findall(r'(?:\d{4})-(\d{2})-(?:\d{2})', '2026-03-26') # → ['03'] (只有月份,因为只有一个捕获组) # 方案2:用 finditer 获取完整 Match 对象 for m in re.finditer(r'(\d{4})-(\d{2})-(\d{2})', '2026-03-26 和 2025-12-31'): print(m.group(0), m.group(1)) # 2026-03-26 2026 # 2025-12-31 2025
WARNING(findall 的捕获组陷阱)这是 Python 正则最常见的误区之一:添加了捕获组后,
findall 的返回值就变了。如果你本意是"用括号分组以便加量词",请使用非捕获组 (?:...),而不是普通捕获组 (...)。
非捕获组 (?:...)
只需要分组功能(如对整体加量词),不需要提取内容时,用非捕获组:
# ❌ 使用捕获组仅为了分组 re.findall(r'(ab)+', 'ab ababab') # → ['ab', 'ab'] (findall 返回捕获组内容,不是整个匹配!) # ✅ 使用非捕获组 re.findall(r'(?:ab)+', 'ab ababab') # → ['ab', 'ababab'] (正确!返回整个匹配) # 非捕获组用于选择 re.findall(r'(?:gray|grey)', 'color: gray or grey') # → ['gray', 'grey'] (findall 返回整个匹配) # 对比:捕获组用于选择,findall 返回组内容 re.findall(r'(gray|grey)', 'color: gray or grey') # → ['gray', 'grey'] (这里结果一样,因为组内容等于整个匹配)
反向引用(Backreference)
反向引用 \1、\2 等在正则内部引用已捕获的内容。这不是"引用模式",而是"引用捕获到的实际字符串"。
/(\w+)\s+\1/
the the the quick fox
\1 要求和第1组捕获的内容完全相同——检测重复单词
# 检测重复单词(常见用途) m = re.search(r'\b(\w+)\s+\1\b', 'the the quick brown fox') m.group() # 'the the' m.group(1) # 'the' # 匹配成对的 HTML 标签 m = re.search(r'<(\w+)>.*?</\1>', '<b>bold text</b>', re.DOTALL) m.group() # '<b>bold text</b>' # 匹配成对引号 m = re.search(r'(["\']).*?\1', '"hello" and \'world\'') m.group() # '"hello"' (开头和结尾引号类型必须相同) # 命名反向引用 re.sub(r'(?P<word>\w+) (?P=word)', r'\g<word>', 'hello hello world') # → 'hello world' (删除重复词)
在替换中使用反向引用
反向引用最强大的用途是文本重组——在 re.sub() 的替换字符串中重新排列捕获的内容:
# 日期格式转换:YYYY-MM-DD → MM/DD/YYYY re.sub( r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', # \1=年 \2=月 \3=日,重排顺序 'Date: 2026-03-26' ) # → 'Date: 03/26/2026' # 交换姓名:First Last → Last, First re.sub( r'(\w+)\s+(\w+)', r'\2, \1', 'John Smith Alice Johnson' ) # → 'Smith, John Johnson, Alice' # 为 Python 函数调用加上类型注解(示例性质) re.sub( r'def (\w+)\(self, (\w+)\)', r'def \1(self, \2: str)', 'def get(self, key)' ) # → 'def get(self, key: str)' // JavaScript 中的反向引用(用 $ 号) '2026-03-26'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1') // → '03/26/2026' // 命名引用 '2026-03-26'.replace( /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, '$<m>/$<d>/$<y>' ) // → '03/26/2026'
分组的嵌套与编号规则
嵌套捕获组的编号规则:按左括号从左到右,从1开始计数。
# 嵌套组示例 m = re.match(r'(a(b(c)d)e)', 'abcde') m.group(1) # 'abcde' ← 最外层的 (a(b(c)d)e) m.group(2) # 'bcd' ← 中间的 (b(c)d) m.group(3) # 'c' ← 最内层的 (c) # 量词重复时,捕获组只记录最后一次匹配 m = re.match(r'(\d)+', '1234') m.group(1) # '4' ← 只记录最后一次捕获! m.group(0) # '1234' ← 整个匹配 # 如果需要捕获整个重复序列,把量词放在括号外 m = re.match(r'(\d+)', '1234') m.group(1) # '1234' ← 正确捕获整个数字串
INFO(量词与捕获组)当捕获组带量词(如
(\d)+)时,每次重复都会覆盖捕获组的内容,最终只保留最后一次匹配的结果。如果你需要匹配"一个或多个数字"并捕获整个数字串,应该把量词放在括号内:(\d+) 而不是 (\d)+。
其他特殊分组语法
| 语法 | 名称 | 含义 |
|---|---|---|
(...) | 捕获组 | 捕获并编号,可通过 \1 等引用 |
(?:...) | 非捕获组 | 仅分组,不捕获,不占用编号 |
(?P<name>...) | 命名捕获组(Python) | 命名捕获,可通过 \g<name> 引用 |
(?<name>...) | 命名捕获组(JS/PCRE) | 命名捕获,可通过 $<name> 引用 |
(?=...) | 正向前瞻(见第4章) | 断言右侧必须匹配 |
(?!...) | 负向前瞻(见第4章) | 断言右侧不匹配 |
(?<=...) | 正向后顾(见第4章) | 断言左侧必须匹配 |
(?<!...) | 负向后顾(见第4章) | 断言左侧不匹配 |
实战:解析日志行
import re # 解析 Apache 访问日志的一行 LOG_LINE = '192.168.1.1 - - [26/Mar/2026:10:30:45 +0800] "GET /index.html HTTP/1.1" 200 1234' LOG_PATTERN = re.compile(r""" (?P<ip>\d+\.\d+\.\d+\.\d+) # IP 地址 \s+-\s+-\s+ # ident 和 authuser(通常为 -) \[(?P<time>[^\]]+)\] # 时间(方括号内) \s+"(?P<method>\w+) # HTTP 方法 \s+(?P<path>\S+) # 请求路径 \s+(?P<proto>HTTP/\S+)" # 协议 \s+(?P<status>\d{3}) # 状态码 \s+(?P<size>\d+|-) # 响应大小 """, re.VERBOSE) m = LOG_PATTERN.match(LOG_LINE) if m: info = m.groupdict() print(info['ip']) # '192.168.1.1' print(info['status']) # '200' print(info['path']) # '/index.html'
小结
本章要点
- 捕获组
(...)既分组又记录子匹配;通过group(n)或groups()获取 - 命名捕获组
(?P<name>...)(Python)/(?<name>...)(JS):用名字比用编号更清晰 - 非捕获组
(?:...):只分组不捕获,避免findall的行为变化,性能略好 - findall + 捕获组:有捕获组时返回捕获内容而非整个匹配——常见陷阱
- 反向引用
\1\2(正则内部)/r'\1'(Python 替换)/$1(JS 替换) - 量词与捕获组组合时(
(\w)+),捕获组只保留最后一次匹配;把量词放括号内((\w+))才能捕获整体