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.IGNORECASEre.I忽略大小写匹配邮件、姓名时不区分大小写
re.MULTILINEre.M使 ^ 和 $ 匹配每行的开头/结尾解析多行日志时非常有用
re.DOTALLre.S让 . 匹配换行符跨行匹配敏感数据块(如 XML/JSON)
re.ASCIIre.A只匹配 ASCII 字符集严格控制非英文字符时使用
re.LOCALEre.L根据本地化设置匹配已废弃,别用
re.UNICODEre.U默认启用,匹配 Unicode 属性几乎不用显式指定
re.VERBOSEre.X允许注释和空白,增强可读性复杂正则必加,不然自己都看不懂
避坑:我曾经在匹配手机号时用了 re.IGNORECASE,虽然没有坏处但也没必要。反而有一次忘了加 re.ASCII,导致正则里的 \d 意外匹配了全角数字(比如 "12"),搞出了误报。所以处理纯英文/数字时,建议加上 re.A。

进阶: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 模块核心函数与 flags 关系 re 模块 re.compile() 直接调用(临时模式) Flags 参数 match() search() findall() sub() 常用 Flags I : IGNORECASE(忽略大小写) M : MULTILINE(多行模式) S : DOTALL(点匹配换行) X : VERBOSE(允许注释) finditer() 迭代器 Pattern 对象(复用 + 编译优化)
我的经验法则:
  • 写一次以上的正则 → 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。

小技巧:调试 flags 时,可以在 re.compile 里逐个测试,比如先加 re.I 看结果,再加 re.M 看差异。我经常在交互式环境里用 oneliner 试错:re.search(pattern, text, re.I | re.S),省得写多行代码。