Python 系列(六十) 掌握 re 模块的常用函数
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 设置断点并检查程序执行上下文
在前面的文章中,我们已经学习了正则表达式的基础知识,包括如何使用字符类、量词和分组来描述需要查找的文本,也学习了如何使用 re.search()、re.match() 等方法进行匹配。
那么,学会编写正则表达式之后,我们能用它来做什么呢?
在 Linux 运维工作中,我们经常需要从大量文本中提取有用的信息。例如:
- 从 Nginx 访问日志中提取客户端 IP 地址。
- 从服务器巡检结果中提取 CPU 使用率和内存使用率。
- 从应用程序日志中查找错误编号。
- 从一段混杂着字母和数字的文本中提取所有数字。
如果只需要判断某个字符串是否存在,匹配一次可能就够了。但如果需要提取所有符合条件的内容、替换错误信息,或者按照某种规则拆分字符串,就需要使用更多正则表达式函数。
Python 提供的 re 模块就包含了这些常用工具。
本文将从一个简单的文本提取任务出发,逐步学习 re 模块中的常用函数,了解它们各自的用途,以及在什么情况下应该选择哪一个。
准备工作:导入 re 模块
在使用 re 模块之前,需要先导入它。
1 | import re |
这里的 import 是 Python 的导入语句,作用是让当前程序能够使用指定模块提供的功能。
re 是 Python 标准库的一部分,安装 Python 后通常就可以直接使用,不需要额外安装第三方软件包。
接下来,我们准备一段简单的文本:
1 | import re |
这段文本模拟了一次服务器巡检结果。
假设我们希望从中提取所有数字,应该怎么做?
这里需要注意:正则表达式中的 \d 表示一个数字字符,而 + 表示前面的规则可以连续出现一次或多次。
因此,r'\d+' 表示匹配一个或多个连续的数字字符。
前面的 r 表示使用原始字符串,方便我们编写正则表达式中的反斜杠。
使用 re.findall() 提取所有匹配内容
在处理文本时,我们经常不只需要找到第一个结果,而是希望把所有符合条件的内容都提取出来。
这时就可以使用 re.findall()。
先看一个简单示例:
1 | import re |
运行程序,输出如下:
1 | ['1', '22', '333'] |
这段代码是如何执行的?
首先,re.findall() 接收两个主要参数:
- 第一个参数
r'\d+'是正则表达式,规定需要查找什么内容。 - 第二个参数
'a1b22c333'是需要检查的原始字符串。
Python 从左向右检查字符串,寻找连续的数字字符。
遇到 1 时,找到一个匹配结果;接着遇到 22,找到第二个结果;最后遇到 333,找到第三个结果。
所有匹配结果会按照出现的顺序放进一个列表中。
因此,re.findall() 的返回值是一个列表,即使只找到一个结果,返回值仍然是列表;如果没有找到任何结果,就会返回空列表 []。
例如:
1 | import re |
输出:
1 | ['01'] |
第一个结果中的 '01' 来自字符串 'server01' 末尾的两个数字。正则表达式 \d+ 会连续匹配数字,因此会把 01 作为一个整体返回。
第二个字符串中没有数字,所以返回空列表。
需要注意,findall() 返回的是匹配到的文本,而不是自动转换后的数字。
例如,结果中的 '01' 是字符串,不是整数。如果后续需要进行数学计算,可以再使用 int() 进行转换。
1 | import re |
输出:
1 | [65, 78, 82] |
这里使用列表推导式,把提取到的每个数字字符串转换为整数。列表推导式的具体用法可以结合之前学习的相关内容理解。
实战:提取服务器巡检数据
回到前面的服务器巡检场景。
假设我们需要从文本中提取 CPU、内存和磁盘使用率,可以使用下面的代码:
1 | import re |
输出:
1 | ['65%', '78%', '82%'] |
这里使用的正则表达式是 r'\d+%'。
它表示先匹配一个或多个数字字符,再匹配一个百分号。
与之前的 r'\d+' 相比,新表达式会把百分号也包含在匹配结果中。
如果我们希望继续提取数值,可以使用捕获组:
1 | import re |
输出:
1 | ['65', '78', '82'] |
这里的圆括号 () 创建了一个捕获组。
当正则表达式中只有一个捕获组时,findall() 返回的是每次匹配中该捕获组的内容,而不是完整匹配内容。
这也是使用 findall() 时需要特别注意的地方。
如果需要同时提取指标名称和使用率,可以进一步编写:
1 | import re |
输出:
1 | [('CPU', '65'), ('内存', '78'), ('磁盘', '82')] |
这里有两个捕获组:
(CPU|内存|磁盘):匹配指标名称。(\d+):匹配使用率的数字部分。
由于存在两个捕获组,findall() 会将每次匹配得到的捕获内容组合成一个元组,再把所有元组放入列表中。
这个示例展示了正则表达式如何从一段普通文本中提取结构化的信息。不过,实际生产环境中的日志格式可能更复杂,编写规则时需要根据真实文本进行验证。
使用 re.search() 查找第一个匹配结果
有时候,我们并不需要提取所有匹配结果,只需要找到第一个符合条件的内容。
例如,我们想知道一条日志中是否出现了错误编号,以及第一个错误编号是什么。
这时可以使用 re.search()。
1 | import re |
输出类似如下内容:
1 | <re.Match object; span=(29, 32), match='503'> |
这个输出不是普通字符串,而是一个匹配对象。
匹配对象中包含匹配到的内容、匹配位置等信息。
这里的 503 来自日志中的第一个错误编号。由于 re.search() 只返回第一个匹配结果,因此不会继续返回后面的 504。
如果我们只想获取匹配到的文本,可以调用 group() 方法:
1 | import re |
输出:
1 | 503 |
这里使用 if result 检查是否找到了匹配结果。
如果找到了,result 就是一个匹配对象,可以调用 group() 获取完整匹配内容;如果没有找到,re.search() 会返回 None,因此不能直接对它调用 group()。
例如:
1 | import re |
输出:
1 | 没有找到数字 |
re.search() 与 re.findall() 的主要区别在于:
re.search()返回第一个匹配对象,适合只关心第一个结果的场景。re.findall()返回所有匹配结果组成的列表,适合需要批量提取的场景。
使用 re.match() 检查字符串开头
假设我们正在编写一个简单的日志处理程序,需要判断一行日志是否以 ERROR 开头。
这时可以使用 re.match()。
1 | import re |
第一条语句会返回匹配对象,第二条语句会返回 None。
原因是 re.match() 只从字符串的开头开始匹配。
即使第二条文本中也包含 ERROR,由于它出现在 INFO 后面,re.match() 也不会继续向后查找。
如果需要判断一行日志是否以 ERROR 开头,可以这样写:
1 | import re |
输出:
1 | 这是一条错误日志 |
如果你想查找字符串中任意位置的 ERROR,则应该使用 re.search()。
简单来说,match() 关注开头,search() 会在整个字符串中寻找第一个匹配结果。
使用 re.fullmatch() 检查整个字符串
有些场景不仅要求字符串开头符合规则,也要求整个字符串都符合规则。
例如,我们想检查用户输入的服务器编号是否只包含字母和数字。
可以使用 re.fullmatch():
1 | import re |
第一条语句返回匹配对象,第二条语句返回 None。
这里的 [A-Za-z0-9]+ 表示一个或多个英文字母或数字。
web01 完全符合规则,而 web01! 中包含感叹号,因此整个字符串不符合要求。
如果希望输出更容易理解的信息,可以写成:
1 | import re |
输出:
1 | 服务器编号格式正确 |
re.fullmatch() 适合检查一个字符串是否完整符合指定格式,例如简单的编号格式、内部标识符格式等。
不过,真实业务中的输入校验通常还需要检查长度、业务规则以及其他约束,不能只依赖正则表达式。
使用 re.sub() 替换匹配内容
前面学习的函数主要用于查找和提取文本。
如果需要把文本中符合规则的内容替换掉,就可以使用 re.sub()。
例如,日志中包含多个 IP 地址,我们希望把它们替换为统一的占位符,方便演示日志内容。
1 | import re |
输出:
1 | 客户端 [IP地址] 访问服务器,备用客户端 [IP地址] |
这里的 re.sub() 接收三个主要参数:
- 第一个参数是正则表达式,表示需要查找的内容。
- 第二个参数是替换内容,即
[IP地址]。 - 第三个参数是需要处理的原始字符串。
程序会查找所有符合规则的内容,并将它们替换为指定文本。
需要注意,这个正则表达式只是用于演示文本替换,并不能严格验证 IPv4 地址是否合法。例如,某些超出有效范围的数字组合也可能匹配成功。
如果只想替换第一次出现的匹配内容,可以使用 count 参数:
1 | import re |
输出:
1 | WARNING error error |
count=1 表示最多替换一次。默认情况下,count=0 表示不限制替换次数,也就是替换所有匹配内容。
使用 re.split() 按规则拆分字符串
在日常运维中,文本不一定总是使用固定的分隔符。
例如,一份数据可能使用逗号、空格或者分号分隔字段。如果只使用普通字符串的 split(),就需要提前确定分隔符。
而 re.split() 可以使用正则表达式描述分隔规则。
看下面的例子:
1 | import re |
输出:
1 | ['web01', 'web02', 'web03', 'web04'] |
这里的 [,;\s]+ 表示匹配一个或多个逗号、分号或空白字符。
其中:
,表示逗号。;表示分号。\s表示空白字符,例如空格和制表符。+表示上述分隔字符可以连续出现一次或多次。
因此,无论服务器名称之间使用逗号、分号还是空格分隔,程序都能按照规则将它们拆分开。
需要注意,re.split() 默认不会把匹配到的分隔符放入结果列表中。如果正则表达式的分隔规则中包含捕获组,则匹配到的分隔符也会出现在结果中。
使用 re.compile() 重复使用正则表达式
前面的示例都是直接将正则表达式写在函数参数中。
这种方式简单直观,适合刚开始学习时使用。
但如果一个程序需要多次使用同一条正则表达式,例如反复检查多行服务器日志,那么可以使用 re.compile() 先编译正则表达式,再重复调用它的方法。
1 | import re |
输出:
1 | ['01'] |
这里的 re.compile(r'\d+') 会返回一个正则表达式对象,并将它保存到变量 pattern 中。
之后,我们可以调用这个对象的 findall() 方法处理不同的字符串,而不必每次都重新写出正则表达式。
同样,也可以使用 search()、match() 和 fullmatch() 等方法:
1 | import re |
输出:
1 | ERROR503 |
使用 re.compile() 可以让代码更清晰,尤其适合在多个地方重复使用同一条规则的场景。
不过,是否使用 compile(),需要结合程序结构和实际需求决定。对于简单的一次性匹配,直接调用 re 模块中的函数通常就足够了。
使用 re.finditer() 逐个获取匹配对象
re.findall() 适合一次性收集所有匹配内容,但它返回的是字符串列表或元组列表。
如果我们不仅想知道匹配到了什么,还希望获取每个结果在原始字符串中的位置,可以使用 re.finditer()。
例如:
1 | import re |
输出:
1 | 65% 14 17 |
这里的正则表达式是 r'\d+%',只匹配由数字和百分号组成的内容。因此,匹配结果是 65% 和 78%,而不是服务器名称中的 01 和 02。
为什么匹配位置分别是 14 到 17、34 到 37?
因为 Python 字符串的索引从 0 开始,而且中文字符在 Python 字符串中也占一个索引位置。空格、英文字母、数字和标点符号同样各占一个位置。
我们可以把这段文本按字符索引展开:
1 | text = "web01 的 CPU 为 65%,web02 的 CPU 为 78%" |
第一个匹配结果 65% 的位置如下:
1 | 索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
为了更准确地核对位置,可以让 Python 直接输出每个字符的索引:
1 | import re |
输出中的字符索引从 0 开始。repr(char) 用于显示字符的可见表示,遇到空格时会显示为 ' ',便于辨认。
对于这段文本,正确的匹配位置为:
1 | 匹配内容: 65% |
这里需要理解 start() 和 end() 的区别:
start()返回匹配内容第一个字符的索引。end()返回匹配内容结束位置的后一位索引,不包含在匹配范围内。
因此,第一个匹配结果 65% 实际包含索引 16、17 和 18,而 end() 返回 19。
同理,第二个匹配结果 78% 包含索引 36、37 和 38,所以结束位置是 39。
使用捕获组提取服务器名称和 CPU 使用率
如果希望提取完整的服务器名称和 CPU 使用率,可以进一步调整表达式:
1 | import re |
输出:
1 | 完整匹配: web01 的 CPU 为 65% |
这次匹配的范围与上一个示例不同。
上一个示例使用 r'\d+%',只匹配百分比,例如 65%,所以位置范围是 16 到 19。
当前示例使用 r'(web\d+).*?(\d+)%',匹配的是从服务器名称开始,一直到 CPU 百分比结束的整段文本。因此,第一个完整匹配从索引 0 开始,到索引 19 结束;第二个完整匹配从索引 20 开始,到索引 39 结束。
两个捕获组分别负责提取:
(web\d+):匹配服务器名称,例如web01、web02。(\d+):匹配 CPU 使用率的数字部分,例如65、78。
这里的 .*? 表示以尽可能短的匹配方式寻找中间内容,直到后面的数字和百分号规则能够匹配成功。
需要注意,start() 和 end() 默认描述的是整个匹配结果的位置。如果需要获取捕获组的位置,可以使用 start(1)、end(1) 等方法。
例如:
1 | import re |
输出:
1 | 完整匹配: web01 的 CPU 为 65% |
这里 start(1) 和 end(1) 返回第一个捕获组 web01 的位置范围;start(2) 和 end(2) 返回第二个捕获组 65 的位置范围。
通过这些示例可以看出,匹配位置始终取决于正则表达式实际匹配的内容,而不是字符串中所有数字的位置。
re 模块常用函数对照
学习了这么多函数之后,我们可以将它们放在一起比较,方便以后根据实际需求选择。
| 函数 | 主要用途 | 返回结果 |
|---|---|---|
re.match() | 从字符串开头尝试匹配 | 匹配对象或 None |
re.search() | 查找第一个匹配结果 | 匹配对象或 None |
re.fullmatch() | 检查整个字符串是否符合规则 | 匹配对象或 None |
re.findall() | 提取所有匹配结果 | 列表 |
re.finditer() | 逐个获取所有匹配对象 | 迭代器 |
re.sub() | 替换匹配内容 | 替换后的字符串 |
re.split() | 按匹配规则拆分字符串 | 列表 |
re.compile() | 创建可重复使用的正则表达式对象 | 正则表达式对象 |
可以根据下面的思路选择函数:
flowchart TD
A["需要处理一段文本"] --> B{"想完成什么任务?"}
B --> C["判断或查找"]
B --> D["提取内容"]
B --> E["修改或拆分"]
C --> F["match / search / fullmatch"]
D --> G["findall / finditer"]
E --> H["sub / split"]
F --> I["根据匹配范围选择函数"]
G --> J["根据是否需要匹配对象选择函数"]
H --> K["替换内容或拆分字符串"]这张图可以帮助我们建立一个初步的选择思路。实际使用时,还需要结合是否只查找第一个结果、是否需要捕获组、是否需要获取匹配位置等具体要求来决定。
动手练习:提取服务器信息
现在,我们把前面学习的内容组合起来,完成一个小练习。
假设有以下服务器巡检记录:
1 | logs = [ |
每条记录都包含服务器名称、CPU 使用率和内存使用率。
请尝试完成以下任务:
- 使用
re.search()提取第一条记录中的服务器名称。 - 使用
re.findall()提取第一条记录中的所有百分比数值。 - 使用
re.sub()将第一条记录中的百分比替换为[已隐藏]。 - 使用
re.finditer()找出第二条记录中所有百分比的位置。
可以先独立编写代码,再参考下面的答案。
参考答案
1 | import re |
输出:
1 | 服务器名称: web01 |
这里的 logs[1] 表示列表中的第二条记录:
1 | "web02 CPU=88% MEMORY=91%" |
列表的索引从 0 开始,因此 logs[0] 是第一条记录,logs[1] 是第二条记录。
在第二条记录中,88% 从索引 10 开始,到索引 13 结束;91% 从索引 20 开始,到索引 23 结束。
这些位置是相对于 logs[1] 这条字符串计算的,而不是相对于整个 logs 列表,也不是相对于其他日志记录。
这里的 r'\d+(?=%)' 使用了正向先行断言 (?=%),表示数字后面必须紧跟 %,但百分号本身不包含在匹配结果中。
因此,提取结果是 ['65', '72'],而不是 ['65%', '72%']。
如果希望连百分号一起提取,直接使用 r'\d+%' 即可。
通过这个练习,我们把查找、提取、替换和获取匹配位置等操作串联起来,体验了正则表达式在服务器巡检文本处理中的基本用法。
写在最后
本文学习了 Python re 模块中的常用函数,包括查找、提取、替换、拆分以及重复使用正则表达式等操作。
刚开始学习时,不需要死记所有函数。更重要的是先明确自己要解决什么问题,再选择合适的工具:
- 只想找到第一个结果,可以考虑
re.search()。 - 只想检查字符串开头,可以考虑
re.match()。 - 需要检查整个字符串是否符合规则,可以考虑
re.fullmatch()。 - 需要提取所有匹配内容,可以考虑
re.findall()。 - 需要获取匹配内容及其位置,可以考虑
re.finditer()。 - 需要替换文本,可以考虑
re.sub()。 - 需要按照规则拆分文本,可以考虑
re.split()。
特别是使用 finditer() 时,要区分完整匹配和捕获组:group() 获取整个匹配内容,group(1)、group(2) 等获取对应捕获组的内容;start() 和 end() 默认返回整个匹配内容的起止位置,传入组编号后则可以查询相应捕获组的位置。
正则表达式的价值不只是匹配几个字符,更重要的是帮助我们从大量文本中提取有用信息,为后续的数据分析和自动化处理打下基础。
下一篇,我们将学习如何在 Python 中集成、处理和输出 JSON 格式的数据,让程序能够读取和生成常见的结构化数据。
