2.36. 分组与锚点¶
一个模式所能做的不仅仅是判断“这个字符串匹配”——它还能把匹配到的各个部分拆分出来,并按名称交给应用程序。在模式的某一部分外面加上括号,就使其成为一个 捕获组;匹配对象随后会把每个分组作为单独的子串暴露出来。
2.36.1. 捕获组¶
把模式的任意部分用 (...) 包裹起来,即可捕获它所匹配的内容:
>>> import re
>>> m = re.search(r'temp (\d+) at (\d+)s', 'temp 42 at 137s ok')
>>> m.group(0)
'temp 42 at 137s'
>>> m.group(1)
'42'
>>> m.group(2)
'137'
分组 0 始终是整个匹配结果。
分组 1、2、…… 是被捕获的子串,按其 左 括号从左到右编号。
用超过最后一个分组的索引调用
match.group()会引发IndexError。
一种常见的模式是“匹配某种已知结构,并把可变部分捕获为整数”:
def parse_temp(line):
m = re.search(r'temp (\d+) at (\d+)s', line)
if not m:
return None
return int(m.group(1)), int(m.group(2))
2.36.2. 非捕获组¶
括号也会把一个子表达式 分组,以便量词能够作用于整个分组。这正是 r'(ab)+' 中分组的唯一用途——“一个或多个 ab”。ab 同时作为分组 1 出现只是一个副作用。
若要分组但不捕获,请使用 (?:...):
>>> re.search(r'(?:ab)+', 'xababy').group(0)
'abab'
当模式仅出于结构目的使用分组、而并不关心把每个部分单独提取出来时,非捕获组能让分组编号保持整洁。
2.36.3. 锚点¶
锚点并不匹配某个字符——它匹配一个 位置。
^—— 字符串的开头。$—— 字符串的结尾。
锚点正是使 re.match() 和 re.search() 行为不同的原因。re.match(p, s) 等同于 re.search('^' + p, s):它强制模式从位置 0 开始匹配。在模式末尾再加上 $,就使模式匹配 整个 字符串而不匹配其他任何内容:
>>> re.search(r'^\d+$', '12345')
<match num=1>
>>> re.search(r'^\d+$', '12345 ok') is None
True
在 MicroPython 的 re 中,^ 和 $ 始终表示传入 re.search() 的 整个 字符串的开头和结尾。没有 re.MULTILINE 标志可以让它们在每一个内嵌换行符处匹配,$ 也不会匹配末尾 \n 之前的位置——它必须是输入的绝对末尾。要获得逐行的行为,请先在换行符处拆分输入,再对每一行运行模式。
2.36.4. 字符集¶
方括号定义了一个明确的字符集合。匹配会从该集合中恰好消耗一个字符。
[abc]——a、b、c中的一个。[a-z]——a-z范围内(含端点)的一个字符。[a-zA-Z0-9]—— 字母或数字。三个范围的组合。[^abc]—— 不是a、b、c中的任何一个。只有当^是方括号内的第一个字符时才表示取反。
示例:
>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest').group(0)
'1A2B3C'
>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest') is None
True
由于字面文本是小写的,第一次调用在实际中返回 None。MicroPython 的 re 没有 re.IGNORECASE 标志——要进行不区分大小写的匹配,请把两种大小写都写进字符集里:
>>> re.search(r'[A-Fa-f0-9]{6}', 'colour #1a2b3c rest').group(0)
'1a2b3c'
字符类简写(\d、\s、\w 及其取反形式)也可以用在 [...] 内部:[\w-] 表示“单词字符或一个字面的连字符”。
2.36.5. 贪婪量词与懒惰量词¶
量词 *、+、? 和 {m,n} 默认是 贪婪 的——它们会在模式的其余部分仍能匹配的前提下,尽可能多地匹配字符。这往往正是我们想要的;但有时并非如此:
>>> re.search(r'<(.+)>', 'a <b> <c> d').group(1)
'b> <c'
贪婪的 .+ 一直抓取到了最后一个 >。在其后追加 ? 会使量词变为 懒惰——它会尽可能少地匹配:
>>> re.search(r'<(.+?)>', 'a <b> <c> d').group(1)
'b'
懒惰形式在第一个 > 处就停止。当从字符串中提取成对的分隔符内容时,懒惰量词会经常用到。
2.36.6. 替换中的反向引用¶
re.sub() 可以在替换字符串中通过 \1、\2、…… 引用被捕获的分组。替换会用捕获到的各部分来重写每一处匹配:
>>> re.sub(r'(\d+)\.(\d+)', r'\2.\1', 'swap 12.34 and 5.6')
'swap 34.12 and 6.5'
每次匹配捕获两个数字,替换会把它们交换。\g<1> 是实现同样效果的另一种语法——当替换中紧随其后的字符是数字时很有用(用 r'\g<1>0' 在分组 1 后追加一个字面的零,而不是被读成“分组 10”)。
2.36.7. 哪些功能不可用¶
提醒一下 MicroPython 的 re 不 支持哪些功能,以免来自 CPython 的某个模式落到这里时让你措手不及:
前瞻
(?=...)和后顾(?<=...)—— 未实现。命名分组
(?P<name>...)和命名反向引用(?P=name)—— 未实现。诸如
re.IGNORECASE、re.MULTILINE、re.DOTALL之类的标志常量 —— 不予支持。请自行构建不区分大小写的字符集,或预先拆分输入。match.groups()、match.span()、match.start()和match.end()方法受限于一个没有任何已发布的 OpenMV 开发板启用的 ROM 级别。依赖它们的代码无法在摄像头上运行。
有了模式、分组和锚点,摄像头上的正则表达式工具集足够小,一次就能学会;也足够丰富,除了上下文相关的解析之外几乎无所不能。