Python 系列(五十八) 搞懂正则表达式中的分组与量词
1 | 作者:李晓辉 |
- Python 系列(一) 从零开始认识 Python,入门其实没那么难
- Python 系列(二) 学 Python 的第一步,把开发环境搭建好
- Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
- Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
- Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
- Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
- Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
- Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
- Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
- Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
- Python 系列(十一) list、tuple、set、dict,到底该怎么选?
- Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
- Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
- Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
- Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
- Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
- Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
- Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
- Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
- Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
- Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
- Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
- Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
- Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
- Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
- Python 系列(二十六) Python 模块入门,从 import 到模块复用
- Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
- Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
- Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
- Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
- Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
- Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
- Python 系列(三十三) 什么是类和对象?零基础理解 Python class
- Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
- Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
- Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
- Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
- Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
- Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
- Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
- Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十三) 别只会处理异常!学会自定义异常,让程序更容易维护
- Python 系列(四十四) 从零认识 assert 断言,让程序学会检查自己
- Python 系列(四十五) 使用 sys 模块处理标准输入与输出
- Python 系列(四十六) 使用 open() 读取和写入文件
- Python 系列(四十七) 学会读写文本文件,让程序保存运行结果
- Python 系列(四十八) 搞懂 bytes、bytearray 和二进制读写
- Python 系列(四十九) 使用 seek() 和 tell() 灵活控制文件读取位置
- Python 系列(五十) 掌握 os 和 os.path,实现文件与目录自动化管理
- Python 系列(五十一) 掌握列表推导式,轻松实现列表生成与数据筛选
- Python 系列(五十二) 掌握字典推导式,让字典创建更简单
- Python 系列(五十三) 从零理解生成器,轻松处理大量数据
- Python 系列(五十五) 使用 zip() 同时遍历多个集合
- Python 系列(五十六) 使用 re 模块进行正则表达式匹配
- Python 系列(五十七) 理解正则表达式的字符类和量词
- Python 系列(五十八) 搞懂正则表达式中的分组与量词
- Python 系列(五十九) 学会正则捕获组,轻松提取文本数据
- Python 系列(六十) 掌握 re 模块的常用函数
- Python 系列(六十一) JSON 是什么?语法规则、数据类型与校验方法
- Python 系列(六十二) 使用 pdb 逐步调试程序,定位代码问题
- Python 系列(六十三) 在交互式 Python shell 中启动调试器
- Python 系列(六十四) 从零开始学习 pdb 命令行调试
- Python 系列(六十五) 使用 pdb 设置断点并检查程序执行上下文
前面我们学习了正则表达式中的字符类和量词。
例如,使用 \d+ 可以匹配一个或多个连续的数字:
1 | import re |
运行结果:
1 | 123 |
这里的 \d 表示匹配一个数字,+ 表示前面的内容可以出现一次或多次。因此,\d+ 可以匹配连续出现的多个数字。
但是,如果我们希望重复匹配的不是一个字符,而是一段完整的内容,该怎么办呢?
例如,下面这段文本中,ab 连续出现了三次:
1 | ababab |
如果希望使用一个量词来表示 ab 可以重复出现,就需要学习正则表达式中的分组。
为什么需要分组
先来看一个例子:
1 | import re |
运行结果:
1 | abbb |
为什么匹配结果是 abbb,而不是只匹配一个 ab 呢?
这是因为,量词默认只作用于它前面的那个字符或正则表达式元素。
在 ab+ 中:
a:匹配一个字母a。b:匹配一个字母b。+:表示前面的b可以出现一次或多次。
所以,ab+ 可以匹配 ab、abb、abbb 等内容。
但是,如果我们希望 ab 这两个字符作为一个整体重复出现,就需要使用圆括号将它们括起来。
使用括号创建分组
在正则表达式中,圆括号 () 可以把一段表达式组合成一个整体。
例如:
1 | import re |
运行结果:
1 | abab |
这里的 (ab)+ 可以拆成两部分理解:
(ab):将字母a和b组合成一个整体。+:表示这个整体可以连续出现一次或多次。
因此,(ab)+ 可以匹配以下内容:
| 待匹配字符串 | 是否能找到匹配 | 实际匹配结果 |
|---|---|---|
ab | 是 | ab |
abab | 是 | abab |
ababab | 是 | ababab |
a | 否 | 无匹配结果 |
aba | 是 | ab |
这里需要特别解释最后一行。
正则表达式 (ab)+ 要求找到一个或多个连续的 ab。字符串 aba 的开头有一个完整的 ab,所以 re.search() 能够找到匹配结果 ab。
但是,最后一个 a 不属于这次匹配的内容。匹配成功不代表整个字符串都符合正则表达式。
如果希望整个字符串必须由一个或多个完整的 ab 组成,可以使用 re.fullmatch():
1 | import re |
运行结果:
1 | abab |
第一个字符串完全由两个 ab 组成,因此匹配成功。
第二个字符串最后多出了一个 a,不能由一个或多个完整的 ab 组成,因此匹配失败,返回 None。
这也说明了两个方法的区别:
re.search():在字符串中查找符合条件的部分。re.fullmatch():要求整个字符串都符合正则表达式。
理解分组与量词的工作过程
我们可以把 (ab)+ 理解为:先匹配一个完整的 ab,然后继续尝试匹配下一个 ab,直到无法继续匹配为止。
假设待匹配的字符串是:
1 | ababab |
匹配过程可以简化为下面的流程:
flowchart TD
A["读取正则表达式 (ab)+"] --> B["将 ab 组合成一个分组"]
B --> C["尝试匹配一个完整的 ab"]
C --> D{"是否匹配成功?"}
D -->|是| E["完成一次分组匹配"]
E --> F{"后面还有完整的 ab 吗?"}
F -->|有| E
F -->|没有| G["结束匹配并返回结果"]
D -->|否| H["没有满足要求的匹配"]对于 ababab,可以把匹配过程理解为:
- 匹配第一个
ab。 - 匹配第二个
ab。 - 匹配第三个
ab。 - 后面已经没有完整的
ab,结束匹配。
最终,整个表达式匹配到的内容是 ababab。
这就是分组与量词配合使用的核心作用:让量词作用于括号中的整个表达式,而不是只作用于最后一个字符。
区分字符量词和分组量词
下面通过一个例子,对比 ab+ 和 (ab)+ 的区别:
1 | import re |
运行结果:
1 | ab |
为什么两个表达式得到的结果不同?
对于 ab+:
- 首先匹配一个
a。 - 接着匹配一个或多个连续的
b。 - 字符串开头是
ab,但后面紧跟的是a,而不是b,因此匹配到ab就结束了。
对于 (ab)+:
- 将
ab作为一个整体。 - 连续匹配三个
ab。 - 最终得到
ababab。
再来看问号 ? 的区别:
1 | import re |
运行结果:
1 | 'a' |
第一种表达式 ab? 中,? 只作用于 b,表示 b 可以出现零次或一次。因此,即使字符串中没有 b,也可以匹配到 a。
第二种表达式 (ab)? 中,? 作用于整个分组,表示整个 ab 可以出现零次或一次。
由于零次出现也是允许的,re.search() 可以在字符串开头找到一个长度为零的匹配,因此 result2.group() 返回空字符串 ''。
理解空匹配:为什么没有字符也能匹配成功
初学者可能会觉得奇怪:既然字符串是 a,没有完整的 ab,为什么 (ab)? 还能匹配成功?
关键在于,? 表示前面的分组可以出现零次或一次。
所谓出现零次,就是这次匹配不需要匹配到 ab 中的任何字符。因此,Python 可以在字符串开头找到一个长度为零的匹配。
我们可以通过下面的代码观察:
1 | import re |
运行结果:
1 | False |
逐行解释:
result is None输出False,说明 Python 找到了匹配对象。repr(result.group())输出'',说明这次匹配得到的是空字符串。result.start()输出0,表示匹配从字符串的起始位置开始。result.end()输出0,表示匹配结束的位置仍然是0。
开始位置和结束位置相同,说明这次匹配没有消耗任何字符。
这里需要区分三个概念:
| 表达形式 | 含义 |
|---|---|
None | 没有找到匹配对象 |
'' | 找到了匹配对象,但匹配内容为空字符串 |
print('') | 输出空字符串,因此屏幕上只会出现一个空行 |
例如:
1 | import re |
运行结果:
1 | None |
第四条输出对应一个空行。这是因为 result2.group() 返回了空字符串,而 print() 输出空字符串后仍然会换行。
因此,屏幕上出现空行,不代表匹配失败。判断是否找到匹配对象,应检查结果是否为 None。
分组可以与哪些量词组合使用
分组可以与多种量词组合使用。
| 量词 | 含义 | 示例 |
|---|---|---|
* | 分组可以出现零次或多次 | (ab)* |
+ | 分组可以出现一次或多次 | (ab)+ |
? | 分组可以出现零次或一次 | (ab)? |
{3} | 分组必须连续出现三次 | (ab){3} |
{2,4} | 分组连续出现两到四次 | (ab){2,4} |
例如,我们希望匹配恰好连续出现三次的 ab:
1 | import re |
运行结果:
1 | ababab |
如果字符串中只有两次 ab,就无法满足要求:
1 | import re |
运行结果:
1 | None |
None 表示没有找到符合要求的完整匹配。
这里使用 re.fullmatch(),是为了要求整个字符串都必须由三组 ab 组成,而不是只要其中某一部分满足条件就算成功。
在 Linux 运维场景中使用分组
假设我们正在分析 Linux 服务器日志,发现某条日志中连续出现了多个 ERROR 标记:
1 | ERRORERRORERROR disk failure |
如果希望提取开头连续重复的 ERROR 标记,可以这样写:
1 | import re |
运行结果:
1 | ERRORERRORERROR |
这里使用 re.match(),因为我们只需要从日志开头开始匹配。
正则表达式 (ERROR)+ 表示:
(ERROR):将整个ERROR单词组合成一个分组。+:要求这个分组连续匹配一次或多次。
如果写成 ERROR+,量词就只作用于最后一个 R,含义会完全不同。
例如:
1 | import re |
运行结果:
1 | ERRORRR |
这是因为 ERROR+ 中的 + 只修饰最后一个 R,允许 R 连续出现多次。
因此,当我们需要重复匹配一个单词、一段字符串或者一组字符时,应该先使用括号组合表达式,再根据需要选择量词。
分组与捕获分组有什么区别
到这里,我们需要区分两个相关但不同的概念:分组用于组合表达式,捕获分组用于保存分组匹配到的内容。
普通圆括号 () 既可以将表达式组合成一个整体,也会创建捕获分组。这两种作用经常同时出现,但解决的问题不同。
使用分组控制重复匹配
例如:
1 | import re |
运行结果:
1 | ababab |
这里使用 (ab){3},是为了把 ab 作为一个整体,并要求它连续出现三次。
括号明确了量词的作用范围,这是分组在这个例子中的主要用途。
使用捕获分组提取内容
普通圆括号还会创建捕获分组。捕获分组可以保存对应部分匹配到的内容,以便后续提取。
例如,我们想从服务器监控记录中提取 CPU 使用率和内存使用率:
1 | import re |
运行结果:
1 | CPU=75% MEMORY=90% |
这段表达式可以拆成几个部分:
CPU=:匹配固定文本CPU=。(\d+):匹配一个或多个数字,并创建第一个捕获分组。% MEMORY=:匹配固定文本% MEMORY=。(\d+):匹配一个或多个数字,并创建第二个捕获分组。%:匹配百分号。
这里:
group(0):返回整个正则表达式匹配到的内容。group(1):返回第一个捕获分组中的内容,即75。group(2):返回第二个捕获分组中的内容,即90。
如果只是想控制一段表达式重复匹配,重点是分组与量词的组合。
如果还需要从匹配结果中提取某个字段,就需要关注捕获分组。
重复匹配时,捕获分组会保存什么
再来看一个容易产生误解的例子:
1 | import re |
运行结果:
1 | ababab |
为什么 group(0) 得到 ababab,而 group(1) 只有 ab?
原因是:
group(0)返回整个正则表达式匹配到的内容。group(1)对应第一个捕获分组(ab)。- 这个捕获分组在重复匹配过程中多次成功匹配,但普通捕获分组只保留最后一次捕获的内容。
因此,group(1) 不会自动把所有重复匹配的结果保存成列表。
可以把它们理解为:
| 表达形式 | 作用 |
|---|---|
(ab)+ | 将 ab 组合成一个分组,并要求它出现一次或多次 |
group(0) | 获取整个正则表达式匹配到的内容 |
group(1) | 获取第一个捕获分组最后一次捕获的内容 |
普通圆括号可以同时承担组合表达式和捕获内容这两种作用,但两者不是同一个概念。
动手练习:观察分组与捕获分组
理解概念之后,最好亲自运行几段代码,观察不同表达式产生的结果。
练习一:比较 ab+ 和 (ab)+
请运行下面的代码,预测两次输出结果,再执行程序验证。
1 | import re |
运行结果:
1 | 表达式一: ab |
思考一下:
- 为什么第一个表达式只匹配到
ab? - 为什么第二个表达式可以匹配到整个
ababab?
参考答案:第一个表达式的 + 只作用于 b;第二个表达式的 + 作用于整个 ab 分组。
练习二:观察重复分组的捕获结果
运行下面的代码:
1 | import re |
运行结果:
1 | 完整匹配: ababab |
思考一下:为什么完整匹配结果是 ababab,而捕获分组只有 ab?
参考答案:group(0) 返回整个表达式匹配到的内容;group(1) 返回第一个捕获分组最后一次捕获的内容。重复捕获分组不会自动收集每一次匹配的结果。
练习三:提取监控数据中的两个数值
现在有一条简化的监控记录:
1 | CPU=65% MEMORY=82% |
请使用两个捕获分组,分别提取 CPU 和内存使用率。
可以先尝试自己编写代码,再对照下面的答案:
1 | import re |
运行结果:
1 | CPU 使用率: 65 |
这里的两个 (\d+) 分别创建了第一个和第二个捕获分组。
正则表达式负责找到符合格式的文本,捕获分组则帮助我们单独提取其中的数值。
注意,这里提取出来的 65 和 82 仍然是字符串。如果后续需要进行数值比较,还可以使用 int() 将它们转换为整数。
练习四:区分匹配失败与空匹配
运行下面的代码:
1 | import re |
运行结果:
1 | True |
请回答:
- 为什么
result1 is None的结果是True? - 为什么
result2 is None的结果是False? - 为什么
result2.group()返回''?
参考答案:
z+ 要求匹配一个或多个连续的 z,而字符串 abc 中没有 z,所以没有匹配结果。
(ab)? 允许分组出现零次或一次,因此即使没有匹配到 ab,也可以在字符串开头得到一个空匹配对象。这个匹配对象存在,但它匹配到的内容是空字符串。
这说明,判断是否找到匹配对象与判断匹配到的内容是否为空,是两个不同的问题。
写在最后
通过本文,我们学习了正则表达式中的分组、组量词和捕获分组。
需要记住以下知识:
- 圆括号
()可以将一段正则表达式组合成一个整体。 - 量词默认作用于它前面的元素;使用括号可以让量词作用于整个分组。
(ab)+表示ab这个整体连续出现一次或多次,而ab+表示a后面跟着一个或多个b。re.search()可以只匹配字符串中的一部分,因此re.search(r'(ab)+', 'aba')得到的是ab,不是整个aba。None表示没有匹配对象,''表示匹配成功但结果为空字符串,两者不能混为一谈。- 普通圆括号既可以组合表达式,也会创建捕获分组。
group(0)返回整个匹配结果,group(1)返回第一个捕获分组的内容。- 重复捕获分组时,普通捕获分组只保留最后一次捕获的内容,不会自动收集所有重复结果。
下一篇,我们将继续学习正则表达式中的捕获分组,了解如何使用分组编号提取不同字段,以及如何更灵活地处理匹配结果。
