Python 正则表达式:re 模块的高级用法
正则表达式是处理文本的利器,而 Python 的 re 模块提供了丰富的功能来应对各种复杂的匹配需求。本文将深入探讨 re 模块的高级用法,帮助你从「能用」进阶到「精通」。
一、编译标志:让匹配更灵活
默认情况下,正则表达式区分大小写且只匹配单行。编译标志可以改变这些行为,让表达式更符合实际需求。
常用标志速查
| 标志 | 简写 | 作用 |
|---|---|---|
re.IGNORECASE |
re.I |
忽略大小写 |
re.MULTILINE |
re.M |
让 ^ 和 `$` 匹配每行的开头和结尾 |
| `re.DOTALL` | `re.S` | 让 `.` 匹配包括换行符在内的任意字符 |
| `re.VERBOSE` | `re.X` | 允许在表达式中使用注释和空白 |
### 实际应用示例
```python
import re
text = """Hello world
Python is great
hello again"""
# 不使用标志 - 只能匹配第一行的 "hello"
pattern1 = r'^hello'
print(re.findall(pattern1, text)) # 输出: []
# 使用 MULTILINE 标志 - 每行开头的 hello 都能匹配
pattern2 = r'^hello'
print(re.findall(pattern2, text, re.MULTILINE)) # 输出: ['hello']
# 组合使用多个标志
pattern3 = r'^hello'
print(re.findall(pattern3, text, re.I | re.M)) # 输出: ['Hello', 'hello']
```
**使用 `re.VERBOSE` 编写可读性强的表达式**:
```python
# 传统写法 - 难以阅读
pattern1 = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
# VERBOSE 写法 - 带注释,易于维护
pattern2 = r'''
\b # 单词边界
[A-Za-z0-9._%+-]+ # 本地部分(用户名)
@ # @ 符号
[A-Za-z0-9.-]+ # 域名
\. # 点号
[A-Z|a-z]{2,} # 顶级域名(至少2个字母)
\b # 单词边界
'''
email = re.compile(pattern2, re.VERBOSE)
```
---
## 二、命名组与反向引用:用名字取代数字
在复杂表达式中,用数字引用组很快就会变得混乱。命名组让你可以用有意义的名称来标识捕获组。
### 命名组语法
```python
import re
text = "2024-05-15"
# 传统方式 - 用数字引用
pattern1 = r'(\d{4})-(\d{2})-(\d{2})'
match1 = re.match(pattern1, text)
print(match1.group(1)) # 输出: '2024'
print(match1.group(2)) # 输出: '05'
print(match1.group(3)) # 输出: '15'
# 命名方式 - 用名字引用
pattern2 = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
match2 = re.match(pattern2, text)
print(match2.group('year')) # 输出: '2024'
print(match2.group('month')) # 输出: '05'
print(match2.group('day')) # 输出: '15'
# 使用 groupdict() 获取所有命名组
print(match2.groupdict())
# 输出: {'year': '2024', 'month': '05', 'day': '15'}
```
### 反向引用的实际应用
**检测重复单词**:
```python
text = "This is a a test test case case"
# 匹配重复的单词(不区分大小写)
pattern = r'\b(?P<word>\w+)\s+(?P=word)\b'
matches = re.findall(pattern, text, re.IGNORECASE)
print(matches) # 输出: ['a', 'test', 'case']
# 获取位置信息
for match in re.finditer(pattern, text, re.IGNORECASE):
print(f"'{match.group()}' 在位置 {match.span()}")
```
**HTML 标签匹配与替换**:
```python
html = "<div>Hello</div><span>World</span>"
# 匹配成对的标签
pattern = r'(?P<tag>\w+)>(?P<content>.*?)</(?P=tag)>'
matches = re.finditer(pattern, html)
for match in matches:
print(f"标签: {match.group('tag')}, 内容: {match.group('content')}")
```
---
## 三、零宽断言:精准定位的神器
零宽断言是一类特殊的结构,它匹配一个位置但不消耗任何字符。它们是「前视」和「后视」的组合。
### 四种断言类型
| 类型 | 语法 | 作用 |
|------|------|------|
| 正向前视 | `(?=...)` | 后面必须是 ... |
| 负向前视 | `(?!...)` | 后面必须不是 ... |
| 正向后视 | `(?<=...)` | 前面必须是 ... |
| 负向后视 | `(?<!...)` | 前面必须不是 ... |
### 实际应用场景
**场景 1:提取跟在特定词汇后的数字**
```python
text = "产品A价格100元,产品B价格200元,产品C价格300元"
# 提取价格(数字前面的"价格"不包含在结果中)
pattern = r'价格(?P<price>\d+)'
prices = re.findall(pattern, text)
print(prices) # 输出: ['100', '200', '300']
```
**场景 2:排除特定模式的匹配**
```python
text = ["apple", "apples", "application", "banana", "apply"]
# 匹配以 "app" 开头但不是 "apple" 的单词
pattern = r'app(?!le)\w*'
matches = [w for w in text if re.match(pattern, w)]
print(matches) # 输出: ['application', 'apply']
```
**场景 3:密码强度验证**
```python
import re
def validate_password(password):
# 至少8位,必须包含数字
pattern = r'^(?=.*\d).{8,}$' |
return bool(re.match(pattern, password))
print(validate_password("abc123")) # False(长度不足)
print(validate_password("abcdefgh")) # False(没有数字)
print(validate_password("abc12345")) # True
**场景 4:特定分隔符提取**
```python
text = "用户ID:123|姓名:张三|年龄:30"
# 使用后视断言提取冒号后的值
pattern = r'(?<=:\s*)\d+|\w+'
result = re.findall(pattern, text)
print(result) # 输出: ['123', '张三', '30']
四、非捕获组与独立组:优化性能与逻辑
非捕获组 (?:...)
当你只需要分组但不需要捕获时,使用非捕获组可以节省内存并提高性能。
text = "2024-05-15 2025-06-20 2026-07-25"
# 捕获组会占用额外内存
pattern1 = r'(\d{4})-(\d{2})-(\d{2})'
for m in re.finditer(pattern1, text):
print(m.groups()) # 输出元组
# 非捕获组 - 只分组不捕获
pattern2 = r'(?:\d{4})-(?:\d{2})-(?:\d{2})'
for m in re.finditer(pattern2, text):
print(m.group()) # 只输出完整匹配
独立组 (?:...) 的区别
注意:Python 的 re 模块不支持原子组(atomic groups),但可以使用 (?>...) 语法(需要 regex 第三方库)。标准库中主要使用非捕获组来优化。
嵌套分组的性能优化
# 复杂日志解析示例
log = "2024-05-15 10:30:45 [INFO] 用户登录成功 [ID:1001]"
# 低效写法 - 过多捕获组
pattern_bad = r'(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s+\[(\w+)\]\s+(.+)\s+\[ID:(\d+)\]'
# 优化写法 - 只捕获需要的部分
pattern_good = r'(?:\d{4})-(?:\d{2})-(?:\d{2})\s+(?:\d{2}):(?:\d{2}):(?:\d{2})\s+\[(?P<level>\w+)\]\s+(?P<message>.+)\s+\[ID:(?P<id>\d+)\]'
match = re.match(pattern_good, log)
print(match.group('level')) # INFO
print(match.group('message')) # 用户登录成功
print(match.group('id')) # 1001
五、条件匹配:更复杂的逻辑
if 条件组
使用 (?(id)yes|no) 语法可以根据某个组是否匹配来选择不同的模式。
text1 = "IP地址: 192.168.1.1"
text2 = "域名: example.com"
# 如果存在引号,匹配引号内容;否则匹配非空白字符
pattern = r'(:\s*)(?(?=")(".*?"|.*?))'
for text in [text1, text2]:
match = re.search(pattern, text)
if match:
print(f"值: {match.group(2)}")
使用 | 实现条件分支
text = ["$100", "€50", "£75", "INVALID"]
# 匹配美元、欧元或英镑金额
pattern = r'^(?P<currency>\$|€|£)(?P<amount>\d+)$'
for t in text:
m = re.match(pattern, t)
if m:
print(f"{m.group('currency')} -> {m.group('amount')}")
else:
print(f"无法解析: {t}")
```
---
## 六、实用技巧与性能优化
### 1. 预编译正则表达式
如果你需要多次使用同一个表达式,**预编译**可以显著提升性能。
```python
import re
# 多次使用时 - 预编译
email_pattern = re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}')
emails = [
"test@example.com",
"user.name@domain.org",
"invalid-email"
]
for email in emails:
if email_pattern.match(email):
print(f"有效: {email}")
else:
print(f"无效: {email}")
```
### 2. 使用 `finditer` 处理大文本
```python
# 对于大文件,使用 finditer 迭代而不是 findall
with open('large_file.log', 'r') as f:
content = f.read()
pattern = re.compile(r'\bERROR\b.*$', re.MULTILINE)
# 逐行处理,节省内存
for match in pattern.finditer(content):
line_start = match.start()
line_end = content.find('\n', line_start)
print(match.group().strip())
3. 贪婪 vs 非贪婪匹配
text = "<div>第一层<div>第二层</div>剩余</div>"
# 贪婪匹配 - 匹配尽可能多
pattern_greedy = r'<div>.*</div>'
print(re.search(pattern_greedy, text).group())
# 输出: <div>第一层<div>第二层</div>剩余</div>
# 非贪婪匹配 - 匹配尽可能少
pattern_lazy = r'<div>.*?</div>'
print(re.search(pattern_lazy, text).group())
# 输出: <div>第一层<div>第二层</div>
4. 边界匹配技巧
# \b 的精确应用
words = ["cat", "catalog", "category", "bat", "batch"]
# 匹配完整的 "cat" 单词
pattern = r'\bcat\b'
for word in words:
if re.match(pattern, word):
print(f"匹配: {word}")
# 输出: cat
七、综合示例:构建一个简单的日志解析器
以下是一个整合多个高级用法的完整示例:
import re
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass
class LogEntry:
timestamp: datetime
level: str
message: str
request_id: Optional[str]
# 日志格式: "2024-05-15 10:30:45 [INFO] Process started [Request-ID: abc123]"
LOG_PATTERN = re.compile(r'''
(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})\s+
(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})\s+
\[(?P<level>INFO|WARNING|ERROR|DEBUG)\]\s+
(?P<message>.+?)\s*
(?:\[Request-ID:\s*(?P<request_id>\w+)\])?
$
''', re.VERBOSE)
def parse_log_line(line: str) -> Optional[LogEntry]:
match = LOG_PATTERN.match(line.strip())
if not match:
return None
try:
timestamp = datetime(
int(match.group('year')),
int(match.group('month')),
int(match.group('day')),
int(match.group('hour')),
int(match.group('minute')),
int(match.group('second'))
)
return LogEntry(
timestamp=timestamp,
level=match.group('level'),
message=match.group('message'),
request_id=match.group('request_id')
)
except ValueError:
return None
# 测试
log_lines = [
"2024-05-15 10:30:45 [INFO] Process started [Request-ID: abc123]",
"2024-05-15 10:30:46 [WARNING] High memory usage detected",
"2024-05-15 10:30:47 [ERROR] Connection failed [Request-ID: def456]"
]
for line in log_lines:
entry = parse_log_line(line)
if entry:
print(f"[{entry.level}] {entry.timestamp} - {entry.message}")
if entry.request_id:
print(f" Request ID: {entry.request_id}")
总结要点
掌握这些高级用法后,你将能够:
- 用
IGNORORECASE、MULTILINE等标志灵活控制匹配行为 - 用命名组替代数字,提高代码可读性和维护性
- 用零宽断言实现精准定位,无需消耗字符
- 用非捕获组优化性能
- 预编译常用表达式,应对高频率匹配场景
正则表达式的强大在于组合。遇到复杂文本处理任务时,先分析需求,再选择合适的高级特性组合使用,往往能写出简洁而高效的解决方案。

暂无评论,快来抢沙发吧!