摘要:正则表达式是文本处理的瑞士军刀,但很多人只会基础的匹配。本文通过12个实战案例,带你掌握分组、断言、贪婪/非贪婪、命名捕获等进阶技巧,让你的正则水平上一个台阶。
基础回顾
先快速过一遍常用语法:
import re
# 基础匹配
re.findall(r'\d+', 'price: 99, qty: 5') # ['99', '5']
re.findall(r'[a-zA-Z]+', 'Hello World 123') # ['Hello', 'World']
re.sub(r'\s+', ' ', 'too many spaces') # 'too many spaces'
这些大家都会。下面进入正题。
案例1:提取嵌套括号内容
text = '函数调用: func(arg1, func2(arg2, arg3), arg4)'
# 错误做法:贪婪匹配会吃掉太多
re.findall(r'\(.*\)', text) # ['(arg1, func2(arg2, arg3), arg4)']
# 非贪婪匹配:最短匹配
re.findall(r'\(.*?\)', text) # ['(arg1, func2(arg2, arg3)', '(arg2, arg3)']
# 正确做法:匹配不含括号的内容
re.findall(r'\([^()]*\)', text) # ['(arg2, arg3)']
关键点:[^()] 匹配除括号外的任意字符,这样就不会跨越括号边界。
案例2:命名捕获组
提取结构化数据时,命名捕获比数字索引可读性好得多:
log = '2026-02-23 08:15:32 [ERROR] Connection timeout: 192.168.1.100:3306'
pattern = r'(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) \[(?P<level>\w+)\] (?P<message>.+)'
match = re.match(pattern, log)
if match:
print(match.group('date')) # 2026-02-23
print(match.group('level')) # ERROR
print(match.group('message')) # Connection timeout: 192.168.1.100:3306
print(match.groupdict()) # {'date': '2026-02-23', 'time': '08:15:32', ...}
案例3:零宽断言(Lookahead/Lookbehind)
断言不消耗字符,只做位置判断:
# 前瞻断言:匹配后面跟着"元"的数字
text = '价格99元,数量5个,重量3kg'
re.findall(r'\d+(?=元)', text) # ['99']
# 否定前瞻:匹配后面不跟"元"的数字
re.findall(r'\d+(?!元)', text) # ['5', '3']
# 后顾断言:匹配前面是"¥"的数字
text = '商品A ¥199 商品B $299'
re.findall(r'(?<=¥)\d+', text) # ['199']
# 否定后顾:匹配前面不是"$"的数字
re.findall(r'(?<!\$)\d+', text) # ['199']
实际应用——提取密码强度验证:
def check_password(pwd):
"""密码必须包含大写、小写、数字,且长度>=8"""
pattern = r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$'
return bool(re.match(pattern, pwd))
check_password('Abc12345') # True
check_password('abc12345') # False(缺大写)
check_password('Abcdefgh') # False(缺数字)
案例4:条件替换
re.sub的替换参数可以是函数:
# 把所有数字翻倍
text = '苹果3个,香蕉5个,橙子12个'
result = re.sub(r'\d+', lambda m: str(int(m.group()) * 2), text)
print(result) # 苹果6个,香蕉10个,橙子24个
# 敏感词过滤:保留首尾字符
def mask_word(match):
word = match.group()
if len(word) <= 2:
return '*' * len(word)
return word[0] + '*' * (len(word) - 2) + word[-1]
sensitive = ['暴力', '赌博', '色情内容']
pattern = '|'.join(re.escape(w) for w in sensitive)
text = '这里有暴力和赌博以及色情内容'
print(re.sub(pattern, mask_word, text))
# 这里有暴*和赌*以及色**容
案例5:解析CSV(处理引号内的逗号)
# 简单split会被引号内的逗号干扰
line = '张三,"北京市,朝阳区",25,"爱好:读书,跑步"'
# 正则正确解析
pattern = r',(?=(?:[^"]*"[^"]*")*[^"]*$)'
fields = re.split(pattern, line)
print(fields)
# ['张三', '"北京市,朝阳区"', '25', '"爱好:读书,跑步"']
这个正则的意思是:匹配逗号,但要求逗号后面的引号数量是偶数(说明逗号不在引号内)。
案例6:提取URL的各个部分
url = 'https://user:pass@www.example.com:8080/path/to/page?key=value&foo=bar#section'
pattern = r'''
^(?P<scheme>https?):// # 协议
(?:(?P<user>[^:@]+) # 用户名
:(?P<password>[^@]+)@)? # 密码
(?P<host>[^/:]+) # 主机
(?::(?P<port>\d+))? # 端口
(?P<path>/[^?#]*)? # 路径
(?:\?(?P<query>[^#]*))? # 查询参数
(?:\#(?P<fragment>.*))?$ # 锚点
'''
match = re.match(pattern, url, re.VERBOSE)
if match:
for k, v in match.groupdict().items():
print(f'{k}: {v}')
re.VERBOSE标志允许在正则中加注释和空白,大幅提升可读性。
案例7:中文文本处理
text = '联系方式:手机13812345678,邮箱test@example.com,QQ:123456789'
# 提取手机号
phones = re.findall(r'1[3-9]\d{9}', text) # ['13812345678']
# 提取邮箱
emails = re.findall(r'[\w.+-]+@[\w-]+\.[\w.]+', text) # ['test@example.com']
# 提取中文
chinese = re.findall(r'[\u4e00-\u9fff]+', text) # ['联系方式', '手机', '邮箱']
# 去除所有非中文字符
clean = re.sub(r'[^\u4e00-\u9fff]', '', text) # '联系方式手机邮箱'
案例8:多行日志解析
log_text = """
[2026-02-23 08:00:01] INFO Server started on port 8080
[2026-02-23 08:00:15] ERROR Database connection failed
Traceback: ConnectionRefusedError
Host: 192.168.1.100
Port: 3306
[2026-02-23 08:00:16] INFO Retrying connection...
"""
# 匹配完整的日志条目(包括多行堆栈)
pattern = r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+)\s+(.+?)(?=\n\[|\Z)'
entries = re.findall(pattern, log_text, re.DOTALL)
for time, level, msg in entries:
print(f'[{level}] {time}: {msg.strip()[:50]}')
案例9:HTML标签处理
html = '<div class="content"><p>Hello <b>World</b></p></div>'
# 去除所有HTML标签
clean = re.sub(r'<[^>]+>', '', html) # 'Hello World'
# 提取所有链接
html2 = '<a href="https://a.com">Link1</a> <a href="https://b.com">Link2</a>'
links = re.findall(r'href="([^"]+)"', html2) # ['https://a.com', 'https://b.com']
# 提取带文本的链接
link_pairs = re.findall(r'<a[^>]+href="([^"]+)"[^>]*>([^<]+)</a>', html2)
# [('https://a.com', 'Link1'), ('https://b.com', 'Link2')]
案例10:数字格式化
# 千分位分隔
def add_commas(n):
return re.sub(r'(\d)(?=(\d{3})+(?!\d))', r'\1,', str(n))
add_commas(1234567890) # '1,234,567,890'
# 金额提取和标准化
text = '总价¥1,234.56元,优惠¥100元,实付1134.56'
amounts = re.findall(r'[¥¥]?([\d,]+\.?\d*)', text)
# ['1,234.56', '100', '1134.56']
性能优化
# 预编译:如果同一个正则要用多次
pattern = re.compile(r'\d{4}-\d{2}-\d{2}')
for line in huge_file:
match = pattern.search(line) # 比re.search()快
# 非捕获组:不需要提取的分组用(?:...)
# 慢:每个()都会创建捕获组
re.findall(r'(https?://)(\w+)(\.com)', text)
# 快:只捕获需要的部分
re.findall(r'(?:https?://)(\w+)(?:\.com)', text)
总结
正则表达式的进阶技巧:
- 命名捕获组让代码更可读
- 零宽断言解决"匹配但不消耗"的需求
re.VERBOSE让复杂正则可维护re.sub配合函数实现条件替换- 预编译提升性能
正则不是万能的(别用它解析HTML/JSON),但在文本处理场景下,它依然是最锋利的刀。