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 设置断点并检查程序执行上下文
在前面的文章中,我们已经认识了正则表达式,并学习了如何使用 Python 的 re 模块查找符合条件的文本。
例如,我们想从一段服务器日志中提取数字:
1 | import re |
运行结果:
1 | 01 |
为什么我们想查找 CPU 使用率,程序却返回了 01,而不是 85?
这是因为 re.search() 会从字符串的开头开始查找,找到第一个符合正则表达式规则的内容后,就会立即返回。
在这个例子中,\d+ 表示匹配一个或多个连续的数字字符。字符串中的 web01 最先出现数字,因此程序首先匹配到了 01。
如果我们希望找到 CPU 使用率中的数字,就需要进一步限定匹配条件,例如匹配 CPU= 后面的数字。
这正是本文要学习的内容:使用字符类规定字符需要满足什么条件,再使用量词规定匹配项可以出现多少次。
认识正则表达式中的字符类和量词
在学习具体语法之前,我们先明确几个容易混淆的概念。
| 概念 | 作用 | 示例 |
|---|---|---|
| 普通字符 | 匹配字符自身 | a、5、% |
| 字符类 | 规定单个字符可以满足什么条件 | [abc]、[a-z] |
| 预定义字符类 | 使用简写表示一类字符 | \d、\w、\s |
| 量词 | 规定前面的匹配项可以出现多少次 | *、+、?、{m,n} |
| 分组 | 将表达式的一部分组合起来,并可捕获匹配内容 | (\d+) |
| 匹配函数 | 决定如何查找或验证匹配结果 | search()、findall()、fullmatch() |
初学阶段,可以先记住四句话:
- 字符类规定一个字符需要满足什么条件。
- 量词规定前面的匹配项可以出现多少次。
- 分组用于组织表达式,并可以捕获匹配内容。
- 匹配函数负责按照正则表达式查找或验证文本。
例如,下面这个表达式:
1 | r"\d+" |
包含两个部分:
\d是预定义字符类的简写,表示匹配一个数字字符。+是量词,表示前面的匹配项至少出现一次。
组合起来,\d+ 就表示匹配一个或多个连续的数字字符。
需要注意,量词修饰的不一定只是一个普通字符。它可以修饰普通字符、字符类、预定义字符类,也可以修饰分组。
使用预定义字符类匹配常见字符
正则表达式提供了一些常用的简写规则,可以帮助我们快速描述数字、单词字符和空白字符。
使用 \d 匹配数字
\d 是预定义字符类的简写,用于匹配一个数字字符。
例如:
1 | import re |
运行结果:
1 | 8 |
为什么输出的是 8,而不是 85?
因为 \d 只匹配一个数字字符。
程序从左向右查找,遇到数字 8 后,就已经完成了一次匹配。后面的 5 不属于这次匹配的内容。
如果想匹配两个连续的数字,可以写成:
1 | import re |
运行结果:
1 | 12 |
这里的 \d\d 表示两个连续的数字字符。
但如果服务器编号长度不固定,例如有 01、123、2026,每次都重复书写 \d 就不方便了。
这时,我们就可以使用量词。
使用 \w 匹配单词字符
\w 是预定义字符类的简写,用于匹配一个单词字符。
在 Python 默认的 Unicode 模式下,它通常包括:
- 英文字母,例如
a、Z。 - 数字,例如
0、9。 - 下划线
_。 - 许多 Unicode 字母和数字,包括常见的汉字。
例如:
1 | import re |
运行结果:
1 | web_01 |
这里的 \w+ 能够匹配整个 web_01,因为字母、数字和下划线都属于 \w 可以匹配的字符。
需要注意,\w 并不等于“只匹配英文字母”。如果只想匹配英文字母,可以使用 [a-zA-Z]。
使用 \s 匹配空白字符
\s 是预定义字符类的简写,用于匹配一个空白字符,例如:
- 普通空格。
- 制表符
\t。 - 换行符
\n。
例如:
1 | import re |
运行结果:
1 | ' ' |
这里使用 repr(),是为了让空格以更容易观察的形式显示出来。
如果文本中存在多个连续空格,单独使用 \s 仍然只匹配其中一个空白字符。
后面学习量词后,就可以使用 \s+ 匹配一个或多个连续的空白字符。
使用方括号创建字符类
除了 \d、\w 和 \s 等预定义字符类,我们还可以使用方括号 [] 自己指定允许匹配的字符。
例如:
1 | import re |
运行结果:
1 | A |
[ABC] 表示匹配 A、B、C 中的任意一个字符。
假设字符串是:
1 | text = "服务器状态:B" |
那么匹配结果就是 B。
如果字符串是:
1 | text = "服务器状态:D" |
那么 [ABC] 就无法匹配到任何内容。
这里有一个重要规则:方括号中的多个普通字符通常表示任选其中一个,而不是要求这些字符按顺序全部出现。
例如:
| 字符类 | 含义 |
|---|---|
[ABC] | 匹配 A、B、C 中的任意一个 |
[0123456789] | 匹配一个 ASCII 数字 |
[a-z] | 匹配一个小写英文字母 |
[A-Z] | 匹配一个大写英文字母 |
[a-zA-Z] | 匹配一个大小写英文字母 |
[a-zA-Z0-9] | 匹配一个英文字母或 ASCII 数字 |
例如,提取服务器名称中的一个英文字母:
1 | import re |
运行结果:
1 | w |
[a-zA-Z] 只匹配一个英文字母,因此结果是 w,而不是整个 web。
如果想匹配连续的多个英文字母,就需要使用量词。
使用 [^...] 匹配指定字符以外的内容
方括号中的 ^ 在特定位置具有特殊含义。
当 ^ 出现在字符类 [] 的开头时,表示取反,也就是匹配不属于指定字符集合的字符。
例如:
1 | import re |
运行结果:
1 | w |
[^0-9] 表示匹配一个不是 ASCII 数字 0 到 9 的字符。
由于字符串以 w 开头,而 w 不是数字,所以程序首先匹配到 w。
注意,^ 并不是在任何位置都表示取反:
[^0-9]:在字符类内部,位于开头的^表示取反。^abc:在字符类外部,^通常表示字符串开头。
理解这个区别,可以避免后续编写正则表达式时产生混淆。
使用量词规定匹配项的出现次数
前面学习了字符类,可以规定字符需要满足什么条件。
但在实际工作中,我们往往还需要规定匹配项出现多少次。
例如:
- 服务器编号可能有两位、三位或四位数字。
- CPU 使用率可能是
8%、85%或100%。 - 日志中可能存在多个连续空格。
- 服务器名称可能由多个英文字母和数字组成。
如果只能匹配一个字符,就无法方便地处理这些情况。
量词就是用来解决这个问题的。
量词用于限定它前面的匹配项可以出现多少次。
这个匹配项可以是普通字符、字符类、预定义字符类,也可以是分组。
使用 + 匹配一个或多个
+ 表示前面的匹配项至少出现一次,可以出现多次。
例如:
1 | import re |
运行结果:
1 | 85 |
这里的 \d+ 可以拆成两部分:
\d:匹配一个数字字符。+:要求前面的匹配项至少出现一次。
因此,\d+ 可以匹配 8、85、100 等不同长度的连续数字。
再看一个例子:
1 | import re |
运行结果:
1 | ['01', '2026', '03'] |
这里使用了 re.findall(),它会查找所有符合条件的内容,并以列表形式返回结果。
这说明了两个不同的概念:
- 正则表达式
\d+决定匹配什么内容。 findall()决定查找并返回所有匹配结果。
使用 * 匹配零个或多个
* 表示前面的匹配项可以出现零次、一次或者多次。
例如:
1 | import re |
运行结果:
1 | '' |
为什么结果是空字符串?
因为 * 允许前面的匹配项出现零次。
字符串开头的 w 不是数字,但 \d* 可以匹配零个数字,因此它在字符串开头就完成了一次空匹配。
如果把表达式改成 \d+,就必须至少匹配一个数字,不能在开头通过零个数字完成匹配。
例如:
1 | import re |
运行结果:
1 | 01 |
初学时需要特别注意:* 并不表示“必须匹配很多次”,而是表示“允许出现零次或多次”。
使用 ? 匹配零个或一个
? 表示前面的匹配项可以出现零次或一次。
例如,英文中有 color 和 colour 两种拼写。如果希望两种写法都能匹配,可以这样写:
1 | import re |
运行结果:
1 | color |
这里的 u? 表示字母 u 可以出现零次或一次。
因此,这个表达式既可以匹配不包含 u 的 color,也可以匹配包含 u 的 colour。
在处理日志时,也可以使用 ? 处理某些可选字符,例如可选连字符或可选空格。
使用 {m,n} 规定出现次数
有时,我们不希望匹配项出现任意次数,而是希望规定一个明确的范围。
例如,假设某个系统使用六位数字作为验证码,我们希望匹配六个连续的数字。
可以这样写:
1 | import re |
运行结果:
1 | 123456 |
这里的 {6} 表示前面的 \d 必须连续匹配六次。
除此之外,还可以使用以下形式:
| 量词 | 含义 |
|---|---|
{3} | 恰好出现三次 |
{2,4} | 出现两到四次 |
{2,} | 至少出现两次 |
{0,3} | 出现零到三次 |
例如,假设服务器编号由两到四位数字组成:
1 | import re |
运行结果:
1 | 1234 |
这里的 \d{2,4} 表示匹配两到四个连续的数字。
不过要注意,re.search() 只要求找到一个符合模式的子串,并不会自动检查整个字段是否完全符合要求。
例如:
1 | import re |
运行结果:
1 | 1234 |
虽然字符串有五位数字,但正则表达式仍然能够从中找到四位数字作为匹配结果。
如果要求整个字符串必须恰好是两到四位数字,可以使用 re.fullmatch():
1 | import re |
运行结果:
1 | True |
第一个字符串符合规则,第二个字符串不符合规则。
将字符类和量词组合起来使用
前面分别学习了字符类和量词。实际编写正则表达式时,我们通常会把它们组合起来。
例如:
| 正则表达式 | 含义 |
|---|---|
\d+ | 一个或多个连续数字 |
[a-z]+ | 一个或多个连续小写英文字母 |
[A-Z]{2,4} | 两到四个连续大写英文字母 |
\w+ | 一个或多个连续单词字符 |
\s+ | 一个或多个连续空白字符 |
这些表达式的共同点是:先确定匹配项的条件,再用量词规定重复次数。
匹配服务器名称
假设我们有一个简单的服务器清单:
1 | import re |
运行结果:
1 | ['web01', 'app2026', 'db03'] |
这里的正则表达式可以拆成两部分:
[a-z]+:匹配一个或多个连续的小写英文字母。\d+:匹配一个或多个连续的数字字符。
组合起来,便可以匹配先出现英文字母、再出现数字的字符串。
例如,web01 符合规则,app2026 也符合规则。
这里需要区分“提取子串”和“验证完整字段”。
findall() 会从文本中提取符合模式的内容。如果希望验证某个完整的服务器名称是否符合规则,可以使用 fullmatch():
1 | import re |
运行结果:
1 | True |
web01 完全符合规则,而 web01-prod 包含连字符,不能通过当前模式的完整匹配。
提取日志中的 CPU 使用率
假设日志内容如下:
1 | import re |
运行结果:
1 | CPU=85% |
这里出现了一个新的写法:(\d+)。
圆括号可以将表达式的一部分组合起来,并捕获这部分匹配内容。这里的 group(1) 获取第一组,也就是数字 85。
从术语上看:
\d是预定义字符类。+是量词,修饰前面的\d。(\d+)是一个捕获分组。CPU=(\d+)%是完整的正则表达式模式。
因此,量词和分组并不是同一类概念。分组将一部分表达式组合起来,而量词可以修饰整个分组。
例如:
1 | import re |
运行结果:
1 | ababab |
这里的 (ab)+ 表示整个 ab 组合可以连续出现一次或多次,而不是只要求字母 b 重复出现。
分组的详细用法将在下一篇文章中介绍。
匹配连续空白字符
假设我们要处理一份格式不太整齐的文本:
1 | import re |
运行结果:
1 | ' ' |
这里的 \s+ 表示匹配一个或多个连续空白字符。
由于服务器名称与 CPU 信息之间存在五个空格,所以程序匹配到了这五个空格。
在日志清洗中,我们还可以利用 re.sub() 将连续空白替换成一个普通空格:
1 | import re |
运行结果:
1 | web01 CPU=85% |
re.sub() 的作用是替换符合正则表达式的内容。这里把一个或多个连续空白字符替换成一个空格,让文本格式更加整齐。
理解量词的贪婪匹配
学习量词时,还有一个值得理解的概念:贪婪匹配。
默认情况下,*、+ 和 {m,n} 等量词通常采用贪婪匹配行为,也就是在不影响整个表达式匹配成功的前提下,尽可能多地匹配。
例如:
1 | import re |
运行结果:
1 | 1234 |
{2,4} 允许前面的数字匹配两到四次。在这个例子中,正则表达式可以匹配四位数字,因此默认优先得到 1234。
如果希望量词尽可能少地匹配,可以在量词后面添加 ?,形成非贪婪量词:
1 | import re |
运行结果:
1 | 12 |
这里的 {2,4}? 表示匹配两到四位数字,但优先尝试匹配最少的两位数字。
需要注意,非贪婪匹配并不意味着永远只匹配最少字符。如果后续的匹配条件要求更多字符,正则表达式仍可能扩大匹配范围。
另外,贪婪和非贪婪描述的是量词的匹配行为,不是与字符类并列的一种字符类型。
字符类和量词的匹配流程
现在,我们已经认识了字符类、量词,以及它们的组合用法。可以通过下面的流程图理解正则表达式查找匹配内容时的大致过程。
flowchart TD
A["准备待匹配的字符串"] --> B["编写正则表达式模式"]
B --> C["从字符串的某个位置开始尝试"]
C --> D{"当前字符满足匹配项条件吗?"}
D -- "不满足" --> E["尝试其他位置或判定失败"]
D -- "满足" --> F["根据量词检查重复次数"]
F --> G{"整个模式能否匹配成功?"}
G -- "能" --> H["返回匹配结果"]
G -- "不能" --> I["尝试调整匹配范围"]
I --> J{"还有其他尝试方式吗?"}
J -- "有" --> C
J -- "没有" --> K["没有找到匹配结果"]可以把这个过程简单理解为:
- 字符类规定字符条件。 例如,
\d匹配数字,[a-z]匹配小写英文字母。 - 量词规定重复次数。 例如,
+表示至少一次,*表示零次或多次,{3}表示恰好三次。 - 完整模式组合匹配规则。 例如,
[a-z]+\d+要求先匹配一个或多个小写英文字母,再匹配一个或多个数字。 - 匹配函数决定查找方式。 例如,
search()返回第一个匹配结果,findall()返回所有匹配结果,fullmatch()要求整个字符串符合模式。
需要说明的是,流程图展示的是便于初学者理解的简化过程。实际正则表达式引擎还会根据量词、分组、分支等规则进行回溯和其他匹配处理。
常见问题
\d 和 [0-9] 有什么区别?
在 Python 默认的 Unicode 模式下,\d 可以匹配许多 Unicode 十进制数字字符,而 [0-9] 只匹配 ASCII 范围内的数字 0 到 9。
例如,处理标准服务器编号、监控数据或验证码时,如果只希望接受普通的 ASCII 数字,可以使用 [0-9]+。
* 和 + 有什么区别?
两者最大的区别是能否匹配零个字符:
\d*:允许没有数字。\d+:至少需要一个数字。
在查找数字时,如果不希望通过空匹配得到空字符串,通常可以考虑使用 \d+。
为什么 \d+ 没有匹配整个字符串?
因为 \d+ 只描述数字匹配规则,并不要求整个字符串都由数字组成。
例如:
1 | import re |
运行结果:
1 | 01 |
search() 能在 web01 中找到数字 01,但 fullmatch() 要求整个字符串都是数字,因此返回 None。
为什么 \d{6} 仍然可能匹配更长的数字?
因为 \d{6} 只要求匹配六个连续的数字,并不限制前后不能再出现数字。
例如:
1 | import re |
运行结果:
1 | 123456 |
如果要求字符串必须恰好包含六位数字,可以使用:
1 | import re |
运行结果:
1 | True |
这再次说明,编写正则表达式时,不仅要考虑字符条件和重复次数,还要明确自己究竟是要提取一段内容,还是验证整个字符串。
动手练习
练习一:提取所有数字
给定字符串:
1 | text = "web01 app2026 db03" |
要求提取其中所有连续的数字,预期结果:
1 | ['01', '2026', '03'] |
提示:使用 re.findall() 和 \d+。
练习二:匹配三到五位数字
给定字符串:
1 | text = "12345" |
要求匹配三到五位连续数字,预期结果:
1 | 12345 |
提示:可以使用 {m,n} 量词。如果要求整个字符串都符合规则,可以使用 re.fullmatch()。
练习三:提取服务器名称
给定字符串:
1 | text = "web01 app2026 db03" |
要求提取由小写英文字母和数字组成、且先出现字母再出现数字的服务器名称,预期结果:
1 | ['web01', 'app2026', 'db03'] |
提示:尝试组合 [a-z]+ 和 \d+。
写在最后
本文学习了正则表达式中的字符类和量词。
字符类用于规定单个字符需要满足什么条件,例如 [a-zA-Z];\d、\w 和 \s 则是常用的预定义字符类简写。
量词用于限定前面的匹配项可以出现多少次,例如 + 表示一个或多个,* 表示零个或多个,? 表示零个或一个,{m,n} 表示指定次数范围。
将字符类与量词组合起来,就可以完成数字提取、服务器名称匹配、日志清洗等常见任务。
下一篇,我们将继续学习正则表达式中的分组功能,了解如何使用圆括号 () 组织表达式、捕获匹配内容,并通过 group() 获取自己需要的数据。
