你是不是也遇到过?
夜深人静,屏幕上的报错信息格外刺眼:
import re
text = “联系电话:138-0013-8000,备用:15800138000”
pattern = r”\d{11}”
result = re.findall(pattern, text)
print(result) # [‘15800138000’] ???
明明文本里躺着两个手机号,期望是双双落网,结果只抓到了一个。问题出在哪?很多时候,问题不在于数据,而在于我们写正则表达式的方式。

下面这几个技巧,能帮你把 Python 的 `re` 模块用到得心应手。
技巧一:别只会用 findall 了,试试 finditer
当任务升级,不再满足于知道“有什么”,还必须知道“在哪里”时,`findall` 就显得力不从心了。
# 常见的 findall 用法:有内容,没位置
text = “错误发生在第 3 行,第 15 行也有问题,还有第 28 行”
matches = re.findall(r”第 (\d+) 行”, text)
print(matches) # [‘3’, ‘15’, ‘28’]
正确的打开方式应该是 `finditer`:
import re
text = “错误发生在第 3 行,第 15 行也有问题,还有第 28 行”
# finditer 返回一个迭代器,每个元素都包含着完整的匹配信息,包括位置
for match in re.finditer(r”第 (\d+) 行”, text):
print(f”行号 {match.group(1)},位置:{match.start()}-{match.end()}”)
# 输出:
# 行号 3,位置:5-10
# 行号 15,位置:11-17
# 行号 28,位置:24-30
这招在日志分析、代码高亮或者需要精确定位进行批量替换的场景下,尤其好用。
技巧二:命名分组,让正则自己文档化
维护过自己三个月前写的正则吗?看到 `match.group(1)`、`match.group(2)` 时,是不是得翻回去看pattern才能想起来哪个是年月日?
pattern = r”(\d{4})-(\d{2})-(\d{2})”
match = re.search(pattern, “2026-03-28”)
year = match.group(1) # 这到底是年,月,还是日?
month = match.group(2)
试试命名分组吧,它能赋予代码自解释的能力。
import re
# 使用 (?Ppattern) 语法给分组起个名字
pattern = r”(?P\d{4})-(?P\d{2})-(?P\d{2})”
match = re.search(pattern, “2026-03-28”)
print(match.group(“year”)) # 2026
print(match.group(“month”)) # 03
print(match.group(“day”)) # 28
# 更棒的是,可以直接拿到字典,处理批量数据时行云流水
dates = [
“2026-01-15”,
“2026-02-20”,
“2026-03-28”
]
for date in dates:
match = re.search(pattern, date)
if match:
print(match.groupdict())
# 输出:
# {‘year’: ‘2026’, ‘month’: ‘01’, ‘day’: ‘15’}
# {‘year’: ‘2026’, ‘month’: ‘02’, ‘day’: ‘20’}
# {‘year’: ‘2026’, ‘month’: ‘03’, ‘day’: ‘28’}
解析日志、配置文件、URL参数这类结构化数据时,命名分组堪称利器。
技巧三:预编译正则,性能提升 10 倍
如果在循环里反复使用同一个正则,千万别在每次循环中都去编译它。那相当于每次跑步前都现造一双鞋。
# 错误示范:每次循环都重新编译,效率低下
emails = [“test@example.com”] * 10000
for email in emails:
if re.match(r”^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$”, email):
pass # 处理逻辑
正确的姿势是预编译,一次编译,到处使用:
import re
# 预编译一次,后续直接使用
EMAIL_PATTERN = re.compile(
r”^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$”)
emails = [“test@example.com”] * 10000
for email in emails:
if EMAIL_PATTERN.match(email):
pass # 处理逻辑
性能提升立竿见影:未编译情况下,一万次匹配可能耗时约0.15秒;而使用预编译后,时间可以缩短到约0.015秒,效率提升一个数量级。
在循环处理海量数据、Web请求验证或需要实时过滤的场景中,这个习惯能显著优化性能。
技巧四:lookahead/lookbehind,匹配但不包含
想提取价格数字,但不想要前面的“¥”符号,怎么办?
text = “商品 A:¥199,商品 B:¥299,商品 C:¥399”
# 错误:会把¥一起匹配进来
re.findall(r”¥\d+”, text) # [‘¥199’, ‘¥299’, ‘¥399’]
# 一个笨办法:匹配后再手动切片
matches = re.findall(r”¥\d+”, text)
prices = [m[1:] for m in matches] # 额外增加了一步处理
其实有更优雅的解决方案——环视断言(lookaround assertions):
import re
text = “商品 A:¥199,商品 B:¥299,商品 C:¥399,商品 Z:1024”
# 正向后顾 (?<=¥):匹配前面有‘¥’的数字,但‘¥’本身不包含在结果里
prices = re.findall(r”(?<=¥)\d+”, text)
print(prices) # [‘199′, ‘299’, ‘399’]
# 正向前瞻 (?=元):匹配后面有‘元’的数字,但‘元’不包含在结果里
text2 = “价格100元,折扣后80元,会员折扣后60”
numbers = re.findall(r”\d+(?=元)”, text2)
print(numbers) # [‘100′, ‘80’]
进阶玩法可以用于密码强度验证这类复杂条件匹配:
# 密码必须包含:至少1个大写字母、1个数字,且长度在8-20位之间
pattern = r”^(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,20}$”
passwords = [“aBc12345”, “abc12345”, “ABC123”, “Abc12345678901234567890”]
for pwd in passwords:
if re.match(pattern, pwd):
print(f”{pwd}: ? 有效”)
else:
print(f”{pwd}: ? 无效”)

可以看到,”abc12345″因缺少大写字母而无效,”ABC123″则因为长度不足被拒之门外。
数据清洗、格式验证或需要组合多个条件进行匹配时,环视断言能派上大用场。
技巧五:sub 的回调函数,动态替换
想把文本里所有匹配到的数字都翻倍,但每个数字都不一样,固定替换行不通。
text = “价格:100 元,折扣:50 元,运费:20 元”
# 普通替换只能换成固定值
re.sub(r”\d+”, “XXX”, text) # 价格:XXX 元,折扣:XXX 元,运费:XXX 元
# 这显然不是我们想要的效果
这时候,`re.sub()` 支持传入回调函数的特性就大放异彩了:
import re
text = “价格:100 元,折扣:50 元,运费:20 元”
# 定义一个函数,对每个匹配项进行动态处理
def double_price(match):
num = int(match.group())
return str(num * 2)
result = re.sub(r”\d+”, double_price, text)
print(result) # 价格:200 元,折扣:100 元,运费:40 元
实战中,这个技巧常用于数据脱敏:
import re
def mask_phone(match):
“”“将手机号中间4位打码”“”
phone = match.group()
return phone[:3] + “****” + phone[7:]
text = “联系人:张三 13800138000,李四 15800138000”
masked = re.sub(r”1[3-9]\d{9}”, mask_phone, text)
print(masked) # 联系人:张三 138****8000,李四 158****8000
无论是批量数据脱敏、动态格式化还是复杂的批量转换任务,利用回调函数的 `re.sub()` 都能轻松应对。
这些坑你踩过几个?
坑 1:忘记转义特殊字符
# 错误:在正则中,点 ‘.’ 默认匹配任意字符
text = “example.com”
re.findall(r”example.com”, text) # 这会匹配到 ‘exampleXcom’!
# 正确做法:使用 re.escape 自动转义
domain = “example.com”
pattern = re.escape(domain)
re.findall(pattern, text) # 精确匹配 ‘example.com’
坑 2:贪婪匹配惹的祸
text = “内容 1
内容 2
”
# 错误:贪婪匹配会一口气吞下尽可能多的字符
re.findall(r”.*
”, text) # 只得到一个结果!
# [‘内容 1
内容 2
’]
# 正确:使用非贪婪匹配 ‘.*?’
re.findall(r”.*?
”, text) # 得到两个独立结果
# [‘内容 1
’, ‘内容 2
’]
坑 3:忽略 re.DOTALL 和 re.MULTILINE 标志
text = “”“第一行
第二行
第三行”“”
# 默认情况下,点 ‘.’ 不匹配换行符
re.findall(r”第一行.*第三行”, text) # [] 匹配失败!
# 加上 re.DOTALL 标志,让 ‘.’ 匹配包括换行在内的所有字符
re.findall(r”第一行.*第三行”, text, re.DOTALL) # [‘第一行\n第二行\n第三行’]
一张表记住 re 的核心方法
几个常用的标志需要留意:
| 标志 | 简写 | 作用 |
|---|---|---|
| re.IGNORECASE | re.I | 忽略大小写 |
| re.DOTALL | re.S | 让 ‘.’ 匹配换行符 |
| re.MULTILINE | re.M | 让 ^ 和 $ 匹配每行的开头结尾 |
| re.VERBOSE | re.X | 允许正则表达式跨多行并添加注释 |
最后再说几句
需要明确的是,正则表达式的语法在不同编程语言中可能存在细微差别,本文所讨论的均特指 Python 中的正则实现。
说到底,正则表达式不是用来炫技的,而是解决实际问题的工具。在考虑使用它时,有几点原则值得反复琢磨:
- 能不用就不用 – 对于简单的字符串操作,内置的字符串方法往往更清晰、更高效。
- 一定要测试 – 尤其是边界情况和各种极端场景,这里是 bug 的高发地带。
- 用了就要注释 – 给复杂的正则加上简明注释。相信我,未来的你(甚至可能就是明天的你)会对此感激不尽。

