前言
很多人用re模块只用到一个函数,随手写re.search(pat, s)就完事。这在小脚本里没问题,但当模式要反复用、还想带上注释和命名分组时,「编译一次、复用多次」的流程会清晰得多。本文把re模块的完整使用链路走一遍:导入、编译、匹配、读取结果、替换与切分、收尾清理。
需要先纠正一个常见误解:「必须用re.compile()才高效」这种说法并不绝对。官方文档明确写着,最近使用过的模式会被缓存,所以只在少量正则上跑的程序不必操心编译问题。真正需要显式编译的场景是:同一个模式要在循环里用成百上千次、或者你想把模式、标志位、命名分组这些信息集中在一处维护、或者想让代码读起来「一个模式一个对象」。
还有一点要提醒:本文讲的是 Python 3 的re。Python 2 已于 2020 年 1 月 1 日停止维护,请不要再参考iteritems、print语句那一套旧写法。
一、完整流程的五步
re模块的典型生命周期可以拆成五步:
导入 → 编译(re.compile) → 匹配(match/search/finditer) → 消费结果(group 等) → 收尾(可选 re.purge)其中第二步是可选的。下面这张表给出手写编译与直接用模块级函数的关系:
| 项目 | 模块级函数 | 编译后的 Pattern |
|---|
| 编译 | 内部自动完成 | re.compile(pattern, flags=0) |
| 匹配入口 | re.search(pat, s, flags) | p.search(s[, pos[, endpos]]) |
| 标志位位置 | 第三个参数 | 编译时固定 |
| 次数控制 | 无 | search/match可传pos、endpos |
| 复用 | 每次调用传入模式 | 一次编译,多次调用 |
| 缓存 | 有(最近的模式) | 不适用 |
re.compile()的签名是re.compile(pattern, flags=0),返回一个Pattern对象。它的方法里,search(string[, pos[, endpos]])允许你从字符串中间某个下标开始找,endpos则限定搜索上界,功能上接近「切片后再搜」,但^仍然认原始字符串的开头。
二、把日志行解析成字典
一个能体现「全过程」的实例是解析访问日志。用命名分组(?P<name>...)给每段起名字,匹配后用groupdict()一次性拿到字典:
# 适用于 Python 3.8+
import re
LOG = re.compile(
r"(?P<ip>\d{1,3}(?:\.\d{1,3}){3})\s+"
r"(?P<method>[A-Z]+)\s+"
r"(?P<path>\S*)\s+"
r"(?P<status>\d{3})\s+"
r"(?P<size>\d+)"
)
lines = [
"192.168.1.10 GET /index.html 200 5120",
"10.0.0.7 POST /api/login 401 128",
"这行不是日志",
]
for line in lines:
m = LOG.match(line)
if m is None: # 没匹配上时返回 None,必须先判断
continue
info = m.groupdict()
print(info["ip"], info["method"], info["status"])运行后前两行被解析,第三行因为match从开头就失败而被跳过。这里(?:\.\d{1,3}){3}用的是非捕获组(?:...):分组只用于配合量词{3},不需要单独取出,也就不在groupdict()里占位置。这是减少无谓分组、让结果更干净的小技巧。
如果需要逐段匹配位置,用finditer得到Match迭代器,再用span()定位:
# 适用于 Python 3.8+
import re
pat = re.compile(r"\d{3}-\d{4}")
s = "电话 010-1234 与 021-5678"
for m in pat.finditer(s):
print(m.group(), m.span())三、替换、切分与清理
Pattern.sub(repl, string, count=0)与模块级re.sub(pattern, repl, string, count=0)行为一致,只是模式已经固定在对象里;count=0表示全部替换。Pattern.split(string, maxsplit=0)同样对应re.split。
# 适用于 Python 3.8+
import re
phone = re.compile(r"(\d{3})-(\d{4})")
print(phone.sub(r"\1* \2", "010-1234")) # 010* 1234
words = re.compile(r"\W+")
print(words.split("Hello, world; python")) # ['Hello', 'world', 'python']最后一个环节是可选的清理。re.purge()会清空正则缓存,签名是re.purge(),无参数、返回None。大多数程序根本不需要调用它,它主要给「在长时间运行的进程里动态生成大量不同模式」这类场景用。缓存容量是 CPython 的实现细节,不要写代码去依赖某个具体数字,需要释放就用公开的re.purge()。
四、标志位怎么传
flags用按位或|组合。编译时一次性写清,比每次调用都传更不容易漏:
# 适用于 Python 3.8+
import re
pat = re.compile(r"^error", re.IGNORECASE | re.MULTILINE)
text = "Error: 磁盘满\nerror: 内存不足\nok"
print(len(pat.findall(text))) # 2re.MULTILINE(也可写成行内(?m))让^除了匹配字符串开头,也匹配每一行的开头;re.IGNORECASE((?i))忽略大小写。常见标志位一览:
| 标志 | 简写 | 作用 |
|---|
re.IGNORECASE | re.I/(?i) | 忽略大小写 |
re.MULTILINE | re.M/(?m) | ^、$按行生效 |
re.DOTALL | re.S/(?s) | 让.也匹配换行 |
re.VERBOSE | re.X/(?x) | 忽略模式中的空白与#注释 |
re.ASCII | re.A | 让\w、\d、\s只认 ASCII |
re.VERBOSE适合写长模式,可以把上面那条日志正则拆成带注释的多行,而re.ASCII在需要精确对齐[0-9]语义时很有用——默认的 Unicode 模式里\d会匹配全角等非 ASCII 数字。
常见坑点
- ❌ 不判断返回值直接取分组:
re.search(r"\d+", "abc").group(),文本里没有数字时抛AttributeError。
✅ 先判断m = re.search(...),if m:之后再访问group()。Match对象恒为真、None恒为假。
- ❌ 以为
p.match(s)和p.search(s)一样,从中间开始的行数据永远匹配不到。
✅match只从下标 0 起匹配;要任意位置用search,要整串用fullmatch。
- ❌ 把标志位当第二个参数传给模块级函数:
re.search(r"a", re.I),把标志位当成了被搜索的字符串。
✅re.search(pattern, string, flags)的顺序固定;不确定时改用re.compile(pattern, flags)。
- ❌ 对
Pattern.split()传flags:pat.split(s, re.I),第二参数其实是maxsplit。
✅ 标志位只能在re.compile()时给;Pattern.split(string, maxsplit=0)没有flags。
- ❌ 用
findall取带命名分组的结果,却拿到一堆元组或单列字符串,搞不清结构。
✅ 规则是:无分组返回整段、单分组返回该组、多分组返回元组;要按名字取值就用finditer+groupdict()。
- ❌ 在循环里对同一模式反复调用
re.compile(),或反过来为「保险」每次调re.purge(),反而丢掉缓存。
✅ 模式固定时编译一次放到模块级或函数外;re.purge()只在确实需要释放缓存时调用。
- ❌ 认为「某个组一定参与了匹配」,直接拿
m.group("optional")去做字符串拼接,得到None拼出的TypeError。
✅ 可选分组没参与匹配时值为None;用m.groups(default="")或m.groupdict(default="")给默认值。
- ❌ 在 Unicode 文本上指望
\d只匹配0-9,遇到全角或阿拉伯-印度数字时数量对不上。
✅ 需要严格 ASCII 语义就加re.ASCII,或干脆把模式写成[0-9]。
总结
| 步骤 | 关键 API | 注意 |
|---|
| 编译 | re.compile(pattern, flags=0) | 固定模式建议复用 |
| 匹配 | search/match/finditer | None必须先判断 |
| 取结果 | group/groups/groupdict/span | 未参与匹配的组为None |
| 替换切分 | sub/subn/split | count、maxsplit默认 0 |
| 清理 | re.purge() | 可选,容量是实现细节 |
把流程固定成「编译一份、匹配到处用、结果按名取」,re模块的代码会比到处散落的模块级调用好维护得多。至于缓存的容量、每次调用的耗时,都受实现和版本影响,需要时才用timeit在自己的数据上量,别套用别人给的固定倍数。