Python 编程:从零基础到精通
第 38 课 - 正则表达式高级用法与分组
所属模块: 模块八:装饰器与高级主题
难度: Advanced
标签: regex, groups, advanced
上一课: 正则表达式基础与 re 模块
下一课: 单元测试基础(unittest 模块)
1. 学习目标
完成本课学习后,你将能够:
- 理解并使用分组(Grouping) 来捕获和提取模式中的特定部分。
- 运用向后引用(Backreferences) 在模式或替换中引用之前捕获的内容。
- 使用命名分组(Named Groups) 提高模式的可读性和可维护性。
- 掌握零宽断言(Lookaround Assertions) 进行复杂的上下文匹配。
- 熟练运用
re.sub()的高级替换功能,包括使用函数进行动态替换。
2. 核心概念
什么是分组?
想象一下,你想从一个字符串中提取日期,比如"今天是 2023-10-27"。你不仅想匹配整个日期串,还想分别获取年、月、日。分组就是用小括号 () 把你想要"捕获"的部分括起来。匹配成功后,这些被捕获的字符串就像被装进了不同的"篮子"里,可以单独取出来使用。
什么是零宽断言?
零宽断言听起来复杂,其实就是"匹配一个位置,而不是匹配字符"。它检查某个位置的前面或后面是否满足特定条件,但本身不消耗任何字符。比如:
(?=...):正向前瞻。确保某个模式出现在后面。(?<=...):正向后顾。确保某个模式出现在前面。(?!...):负向前瞻。确保某个模式没有出现在后面。(?<!...):负向后顾。确保某个模式没有出现在前面。
3. 代码示例
示例 1:基础分组与捕获
import re
text = "我的电话是 138-1234-5678,备用电话是 139-8765-4321。"
# 使用分组捕获电话号码的各个部分
pattern = r'(\d{3})-(\d{4})-(\d{4})'
matches = re.findall(pattern, text)
print("所有匹配项:")
for match in matches:
print(match) # 输出元组:('138', '1234', '5678')
# 使用 `group()` 方法在单个匹配中获取特定组
match_obj = re.search(pattern, text)
if match_obj:
full_number = match_obj.group(0) # 整个匹配
area_code = match_obj.group(1) # 第一组
part1 = match_obj.group(2) # 第二组
part2 = match_obj.group(3) # 第三组
print(f"\n完整号码: {full_number}")
print(f"区号: {area_code}")
示例 2:向后引用与命名分组
import re
text = "the cat sat on the mat, the dog sat on the log."
# 使用向后引用 (\1) 查找连续重复的单词
pattern_repeated = r'\b(\w+)\s+\1\b' # \1 引用前面 (\w+) 捕获的内容
matches_repeated = re.findall(pattern_repeated, text)
print(f"重复单词: {matches_repeated}") # 输出: []
# 使用命名分组提高可读性,并解析 HTML 简单标签(仅作演示,解析复杂HTML请用专门库)
html_snippet = '<h1>Title</h1><p>Paragraph</p>'
pattern_named = r'<(?P<tag>\w+)>(?P<content>.*?)</(?P=tag)>'
matches_named = re.finditer(pattern_named, html_snippet)
for m in matches_named:
tag = m.group('tag') # 通过名称获取"标签"组
content = m.group('content') # 通过名称获取"内容"组
print(f"找到标签: <{tag}>,内容: {content}")
示例 3:零宽断言实战
import re
prices = "$100, €200, ¥500, $150.5"
# 1. 正向前瞻:匹配所有以 $ 开头的数字(不包含 $ 符号)
pattern_dollar = r'\d+\.?\d*(?=\$)' # "后面是 $" 的数字
matches_dollar = re.findall(pattern_dollar, prices)
print(f"美元价格数字: {matches_dollar}") # 输出: ['100', '150.5']
# 2. 正向后顾:匹配所有紧跟在 ¥ 后面的数字
pattern_yen = r'(?<=¥)\d+'
matches_yen = re.findall(pattern_yen, prices)
print(f"日元价格数字: {matches_yen}") # 输出: ['500']
# 3. 负向前瞻:匹配所有**不是**以 € 开头的数字
pattern_not_euro = r'\b\d+(?!€)\b' # 这个例子需要更精确,这里仅作概念演示
# 实际应用:匹配不在引号内的单词
sentence = 'She said "hello world" and then hello.'
pattern_outside_quotes = r'\b\w+\b(?![^"]*")' # 简化版,查找不在引号内的单词
matches_outside = re.findall(pattern_outside_quotes, sentence)
print(f"引号外的单词: {matches_outside}") # 输出: ['She', 'said', 'and', 'then', 'hello']
示例 4:re.sub() 的高级替换(使用函数)
import re
def make_bold(match_obj):
"""将匹配的文本用 <b> 标签包裹"""
content = match_obj.group(0)
return f"<b>{content}</b>"
def multiply_by_two(match_obj):
"""将匹配到的数字乘以2"""
number_str = match_obj.group(0)
return str(int(number_str) * 2)
text = "今天有3个苹果和5个橘子。"
# 将数字替换为乘以2后的结果
new_text = re.sub(r'\d+', multiply_by_two, text)
print(f"替换后: {new_text}") # 输出: 今天有6个苹果和10个橘子。
# 将"苹果"和"橘子"加粗(使用命名分组和lambda)
new_text_html = re.sub(r'(?P<fruit>苹果|橘子)', lambda m: f"<b>{m.group('fruit')}</b>", text)
print(f"HTML替换: {new_text_html}")
4. 实践练习
练习 1 (基础): 编写一个正则表达式,从字符串 "Error code: 404, Error message: Not Found, Error code: 500, Error message: Internal Server Error" 中提取所有错误代码(404, 500等)。使用分组实现。
- 预期输出:
['404', '500']
练习 2 (中等): 从日志字符串 "2023-10-27 10:30:15 [INFO] User logged in, 2023-10-27 10:31:20 [ERROR] Database connection failed" 中提取日期、时间、日志级别和日志消息。使用命名分组使你的模式清晰易读。
- 预期输出: 一个字典列表,如
[{'date': '2023-10-27', 'time': '10:30:15', 'level': 'INFO', 'message': 'User logged in'}, ...]
练习 3 (进阶): 编写一个函数,接受一段文本,将其中所有的价格(如 $9.99, $100)增加10% 并格式化为两位小数。使用 re.sub() 和自定义函数实现。
- 示例输入:
"商品A价格是$10,商品B价格是$19.99。" - 预期输出:
"商品A价格是$11.00,商品B价格是$21.99。"
5. 常见错误
- 忘记转义特殊字符:在字符类
[]外,像.*?+等元字符需要匹配字面值时,必须用\转义。例如,匹配小数点应写\.,而不是.。 - 混淆分组索引:第一个
(产生的组是 1,不是 0。group(0)或整个匹配是特殊的,不消耗分组计数。 - 贪婪与非贪婪的误用:默认
*,+,?是贪婪的(匹配尽可能多)。想非贪婪(匹配尽可能少)需加?,如.*?。误用会导致匹配结果不符合预期。 - 在零宽断言中使用复杂模式:零宽断言内部的模式不能是任意长度(Python的
re模块限制),且不会消耗字符,因此不能在其中捕获组(但Python 3.x部分版本支持变通)。 - 过度依赖正则表达式:对于结构非常复杂的文本(如完整的HTML/XML),使用正则解析容易出错且难以维护。应优先考虑使用专用的解析库(如
BeautifulSoup,lxml)。
6. 小结
本节课我们深入探索了正则表达式的高级特性:
- 分组
()是提取子模式的核心,findall会返回元组列表,search的group()方法可以按编号或名称获取内容。 - 向后引用
\1和 命名分组(?P<name>...)以及(?P=name)增强了模式的复用性和可读性。 - 零宽断言
(?=...),(?<=...),(?!...),(?<!...)允许我们进行"位置判断",实现"包含"或"不包含"某模式的精确匹配,而不消耗字符。 re.sub()的替换字符串或函数参数极其强大,特别是结合分组,可以实现复杂的文本转换逻辑。
掌握这些高级用法,能让你用正则表达式解决更加复杂和精细的文本处理问题。下一课,我们将进入保证代码质量的重要领域——单元测试。