pin_drop当前位置:知识文库 ❯ 图文

Python正则表达式分组与捕获 - 命名分组和非捕获分组详解

一、什么是分组与捕获

在Python正则表达式中,分组(Grouping)与捕获(Capturing)是提取匹配内容的核心机制。通过使用圆括号 (),我们可以将多个字符组合成一个逻辑单元,并提取出匹配到的具体内容。

分组与捕获的主要作用包括:提取子匹配内容、对量词作用范围进行分组、在正则内部引用前面匹配的内容等。掌握这些技巧,可以大幅提高正则表达式的灵活性和实用性。


二、基本分组语法

最基本的分组就是使用圆括号 () 包裹需要分组的模式。Python会自动为每个左括号分配一个组号,从1开始递增。

代码示例

import re

# 示例:提取日期中的年、月、日
text = "今天是2024-12-25"
pattern = r"(\d{4})-(\d{2})-(\d{2})"
match = re.search(pattern, text)

if match:
    print(f"完整匹配: {match.group()}")    # 2024-12-25
    print(f"第1组(年): {match.group(1)}")  # 2024
    print(f"第2组(月): {match.group(2)}")  # 12
    print(f"第3组(日): {match.group(3)}")  # 25

group() 方法不传参数时返回整个匹配结果,传入数字参数则返回对应组号的捕获内容。groups() 方法则返回所有捕获组的元组:

代码示例

import re

text = "订单号: ORD-2024-A12345"
pattern = r"ORD-(\d{4})-([A-Z]\d{5})"
match = re.search(pattern, text)

if match:
    print(match.groups())       # ('2024', 'A12345')
    print(match.group(0))       # ORD-2024-A12345
    print(match.group(1))       # 2024
    print(match.group(2))       # A12345

三、命名分组

当正则表达式中有多个分组时,使用数字组号容易混淆。Python支持使用 (?Ppattern) 语法为分组命名,使代码更易读。

代码示例

import re

# 使用命名分组解析邮箱
email = "zhangsan@example.com"
pattern = r"(?P<username>[a-zA-Z0-9_.+-]+)@(?P<domain>[a-zA-Z0-9-]+\.[a-zA-Z]{2,})"
match = re.search(pattern, email)

if match:
    print(f"用户名: {match.group('username')}")    # zhangsan
    print(f"域名: {match.group('domain')}")         # example.com
    
    # groupdict() 返回所有命名分组的字典
    print(match.groupdict())
    # {'username': 'zhangsan', 'domain': 'example.com'}

命名分组同样可以通过组号访问,组号按照左括号出现的顺序分配:

代码示例

import re

text = "2024-06-07"
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
match = re.search(pattern, text)

# 通过组号访问
print(match.group(1))       # 2024
print(match.group(2))       # 06

# 通过名称访问
print(match.group('year'))  # 2024
print(match.group('month')) # 06

四、非捕获分组

有时我们只需要对模式进行分组,但不需要捕获该分组的内容。这时可以使用非捕获分组 (?:pattern),它不会占用组号,也不会出现在匹配结果中。

代码示例

import re

# 捕获分组 vs 非捕获分组对比
text = "Python 3.12"

# 使用捕获分组
p1 = r"(Python) (\d+\.\d+)"
m1 = re.search(p1, text)
print(m1.groups())   # ('Python', '3.12')

# 使用非捕获分组(版本号部分不捕获)
p2 = r"(Python) (?:\d+\.\d+)"
m2 = re.search(p2, text)
print(m2.groups())   # ('Python',)

非捕获分组的典型应用场景:

  • 限定量词范围:如 (?:ab)+ 匹配 "ababab"

  • 逻辑或分组:如 (?:jpg|png|gif) 匹配图片扩展名

  • 减少内存开销:不需要提取的内容使用非捕获分组更高效


五、分组的引用与反向引用

反向引用(Backreference)允许在正则表达式中引用前面已经匹配过的分组内容。语法为 \1\2 等,或在命名分组中使用 (?P=name)

代码示例

import re

# 查找连续重复的单词
text = "hello hello world world"
pattern = r"\b(\w+)\s+\1\b"
matches = re.findall(pattern, text)
print(matches)  # ['hello', 'world']

# 查找HTML标签的匹配
html = "<div>content</div><span>more</span>"
pattern = r"<(\w+)>(.*?)</\1>"
matches = re.findall(pattern, html)
print(matches)  # [('div', 'content'), ('span', 'more')]

使用命名分组的反向引用:

代码示例

import re

# 使用命名分组的反向引用
text = "<title>网页标题</title>"
pattern = r"<(?P<tag>\w+)>(?P<content>.*)</(?P=tag)>"
match = re.search(pattern, text)

if match:
    print(f"标签名: {match.group('tag')}")      # title
    print(f"标签内容: {match.group('content')}") # 网页标题

六、实战应用示例

1. 解析URL路径

代码示例

import re

# 解析URL中的协议、域名和路径
urls = [
    "https://www.example.com/path/to/page",
    "http://api.test.com/v1/users",
    "ftp://files.server.com/docs/report.pdf"
]

pattern = r"(?P<protocol>https?|ftp)://(?P<domain>[^/]+)/(?P<path>.*)"

for url in urls:
    match = re.search(pattern, url)
    if match:
        d = match.groupdict()
        print(f"协议: {d['protocol']}, 域名: {d['domain']}, 路径: {d['path']}")

2. 提取日志中的关键信息

代码示例

import re

# 解析服务器日志格式: [时间] 级别: 消息
logs = [
    "[2024-12-25 10:30:00] INFO: Server started on port 8080",
    "[2024-12-25 10:30:05] ERROR: Database connection failed",
    "[2024-12-25 10:31:00] WARNING: High memory usage detected"
]

pattern = r"\[(?P<time>[^\]]+)\] (?P<level>\w+): (?P<message>.*)"

for log in logs:
    match = re.search(pattern, log)
    if match:
        d = match.groupdict()
        print(f"[{d['time']}] {d['level']} -> {d['message']}")

小贴士

在Python中,re.match() 只从字符串开头匹配,re.search() 扫描整个字符串。提取单个匹配用 search,提取多个匹配用 re.finditer() 可以获取每个匹配的分组信息。

注意:非捕获分组 (?:) 不会计入组号。在混合使用捕获和非捕获分组时,建议用命名分组避免混淆。

常见问题

group(0) 和 groups() 有什么区别?

group(0) 返回整个匹配结果,而 groups() 返回一个元组,包含所有捕获组的内容(不包括group(0))。

什么时候应该使用非捕获分组?

当你只需要用括号来组合模式(比如给量词划定范围或做逻辑或),但不需要提取该部分内容时,应该使用非捕获分组 (?:)。这样可以避免无意义的捕获,节省内存并简化组号管理。

反向引用和分组引用有什么区别?

反向引用 \1 是在正则表达式内部引用前面匹配的内容,用于确保后面匹配的内容与前面一致。而分组引用 match.group(1) 是在匹配完成后从匹配对象中提取内容。

findall() 返回的为什么是元组列表?

当正则表达式中有捕获分组时,re.findall() 返回的是每个匹配的分组内容组成的元组列表。如果没有分组,则返回字符串列表。如果需要完整匹配信息,建议使用 re.finditer()

练习1

编写正则表达式,从字符串 "姓名: 张三, 电话: 13800138000, 邮箱: zhangsan@example.com" 中分别提取姓名、电话和邮箱。

练习2

编写一个函数,使用命名分组解析CSV格式数据 "2024-06-07, 产品A, 299.99, 已售出",返回包含日期、产品名、价格和状态的字典。

标签: 正则表达式 分组捕获 命名分组 非捕获分组 反向引用 Python教程

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: Python re.compile()预编译正则详解 - 性能优化与Pattern对象使用 下一篇: Python正则贪婪与非贪婪匹配 - 量词使用与回溯优化技巧

poll相关推荐