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支持使用 (?P 语法为分组命名,使代码更易读。
代码示例
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, 已售出",返回包含日期、产品名、价格和状态的字典。
本文涉及AI创作
内容由AI创作,请仔细甄别