2.35. 模式基础¶
正则表达式是一种小型语言,用于按照字符串的 形式 而非确切内容来描述字符串。当且仅当一个字符串遵循某种你能描述却无法一一列举的 模式 时它才匹配,这时就该使用正则表达式——例如“一串数字后跟一个单位”、“以 ERROR 开头并以数字结尾的一行”、“这些文件扩展名中的任意一个,顺序不限,可带可选的 v 前缀”。
只有 当普通字符串方法无法胜任时,才考虑使用 re。
str.startswith()、str.endswith()—— 测试固定的前缀或后缀。in—— 测试是否存在某个固定的子串。str.split()、str.find()、str.replace()—— 处理固定的分隔符。
上述每一种方法都比等价的正则表达式更快、更易读、更不容易出错。当字符串的 形式 才是关键、而确切子串无关紧要时,再使用正则表达式。
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}—— 介于 m 和 n 之间(含端点)
组合使用:\d{3}-\d{4} 匹配三个数字、一个连字符、四个数字。\s+ 匹配一个或多个空白字符。hello.*world 匹配 hello、任意内容(包括空内容),然后是 world。
备注
贪婪 意味着量词会在仍能让模式其余部分匹配的前提下,尽可能多地消耗输入。对于 hello x world y world,hello.*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]显式地编写模式来处理换行符,或者每次只送入一行。
有了这些组成部分,一个模式几乎可以匹配任何 固定形式 的文本片段。而要从匹配中把结构化的 数据 提取出来,则需要捕获组。