☰
Python re模块的使用全过程
2026/10/8 13:02:27 网站建设 项目流程

前言


很多人用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))) # 2

re.MULTILINE(也可写成行内(?m))让^除了匹配字符串开头,也匹配每一行的开头;re.IGNORECASE((?i))忽略大小写。常见标志位一览:




标志简写作用



re.IGNORECASEre.I/(?i)忽略大小写

re.MULTILINEre.M/(?m)^、$按行生效

re.DOTALLre.S/(?s)让.也匹配换行

re.VERBOSEre.X/(?x)忽略模式中的空白与#注释

re.ASCIIre.A让\w、\d、\s只认 ASCII



re.VERBOSE适合写长模式,可以把上面那条日志正则拆成带注释的多行,而re.ASCII在需要精确对齐[0-9]语义时很有用——默认的 Unicode 模式里\d会匹配全角等非 ASCII 数字。


常见坑点



  1. ❌ 不判断返回值直接取分组:re.search(r"\d+", "abc").group(),文本里没有数字时抛AttributeError。


✅ 先判断m = re.search(...),if m:之后再访问group()。Match对象恒为真、None恒为假。



  1. ❌ 以为p.match(s)和p.search(s)一样,从中间开始的行数据永远匹配不到。


✅match只从下标 0 起匹配;要任意位置用search,要整串用fullmatch。



  1. ❌ 把标志位当第二个参数传给模块级函数:re.search(r"a", re.I),把标志位当成了被搜索的字符串。


✅re.search(pattern, string, flags)的顺序固定;不确定时改用re.compile(pattern, flags)。



  1. ❌ 对Pattern.split()传flags:pat.split(s, re.I),第二参数其实是maxsplit。


✅ 标志位只能在re.compile()时给;Pattern.split(string, maxsplit=0)没有flags。



  1. ❌ 用findall取带命名分组的结果,却拿到一堆元组或单列字符串,搞不清结构。


✅ 规则是:无分组返回整段、单分组返回该组、多分组返回元组;要按名字取值就用finditer+groupdict()。



  1. ❌ 在循环里对同一模式反复调用re.compile(),或反过来为「保险」每次调re.purge(),反而丢掉缓存。


✅ 模式固定时编译一次放到模块级或函数外;re.purge()只在确实需要释放缓存时调用。



  1. ❌ 认为「某个组一定参与了匹配」,直接拿m.group("optional")去做字符串拼接,得到None拼出的TypeError。


✅ 可选分组没参与匹配时值为None;用m.groups(default="")或m.groupdict(default="")给默认值。



  1. ❌ 在 Unicode 文本上指望\d只匹配0-9,遇到全角或阿拉伯-印度数字时数量对不上。


✅ 需要严格 ASCII 语义就加re.ASCII,或干脆把模式写成[0-9]。


总结




步骤关键 API注意



编译re.compile(pattern, flags=0)固定模式建议复用

匹配search/match/finditerNone必须先判断

取结果group/groups/groupdict/span未参与匹配的组为None

替换切分sub/subn/splitcount、maxsplit默认 0

清理re.purge()可选,容量是实现细节



把流程固定成「编译一份、匹配到处用、结果按名取」,re模块的代码会比到处散落的模块级调用好维护得多。至于缓存的容量、每次调用的耗时,都受实现和版本影响,需要时才用timeit在自己的数据上量,别套用别人给的固定倍数。





需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询