Python 正则表达式进阶:零宽断言、贪婪非贪婪、re 模块内核解析
Introduction
正则表达式是文本处理的瑞士军刀,但很多人只用到了皮毛。本文深入讲解零宽断言( lookahead/lookbehind)、正则引擎工作原理、Python re 模块内核函数、以及 re.DEBUG 调试技巧。
---
基础回顾
import re
text = '我的邮箱是 zhangsan@example.com,电话 13812345678,订单号 ORD2026001'
# search:查找第一个匹配
re.search(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}', text)
# findall:所有匹配
re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}', text)
# sub:替换
re.sub(r'd{11}', '***隐藏***', text)
零宽断言(核心难点)
零宽断言不匹配字符,只匹配位置。
# 正向前瞻 (?=...)
# 匹配后面跟着 "密码" 的 "设置"
re.findall(r'设置(?=密码)', '设置密码和设置用户名') # ['设置'](只匹配"设置",不匹配"密码")
# 负向前瞻 (?!...)
# 匹配不是数字开头的单词
re.findall(r'(?!d)w+', 'abc 123 xyz789') # ['abc', 'xyz']
# 正向后顾 (?<=...)
# 匹配 "密码" 前面的 "设置"(需要固定宽度)
re.findall(r'(?<=设置)密码', '设置密码和重置密码') # ['密码'](只匹配"密码",不匹配"设置")
# 负向后顾 (?<!...)
# 匹配不是 "已" 开头的标题内容
re.findall(r'(?<!已)w+', '已处理 未处理 已完成') # ['处理', '完成']
# 实战:密码强度验证(用前瞻)
def validate_password(pwd):
if not re.search(r'[A-Z]', pwd):
return '需要大写字母'
if not re.search(r'[a-z]', pwd):
return '需要小写字母'
if not re.search(r'd', pwd):
return '需要数字'
if not re.search(r'[!@#$%^&*]', pwd):
return '需要特殊字符'
if len(pwd) < 8:
return '至少8位'
return 'OK'
print(validate_password('Pass1234!')) # OK
贪婪 vs 非贪婪
text = '<div>内容1</div><div>内容2</div>'
# 贪婪:尽可能多匹配
re.search(r'<div>.*</div>', text).group() # 匹配整段(从第一个<div>到最后一个</div>)
# 非贪婪:尽可能少匹配(加 ?)
re.search(r'<div>.*?</div>', text).group() # 只匹配 <div>内容1</div>
# 实战:提取 HTML 标签内容
re.findall(r'<div>(.*?)</div>', text) # ['内容1', '内容2']
# 实战:提取 JSON 值(不能用 .*? 因为 JSON 可能嵌套)
import json
# JSON 用专门的解析器,不推荐正则
data = json.loads(text) # 正确做法
分组和命名分组
text = '订单号: ORD2026001, 金额: ¥299.50'
# 普通分组
m = re.search(r'订单号: (w+), 金额: ¥(d+.d{2})', text)
print(m.group(1), m.group(2)) # ORD2026001, 299.50
print(m.groups()) # ('ORD2026001', '299.50')
# 命名分组
m = re.search(r'订单号: (?P<order_id>w+), 金额: ¥(?P<amount>d+.d{2})', text)
print(m.group('order_id'), m.group('amount')) # ORD2026001, 299.50
# 反向引用(匹配重复内容)
re.search(r'(?P<word>w+) (?P=word)', 'hello hello world') # 匹配 "hello hello"
re.search(r'<(w+)>.*?</>', '<div>text</div>') # 匹配成对标签
re.DEBUG 调试
# 用 DEBUG 模式看正则表达式的解析树
print(re.compile(r'(?<=设置)密码', re.DEBUG))
# 输出:
# literal 112 # 'p' 的 ASCII 码
# literal 117
# ...
re.sub 的高级用法
# 用函数做替换
def mask_amount(m):
amount = float(m.group(1))
return f'¥{amount:.2f}'
re.sub(r'¥(d+.?d*)', mask_amount, '价格: ¥99 和 ¥199.5') # '价格: ¥99.00 和 ¥199.50'
# 条件替换
re.sub(r'(d+)(独角兽|企业)', r'企业', '3独角兽 5企业') # '企业3 5企业'
# 分割(比 str.split 更强大)
re.split(r'[,s]+', 'a,b,c d
e') # ['a', 'b', 'c', 'd', 'e']
re.split(r'(?<=w);', 'word1;word2;word3') # 保留分号在前面 ['word1;', 'word2;', 'word3']
常用正则模式速查
# 手机号(中国大陆)
r'1[3-9]d{9}'
# 邮箱
r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}'
# URL
r'https?://[a-zA-Z0-9.-]+(?:/[^s]*)?'
# 身份证(18位)
r'[1-9]d{5}(?:19|20)d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]d|3[01])d{3}[dXx]'
# IP 地址
r'(?:(?:25[0-5]|2[0-4]d|[01]?dd?).){3}(?:25[0-5]|2[0-4]d|[01]?dd?)'
# 日期(YYYY-MM-DD)
r'd{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]d|3[01])'
常见问题
Q1: 为什么正则匹配中文会出问题?Python 3 的 str 默认是 Unicode,用字符类 [a-zA-Z] 只能匹配 ASCII 字母。匹配中文用 [一-鿿] 或 Unicode 属性 p{L}(需要 regex 库)。
Q2: compile 有什么用?re.compile(pattern) 预编译正则表达式,在循环中重复使用同一个模式时效率更高。
Q3: 负向前顾 (?!) 和负向后顾 (?<!) 怎么理解?(?!x) = "此位置后面不是 x";(?<!x) = "此位置前面不是 x"。两者都是"排除后面/前面是特定模式的位置"。
延伸阅读
- regex 库(支持 Unicode 属性和递归正则)
- Python 文本清洗完整指南
- 正则表达式性能优化
---
作者:小马 | 绍大技术网 shaoda.net