Python 正则:re 模块、findall/finditer/sub、raw string 的必要性
一切从 compile 开始
Python 的正则入口是标准库 re 模块。所有函数都有两个版本:re.search(pattern, s) 直接调用,或 re.compile(pattern) 编译后复用。同一个模式要在一处代码里用两次以上,就先 compile——既省重复编译开销,又给模式一个名字:
import re
ORDER_RE = re.compile(r"DD\d{12}")
text = "退款单 DD202410260018 已受理,原单号 DD202410260019"
print(ORDER_RE.findall(text))
# 输出:['DD202410260018', 'DD202410260019']
模块级常量加正则的组合(如 ORDER_RE)是 Python 社区的事实标准写法,一眼即知这是编译过的模式。
查找四兄弟:match/search/fullmatch/findall
四个查找函数的差异是 Python 正则面试的保留节目:
| 函数 | 从哪开始 | 匹配到哪 | 返回 |
|---|---|---|---|
match |
仅串首 | 尽量长 | Match 对象或 None |
search |
任意位置 | 第一个命中 | Match 对象或 None |
fullmatch |
仅串首 | 必须整串 | Match 对象或 None |
findall |
任意位置 | 所有命中 | 字符串/元组列表 |
finditer |
任意位置 | 所有命中 | Match 迭代器 |
校验场景请用 fullmatch(或 search 配 ^...$):re.fullmatch(r"1[3-9]\d{9}", "13812348000abc") 返回 None,自动挡住尾部多余字符。match 只锚头部、search 全文扫描、fullmatch 全串约束——三者语义差别要在写代码前想清楚。
import re
m = re.search(r"(\d{2}):(\d{2}):(\d{2})", "提交时间 14:32:07")
print(m.group(0), m.group(1), m.groups())
# 输出:14:32:07 14 ('14', '32', '07')
findall 的分组规则与 finditer 的补位
findall 遇到捕获组会改变返回结构:一个组返回组字符串列表,多个组返回元组列表,没有组返回整匹配列表。这条规则前面提过,这里给出它的"正确打开方式"——想同时拿整匹配和分组时,finditer 是更稳的选择:
import re
access = "203.0.113.7 GET /list 200\n198.51.100.3 POST /login 401"
for m in re.finditer(r"(\d{1,3}(?:\.\d{1,3}){3}) ([A-Z]+) (\S+) (\d{3})", access):
ip, method, path, code = m.groups()
if code == "401":
print("登录失败来源:", ip, path)
# 输出:登录失败来源: 198.51.100.3 /login
finditer 逐个吐 Match 对象,分组、位置(m.start())信息一个不少,处理大文件时还天然流式、不爆内存。
sub:替换与回调
re.sub 的替换串支持 \1 反向引用与 \g<name> 命名引用;更复杂的转换传一个函数,函数收到 Match、返回替换文本,还能顺带统计次数——subn 返回 (新文本, 替换次数):
import re
def mask(m):
s = m.group(0)
return s[:3] + "****" + s[7:]
new, n = re.subn(r"1[3-9]\d{9}", mask, "联系 13812348000 或 15900992107")
print(new, "| 替换了", n, "处")
# 输出:联系 138****8000 或 159****2107 | 替换了 2 处
日志脱敏、敏感词打码这类"匹配后要算一算"的需求,回调函数是唯一正路。
raw string:反斜杠的双倍快乐
Python 字符串自己会消化反斜杠:"\d" 里的 \d 不幸不是合法转义,侥幸原样保留,但 "\b" 会被解释成退格符(ASCII 8)——于是 "\b(\w+)\b" 这条正则在没加 r 前缀时是坏的,而且坏得很隐蔽。写正则必加 r 前缀是 Python 社区的铁律:
import re
print(re.findall(r"\bcat\b", "cat category")) # ['cat']
# 若写成 "\bcat\b",\b 变成退格符,结果为 [] 且难以察觉
只有模式里要匹配字面量反斜杠时才需要在 raw string 里写 \\,其余场景 r"..." 一把梭。审 code 时看到没有 r 的正则字符串,直接标红。
避坑提醒:Python 正则里最贵的两个"想当然"——一是以为
match等于"全串匹配"(它只锚头部,尾部照样漏),二是在循环里反复调用re.search字符串形式(每次都重新解析模式)。前者换成fullmatch,后者提前compile,一行改动各省一个 bug。
两个细节:split 的捕获组与 re.escape
re.split 若在分隔模式里放了捕获组,分隔符本身会保留在结果里——既是特性也是坑:
import re
print(re.split(r"([\s,]+)", "a, b"))
# 输出:['a', ', ', 'b']
print(re.split(r"[\s,]+", "a, b,,c"))
# 输出:['a', 'b', 'c']
要保留分隔符(比如做语法高亮)就写捕获组,要纯净切分就去掉括号——一字之差,语义两样。
第二个细节是 re.escape:把任意字符串转义成安全的正则字面量。处理"用户输入当搜索词"的场景,它是你唯一的护身符:
import re
kw = "1+1=2"
print(re.findall(re.escape(kw), "请计算 1+1=2 对吗"))
# 输出:['1+1=2']
不加 re.escape,+ 和 = 会让这条搜索要么报错、要么误匹配。Python 侧凡是拼接用户输入进模式,一律先 escape,这条和 JS 章的手工转义规则等价,只是这里引擎替你做了。
一个冷门但好用的调试开关
Python 的 re 模块藏着一个官方调试器:编译时传 re.DEBUG,引擎会把模式的解析树打印出来——每个符号被解析成什么、量词作用到哪、组怎么编号,一目了然:
import re
re.compile(r"\d+px", re.DEBUG)
# 控制输出(节选):MAX_REPEAT 1 4294967295 / IN [CATEGORY_DIGIT] / LITERAL 112 / LITERAL 120
输出里的 LITERAL 112 是 p 的 ASCII 码、120 是 x——引擎眼里的模式是这副"骨架"模样。排查两个问题特别好使:量词到底绑定了谁(看 MAX_REPEAT 包住了哪个块),以及转义有没有生效(该是字面量的位置出现了 CATEGORY 就说明转义错了)。它不如 regex101 直观,但零依赖、离线可用,SSH 进服务器排查时是唯一趁手的工具。
参考资源
- Python re 模块官方文档(函数语义与 raw string 建议都出自这里):re — Regular expression operations
- Python 官方正则 HOWTO(含性能与常见误区):Regular Expression HOWTO
本章由 WorkBuddy 原创撰写,示例均经实际验证。
