38·模块八:装饰器与高级主题高级

正则表达式高级用法与分组

regexgroupsadvanced

Python 编程:从零基础到精通

第 38 课 - 正则表达式高级用法与分组

所属模块: 模块八:装饰器与高级主题
难度: Advanced
标签: regex, groups, advanced
上一课: 正则表达式基础与 re 模块
下一课: 单元测试基础(unittest 模块)


1. 学习目标

完成本课学习后,你将能够:

  1. 理解并使用分组(Grouping) 来捕获和提取模式中的特定部分。
  2. 运用向后引用(Backreferences) 在模式或替换中引用之前捕获的内容。
  3. 使用命名分组(Named Groups) 提高模式的可读性和可维护性。
  4. 掌握零宽断言(Lookaround Assertions) 进行复杂的上下文匹配。
  5. 熟练运用 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. 忘记转义特殊字符:在字符类 [] 外,像 . * ? + 等元字符需要匹配字面值时,必须用 \ 转义。例如,匹配小数点应写 \.,而不是 .
  2. 混淆分组索引:第一个 ( 产生的组是 1,不是 0。group(0) 或整个匹配是特殊的,不消耗分组计数。
  3. 贪婪与非贪婪的误用:默认 *, +, ? 是贪婪的(匹配尽可能多)。想非贪婪(匹配尽可能少)需加 ?,如 .*?。误用会导致匹配结果不符合预期。
  4. 在零宽断言中使用复杂模式:零宽断言内部的模式不能是任意长度(Python的 re 模块限制),且不会消耗字符,因此不能在其中捕获组(但Python 3.x部分版本支持变通)。
  5. 过度依赖正则表达式:对于结构非常复杂的文本(如完整的HTML/XML),使用正则解析容易出错且难以维护。应优先考虑使用专用的解析库(如 BeautifulSoup, lxml)。

6. 小结

本节课我们深入探索了正则表达式的高级特性:

  • 分组 () 是提取子模式的核心,findall 会返回元组列表,searchgroup() 方法可以按编号或名称获取内容。
  • 向后引用 \1命名分组 (?P<name>...) 以及 (?P=name) 增强了模式的复用性和可读性。
  • 零宽断言 (?=...), (?<=...), (?!...), (?<!...) 允许我们进行"位置判断",实现"包含"或"不包含"某模式的精确匹配,而不消耗字符。
  • re.sub() 的替换字符串或函数参数极其强大,特别是结合分组,可以实现复杂的文本转换逻辑。

掌握这些高级用法,能让你用正则表达式解决更加复杂和精细的文本处理问题。下一课,我们将进入保证代码质量的重要领域——单元测试。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「单元测试基础(unittest 模块)」 以巩固所学知识。