正则表达式
00:00
Python正则表达式re模块详解:模式语法、匹配方法、分组、替换与实战应用。
1. re 模块基础
1.1 基本匹配
import re
# re.search: 搜索第一个匹配(返回Match对象或None)
match = re.search(r'\d+', 'abc123def')
if match:
print(f"匹配: {match.group()}") # 123
print(f"位置: {match.start()}-{match.end()}") # 3-6
# re.match: 从字符串开头匹配
match = re.match(r'Hello', 'Hello World')
print(match.group() if match else "No match") # Hello
match = re.match(r'World', 'Hello World')
print(match) # None(不在开头)
# re.fullmatch: 完全匹配整个字符串
match = re.fullmatch(r'\d{3}', '123')
print(bool(match)) # True
match = re.fullmatch(r'\d{3}', '1234')
print(bool(match)) # False
# re.findall: 找到所有匹配,返回列表
numbers = re.findall(r'\d+', 'a1b22c333')
print(numbers) # ['1', '22', '333']
# re.finditer: 找到所有匹配,返回迭代器
for m in re.finditer(r'\d+', 'a1b22c333'):
print(f"Match: {m.group()} at {m.start()}")
# Match: 1 at 1
# Match: 22 at 3
# Match: 333 at 6
1.2 替换与分割
import re
# re.sub: 替换
result = re.sub(r'\d+', 'NUM', 'a1b22c333')
print(result) # aNUMbNUMcNUM
# 使用回调函数替换
result = re.sub(r'\d+', lambda m: str(int(m.group()) * 2), 'a1b22c333')
print(result) # a2b44c666
# 限制替换次数
result = re.sub(r'\d+', 'NUM', 'a1b22c333', count=2)
print(result) # aNUMbNUMc333
# re.split: 分割
parts = re.split(r'[,;|]', 'one,two;three|four')
print(parts) # ['one', 'two', 'three', 'four']
# 保留分隔符(使用捕获组)
parts = re.split(r'([,;|])', 'one,two;three|four')
print(parts) # ['one', ',', 'two', ';', 'three', '|', 'four']
2. 模式语法
2.1 字符类与量词
import re
# 字符类
print(re.findall(r'[aeiou]', 'Hello World')) # ['e', 'o', 'o']
print(re.findall(r'[^aeiou\s]', 'Hello World')) # ['H', 'l', 'l', 'W', 'r', 'l', 'd']
# 预定义字符类
# \d 数字 \D 非数字
# \w 单词字符 \W 非单词字符
# \s 空白 \S 非空白
# \b 单词边界
# 量词
print(re.findall(r'a{2,4}', 'a aa aaa aaaa aaaaa'))
# ['aa', 'aaa', 'aaaa', 'aaaa']
# 贪婪 vs 非贪婪
html = '<div>content1</div><div>content2</div>'
greedy = re.findall(r'<div>.*</div>', html)
print(greedy) # ['<div>content1</div><div>content2</div>'](贪婪)
non_greedy = re.findall(r'<div>.*?</div>', html)
print(non_greedy) # ['<div>content1</div>', '<div>content2</div>'](非贪婪)
2.2 分组与捕获
import re
# 基本分组
match = re.search(r'(\d{4})-(\d{2})-(\d{2})', 'Date: 2026-06-13')
if match:
print(match.group()) # 2026-06-13(完整匹配)
print(match.group(1)) # 2026(第一个分组)
print(match.group(2)) # 06
print(match.group(3)) # 13
print(match.groups()) # ('2026', '06', '13')
# 命名分组
match = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-06-13')
if match:
print(match.group('year')) # 2026
print(match.group('month')) # 06
print(match.groupdict()) # {'year': '2026', 'month': '06', 'day': '13'}
# 非捕获分组
result = re.findall(r'(?:https?://)?(\w+\.\w+)', 'Visit example.com or https://google.com')
print(result) # ['example.com', 'google.com']
# 前瞻断言
# (?=...) 正向前瞻:后面匹配
# (?!...) 负向前瞻:后面不匹配
prices = re.findall(r'\d+(?=元)', '苹果5元,香蕉3元,橙子4斤')
print(prices) # ['5', '3']
# 后顾断言(Python支持)
names = re.findall(r'(?<=用户:)\w+', '用户:Alice,用户:Bob')
print(names) # ['Alice', 'Bob']
3. 编译标志
import re
# re.IGNORECASE (re.I): 忽略大小写
print(re.findall(r'hello', 'Hello HELLO hello', re.I))
# ['Hello', 'HELLO', 'hello']
# re.MULTILINE (re.M): 多行模式(^和$匹配行首行尾)
text = "Line 1\nLine 2\nLine 3"
print(re.findall(r'^Line', text, re.M)) # ['Line', 'Line', 'Line']
# re.DOTALL (re.S): . 匹配换行符
text = "Start\nMiddle\nEnd"
print(re.findall(r'Start.*End', text, re.S)) # ['Start\nMiddle\nEnd']
# re.VERBOSE (re.X): 允许注释和空白
pattern = re.compile(r"""
^ # 字符串开头
(?P<protocol>\w+) # 协议名
:// # 分隔符
(?P<host>[\w.]+) # 主机名
(?::(?P<port>\d+))? # 可选端口
(?P<path>/.*)? # 可选路径
$ # 字符串结尾
""", re.VERBOSE)
match = pattern.match('https://example.com:8080/path')
if match:
print(match.groupdict())
# {'protocol': 'https', 'host': 'example.com', 'port': '8080', 'path': '/path'}
4. 预编译正则
import re
# 编译正则表达式(提升重复使用的性能)
email_pattern = re.compile(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
)
# 使用编译后的对象
emails = ['user@example.com', 'invalid-email', 'test@test.org']
valid_emails = [e for e in emails if email_pattern.match(e)]
print(valid_emails) # ['user@example.com', 'test@test.org']
# 编译时指定标志
phone_pattern = re.compile(r'^1[3-9]\d{9}$')
print(phone_pattern.match('13800138000')) # Match
5. 实战应用
5.1 数据提取与清洗
import re
# 提取URL
text = 'Visit https://example.com and http://test.org for info.'
urls = re.findall(r'https?://[^\s<>"\']+', text)
print(urls) # ['https://example.com', 'http://test.org']
# 清理HTML标签
html = '<p>Hello <b>World</b></p>'
clean = re.sub(r'<[^>]+>', '', html)
print(clean) # Hello World
# 提取中文
text = 'Hello世界,Python编程123'
chinese = re.findall(r'[\u4e00-\u9fa5]+', text)
print(chinese) # ['世界', '编程']
# 千分位格式化
def format_number(n):
return re.sub(r'(?<=\d)(?=(\d{3})+$)', ',', str(n))
print(format_number(1234567890)) # 1,234,567,890
# 驼峰转下划线
def camel_to_snake(name):
s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
print(camel_to_snake('myVariableName')) # my_variable_name
# 密码强度检测
def check_password_strength(password):
checks = {
'length': len(password) >= 8,
'lower': bool(re.search(r'[a-z]', password)),
'upper': bool(re.search(r'[A-Z]', password)),
'digit': bool(re.search(r'\d', password)),
'special': bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password)),
}
score = sum(checks.values())
return score, checks
score, details = check_password_strength('MyP@ss123')
print(f"强度分数: {score}/5, 详情: {details}")
5.2 日志解析
import re
from collections import defaultdict
# Apache日志格式解析
log_pattern = re.compile(
r'(?P<ip>\d+\.\d+\.\d+\.\d+)'
r' - - '
r'\[(?P<datetime>[^\]]+)\]'
r' "(?P<method>\w+) (?P<path>\S+) HTTP/\d\.\d"'
r' (?P<status>\d{3})'
r' (?P<size>\d+)'
)
def parse_log_file(filepath):
ip_counts = defaultdict(int)
status_counts = defaultdict(int)
with open(filepath, 'r') as f:
for line in f:
match = log_pattern.match(line)
if match:
ip_counts[match.group('ip')] += 1
status_counts[match.group('status')] += 1
return dict(ip_counts), dict(status_counts)
# 示例日志行
log_line = '192.168.1.1 - - [13/Jun/2026:14:30:00 +0800] "GET /api/users HTTP/1.1" 200 1234'
match = log_pattern.match(log_line)
if match:
print(match.groupdict())
6. 常见问题与解决方案
6.1 正则中的反斜杠
# Python原始字符串(r前缀)避免双重转义
# 匹配反斜杠
print(re.findall(r'\\', r'a\b\c')) # ['\\', '\\']
# 不用r前缀需要双重转义
print(re.findall('\\\\', 'a\\b\\c')) # ['\\', '\\']
# 始终使用r前缀!
6.2 性能优化
import re
# 1. 预编译常用正则
EMAIL_RE = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
# 2. 避免灾难性回溯
# 危险:嵌套量词
# bad = re.compile(r'(a+)+b')
# 3. 使用非捕获分组(稍微更快)
# (?:pattern) 代替 (pattern) 当不需要捕获时
# 4. 尽早使用锚定和具体字符
# ^\d{4}-\d{2}-\d{2}$ 比 .*日期.* 更高效
7. 总结与最佳实践
7.1 方法选择
| 需求 | 方法 | 返回值 |
|---|---|---|
| 检查是否匹配 | re.search / re.match | Match对象或None |
| 完全匹配 | re.fullmatch | Match对象或None |
| 找所有匹配 | re.findall | 列表 |
| 替换 | re.sub | 新字符串 |
| 分割 | re.split | 列表 |
7.2 最佳实践
- 使用原始字符串:
r'pattern'避免转义问题 - 预编译正则:重复使用的正则用
re.compile - 使用命名分组:
(?P<name>...)提高可读性 - VERBOSE模式:复杂正则用注释说明
- 非贪婪默认:需要精确匹配时用
*?+? - 测试正则:使用 regex101.com 等工具验证