2.35. 模式基础

正则表达式是一种小型语言,用于按照字符串的 形式 而非确切内容来描述字符串。当且仅当一个字符串遵循某种你能描述却无法一一列举的 模式 时它才匹配,这时就该使用正则表达式——例如“一串数字后跟一个单位”、“以 ERROR 开头并以数字结尾的一行”、“这些文件扩展名中的任意一个,顺序不限,可带可选的 v 前缀”。

只有 当普通字符串方法无法胜任时,才考虑使用 re

上述每一种方法都比等价的正则表达式更快、更易读、更不容易出错。当字符串的 形式 才是关键、而确切子串无关紧要时,再使用正则表达式。

2.35.1. 你将用到的四样东西

MicroPython 的 re 模块对外提供四样东西:

  • re.compile() —— 把一个模式字符串编译成可重复使用的已编译模式对象。

  • re.match() —— 在字符串的 开头 尝试匹配模式。模式被锚定在位置 0。

  • re.search() —— 在字符串的 任意位置 尝试匹配模式。返回第一处匹配。

  • re.sub() —— 找出每一处匹配并替换它。

与 CPython 相比的显著缺失:没有 re.findall、没有 re.finditer、模块级没有 re.split(已编译的模式带有一个 split 方法)、没有 re.fullmatch、没有像 re.IGNORECASE 这样的标志常量。凡是在 CPython 上你会用到其中之一的地方,请在循环中用 re.search() 来构建等价的功能。

2.35.2. 第一个模式

模式 r'\d+' 匹配一个或多个数字:

>>> import re
>>> m = re.search(r'\d+', 'sensor reading 42 ok')
>>> m.group(0)
'42'

有几点需要注意:

  • 该模式写成 原始字符串r'...'),这样 \d 中的反斜杠才能传到 re,而不会被当作 Python 字符串转义来处理。请始终对正则表达式模式使用原始字符串。

  • re.search() 成功时返回一个 匹配对象,失败时返回 None。在调用 match.group() 之前请务必检查。

  • m.group(0) 是模式匹配到的完整文本。一旦模式包含捕获括号,分组 1、2、…… 才会随后出现。

同样的模式配合 re.match() 会返回 None,因为该字符串并不以数字 开头:

>>> re.match(r'\d+', 'sensor reading 42 ok') is None
True
>>> re.match(r'\d+', '42 readings')
<match num=1>

2.35.3. 模式的各个组成部分

大多数有用的模式都是由一小组组成部分构建而成的。在 MicroPython 中可用的有:

字面字符 —— 任何非特殊字符都匹配它自身。hello 匹配 hello

特殊字符 —— . ^ $ * + ? { } [ ] \ | ( ) 都具有下文所述的含义。要按字面匹配其中之一,请用反斜杠对其转义:\. 匹配一个字面的点号。

字符类 —— 常见字符集的简写:

  • \d —— 任意数字 0-9

  • \D —— 任意非数字

  • \s —— 任意空白字符(空格、制表符、换行符)

  • \S —— 任意非空白字符

  • \w —— 任意“单词”字符:字母、数字、下划线

  • \W —— 任意非单词字符

  • . —— 除换行符外的任意字符

量词 —— 前一个组成部分必须匹配的次数:

  • * —— 零次或多次(贪婪)

  • + —— 一次或多次(贪婪)

  • ? —— 零次或一次

  • {n} —— 恰好 n

  • {m,n} —— 介于 mn 之间(含端点)

组合使用:\d{3}-\d{4} 匹配三个数字、一个连字符、四个数字。\s+ 匹配一个或多个空白字符。hello.*world 匹配 hello、任意内容(包括空内容),然后是 world

备注

贪婪 意味着量词会在仍能让模式其余部分匹配的前提下,尽可能多地消耗输入。对于 hello x world y worldhello.*world 中的 .* 会匹配仍能在末尾留下一个 world 的最长一段——它捕获的是 x world y,而不是较短的 x+ 以及 {m,n} 范围形式也是如此:引擎会取它能取到的最长匹配,仅当模式其余部分匹配失败时才回退。

2.35.4. 替换

re.sub() 找出每一处匹配并用一个字符串替换它。替换内容可以通过 \1\2、…… 引用被捕获的分组(连同其余分组语法将在后文介绍)。在没有分组的情况下,re.sub 就是对正则表达式进行直接的查找和替换:

>>> re.sub(r'\s+', ' ', 'too    many   spaces')
'too many spaces'

>>> re.sub(r'\d+', 'N', 'log 12, log 345, log 6')
'log N, log N, log N'

第三个参数是要操作的字符串;结果是一个把每一处匹配都替换掉的新字符串。

2.35.5. 拆分——仅对已编译的模式可用

模块级没有 re.split。要按正则表达式拆分,请先编译模式,再调用其 split 方法:

>>> sep = re.compile(r'\s*,\s*')
>>> sep.split('a , b,c ,  d')
['a', 'b', 'c', 'd']

可选的第二个参数限制拆分的次数:

>>> sep.split('a, b, c, d', 2)
['a', 'b', 'c, d']

2.35.6. 为重用而编译

如果同一个模式要运行许多次——在循环内或在热点函数中——请将它编译一次并重用该已编译对象:

digit_run = re.compile(r'\d+')

def first_number(line):
    m = digit_run.search(line)
    return int(m.group(0)) if m else None

在已编译对象上调用 pattern.match()pattern.search() 与模块级函数相同,但省去了每次调用时的重新编译开销。

2.35.7. 什么都匹配不到的模式

尤其有三种模式会让开发者栽跟头:

  • .* 匹配空字符串。对于任意输入,re.search(r'.*', s).group(0) 都会返回 ''

  • 含有未转义特殊字符的模式是一个语法错误。re.compile(r'cost: $5') 会引发 ValueError,因为 $ 表示“字符串结尾”。请使用 r'cost: \$5'

  • 点号 . 匹配换行符。要跨换行符匹配,请用 [\s\S] 显式地编写模式来处理换行符,或者每次只送入一行。

有了这些组成部分,一个模式几乎可以匹配任何 固定形式 的文本片段。而要从匹配中把结构化的 数据 提取出来,则需要捕获组。