Python 正则库 re:常用函数与 flags 详解
正则表达式写好了,但怎么用起来?Python 的 re 模块就是那个搭桥的。说白了一句话:模式写得再漂亮,调不对函数照样跑偏。这一节我把最常用的几个函数和 flags 掰开揉碎讲给你,每个都带上我踩过的坑。
四个主力函数:match、search、findall、sub
我习惯把这四个函数分成两对:查找对(match、search、findall)和替换对(sub)。先聊查找。
re.match() — 从头开始,不回头
match 只从字符串的起始位置匹配。如果开头对不上,直接返回 None。很多新手误以为 match 是全匹配,其实它是“掐头检查”。
import re
text = " 2024-01-15 敏感数据"
pattern = r"\d{4}-\d{2}-\d{2}"
result = re.match(pattern, text)
print(result) # None
# 因为开头是空格,不是数字
我在做日志解析时吃过这个亏。有一次要提取一行开头的时间戳,写了 match 死活匹配不上,debug 半天才发现日志行首有不可见空格。后来我就学乖了:只要不确定字符串开头,都用 search。
re.search() — 扫全串,找到第一个
search 扫描整个字符串,返回第一个匹配位置。说白了就是“大海捞针,捞到就停”。
result = re.search(pattern, text)
if result:
print(result.group()) # 输出 "2024-01-15"
你想想看,扫描敏感数据时,我们更关心“有没有”,而不是“从哪开始”。所以我推荐:默认用 search,除非你明确要求从头匹配。
re.findall() — 我要全部,一个不落
findall 返回所有匹配结果的列表。模式里如果有分组,它返回元组列表。这个函数在统计敏感数据出现次数时特别好使。
emails = re.findall(r"[\w.-]+@[\w.-]+\.\w+", long_text)
print(f"共发现 {len(emails)} 个邮箱")
注意:findall 如果包含分组,只返回分组捕获的内容。如果你想同时获取匹配整体和分组,用 finditer(稍后说)。
re.sub() — 替换的艺术
替换敏感数据最常用的就是 sub。它把匹配到的内容替换成你指定的字符串,还能传个计数参数。
# 手机号脱敏:保留前3后4,中间变星号
text = "联系方式:13812345678"
masked = re.sub(r"(\d{3})\d{4}(\d{4})", r"\1****\2", text)
print(masked) # "联系方式:138****5678"
我在做数据脱敏工具时,sub 配合回调函数可以做到动态替换——比如根据匹配到的身份证号长度决定保留几位。对,sub 第二个参数可以传函数!
re.compile() — 为什么我建议你每次都编译
compil 把正则模式预编译成 Pattern 对象。之后调用 match/search 等方法时,性能会好很多——尤其是同一个模式反复用的时候。
email_pat = re.compile(r"[\w.-]+@[\w.-]+\.\w+", re.IGNORECASE)
for line in huge_file:
if email_pat.search(line):
# 处理敏感行
pass
嗯,这里要注意:编译后的 Pattern 对象还能单独加 flags,不依赖全局。我曾经接手一个项目,全篇用字符串模式到处传参,后来统一改成 compile,代码清晰多了,debug 也快。
flags 详解:控制匹配行为的神器
flags(或叫编译标志)能改变正则的匹配方式。我把它分成三类讲:大小写、换行、其他优化。
| 标志 | 缩写 | 作用 | 我的使用场景 |
|---|---|---|---|
re.IGNORECASE | re.I | 忽略大小写 | 匹配邮件、姓名时不区分大小写 |
re.MULTILINE | re.M | 使 ^ 和 $ 匹配每行的开头/结尾 | 解析多行日志时非常有用 |
re.DOTALL | re.S | 让 . 匹配换行符 | 跨行匹配敏感数据块(如 XML/JSON) |
re.ASCII | re.A | 只匹配 ASCII 字符集 | 严格控制非英文字符时使用 |
re.LOCALE | re.L | 根据本地化设置匹配 | 已废弃,别用 |
re.UNICODE | re.U | 默认启用,匹配 Unicode 属性 | 几乎不用显式指定 |
re.VERBOSE | re.X | 允许注释和空白,增强可读性 | 复杂正则必加,不然自己都看不懂 |
进阶:finditer 和 groupdict
findall 返回列表,但如果你需要遍历大量匹配并且每次只处理一个,用 finditer 更省内存。它返回一个迭代器,每个元素是 Match 对象。
pat = re.compile(r"(?P<type>\w+): (?P<value>\S+)")
for m in pat.finditer(raw_data):
print(m.group("type"), m.group("value"))
搭配命名分组,写出来的代码像读说明书一样清楚。我自己的习惯:只要模式超出 5 个分组,一定用命名分组 + VERBOSE 模式。
知识体系总览
下面这张图把 re 模块的函数和 flags 串起来了。你可以把它贴在工位旁边,写正则的时候扫一眼。
- 写一次以上的正则 →
re.compile()再重用 - 不确定开头 → 用
search别用match - 需要全部匹配 →
findall或finditer - 脱敏替换 →
sub配合命名分组 - 复杂正则 → 一定要加
re.VERBOSE写注释
flags 组合使用的常见坑
多个 flags 可以用 | 组合。比如 re.IGNORECASE | re.DOTALL。但要注意顺序不影响结果。我看过有人写成 re.IGNORECASE | re.IGNORECASE,重复了但不会报错,就是多余。
另外一个坑: re.MULTILINE 只影响 ^ 和 $ 的行为。如果你用了 \A 和 \Z,它们仍然只匹配开头和结尾。我曾经写多行正则时,想匹配每行开头但用了 \A,结果只匹配了第一行。教训就是:多行模式一定用 ^ 和 $,别用 \A 和 \Z。
re.search(pattern, text, re.I | re.S),省得写多行代码。