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 设置断点并检查程序执行上下文
前面我们已经学习了 Python 的文件读写、列表推导式、字典推导式、生成器以及 zip() 等内容。
现在,假设你正在维护一台 Linux 服务器,需要检查 Nginx 日志,找出所有访问了某个接口的请求。
日志可能长这样:
1 | 192.168.1.10 - - [22/Oct/2026:10:15:30] "GET /index.html HTTP/1.1" 200 |
如果只想查找 /api 相关请求,我们可以使用字符串的 in 操作符:
1 | log = 'GET /api/users HTTP/1.1' |
执行结果:
1 | True |
这种方式可以检查字符串中是否包含指定内容。
但如果需求变得复杂呢?
- 查找所有以
/api开头的请求路径。 - 找出日志中所有的 IP 地址。
- 提取 HTTP 状态码为
500的记录。 - 查找包含数字的字符串。
- 找出符合指定格式的邮箱地址。
如果每种需求都要自己编写一套字符串判断逻辑,代码很容易变得复杂。
有没有一种方法,能够先描述我们要查找的内容具有什么规律,再让 Python 按照这个规律进行匹配?
这就是正则表达式可以发挥作用的地方。
什么是正则表达式
正则表达式(Regular Expression,简称 Regex 或 RegEx)是一种用于描述字符串匹配规则的表达式。
听起来有些抽象,我们可以用一个简单的例子理解。
假设有下面三个字符串:
1 | text1 = 'abc123' |
如果希望找到包含数字的字符串,我们可以直接检查每个字符串中是否出现数字。
但如果希望进一步要求字符串中必须出现连续的三个数字,手动判断就会麻烦一些。
正则表达式允许我们用一种专门的语法来描述这些规则。
例如:
1 | pattern = r'\d{3}' |
这里暂时不需要记住每个符号的含义,只需要知道:这个表达式描述的是连续出现三个数字的规则。
后续文章会详细讲解 \d、{3} 等符号。
可以先把正则表达式理解成一张用于查找内容的规则说明书。
Python 为什么需要 re 模块
Python 提供了内置的 re 模块,用于处理正则表达式。
使用之前,需要先导入模块:
1 | import re |
这行代码的意思是:让当前 Python 程序能够使用 re 模块提供的功能。
这里的 import 可以理解为引入一个工具箱。引入之后,我们就可以调用其中的方法。
例如:
1 | import re |
执行结果类似:
1 | <re.Match object; span=(2, 3), match='c'> |
运行时看到的对象显示信息可能因 Python 版本等因素有所不同,但匹配结果是一致的。
我们来拆解这段代码:
1 | re.search("c", "abcdefg") |
其中:
re:刚才导入的模块。search():在字符串中搜索符合规则的内容。"c":要查找的内容,这里使用的是最简单的正则表达式。"abcdefg":被搜索的字符串。
Python 从字符串中查找字符 c,找到了符合条件的内容,于是返回一个匹配对象。
这个对象不仅表示匹配成功,还保存了匹配内容以及它在字符串中的位置等信息。
注意,search() 返回的不是简单的 True 或 False。
如果没有找到匹配内容,它会返回 None。
理解正则表达式的匹配流程
在继续学习具体方法之前,我们先从整体上了解正则表达式是如何工作的。
假设我们希望从下面这段文本中提取数字:
1 | text = "服务器 web01 的 CPU 使用率为 85%" |
我们编写一条匹配规则:
1 | pattern = r"\d+" |
这条规则表示匹配一个或多个连续数字。
接下来,Python 会使用这条规则检查文本,并根据匹配方法返回结果。
整个过程可以用下面的流程图表示:
flowchart TD
A["准备待检查的字符串"] --> B["编写正则表达式规则"]
B --> C{"是否需要重复使用规则?"}
C -->|是| D["re.compile() 编译规则"]
C -->|否| E["直接调用 re.match()、re.search() 等方法"]
D --> F["调用编译对象的匹配方法"]
E --> G["执行正则表达式匹配"]
F --> G
G --> H{"是否找到匹配内容?"}
H -->|是| I["获取匹配结果"]
H -->|否| J["返回 None 或空列表"]
I --> K["提取、显示或进一步处理数据"]这张图展示的是正则表达式从规则定义到结果处理的大致过程。
需要注意,re.compile() 并不是每次匹配都必须执行的步骤。
如果只是简单地搜索一次内容,可以直接使用 re.search()。如果需要反复使用同一条规则,可以先使用 re.compile() 创建正则表达式对象。
另外,不同匹配方法的失败结果也有所不同:
re.match()和re.search():没有找到匹配内容时返回None。re.findall():没有找到匹配内容时返回空列表[]。
了解这一点之后,再学习具体方法就会更加容易。
第一次使用正则表达式
先从最简单的例子开始。
创建一个文件 re_demo.py,输入以下代码:
1 | import re |
在终端执行:
1 | python3 re_demo.py |
执行结果:
1 | 找到了匹配内容 |
这段代码完成了一个完整的搜索过程。
首先,程序将字符串 Python is useful 保存到变量 text 中。
接着,re.search() 在这个字符串中查找 Python。
如果找到了,返回匹配对象,并将对象保存到变量 result 中;如果没有找到,result 就是 None。
然后,程序通过 if result 判断搜索是否成功。
最后,result.group() 取出真正匹配到的文本。
这里需要认识一个新的方法:group()。
它的作用是获取匹配结果中的文本内容。对于我们目前学习的简单匹配,可以先把它理解为“取出找到的内容”。
使用 search 查找字符串
re.search() 是学习正则表达式时最常用的方法之一。
它会从字符串开头开始搜索,找到第一个符合规则的位置后,就返回匹配结果。
例如:
1 | import re |
执行结果:
1 | 123 |
这里的 r"\d+" 是一个正则表达式。
暂时只需要知道,它能够匹配一个或多个连续数字。后续文章会进一步讲解其中的符号。
字符串中有 123 和 456 两组数字,为什么只输出 123?
因为 search() 找到第一处符合条件的内容后,就会停止搜索并返回结果。
如果想找出所有符合条件的内容,就需要使用其他方法。
使用 findall 查找所有匹配内容
假设我们需要从一段文本中提取所有数字:
1 | import re |
执行结果:
1 | ['01', '80', '02', '120'] |
这里的 findall() 会查找所有符合规则的内容,并将匹配结果放进一个列表中。
为什么结果是一个列表?
因为我们可能找到多个数字,而列表正好可以保存多个数据。
需要注意,findall() 返回的是字符串列表,而不是整数列表。如果之后需要进行数学计算,还需要根据实际情况使用 int() 等方法进行类型转换。
例如:
1 | import re |
执行结果:
1 | 1 |
这里出现了一个容易让初学者困惑的细节:web01 中的 01 转换成整数后变成了 1。
这是因为整数不会保留数字开头的零。如果需要保留原始格式,就应该继续使用字符串,不要急着转换成整数。
使用 match 从字符串开头匹配
re.match() 也是用于匹配字符串的方法,但它与 search() 有一个重要区别。
match() 只尝试从字符串的开头进行匹配。
看下面的例子:
1 | import re |
执行结果:
1 | Python |
因为字符串开头就是 Python,所以匹配成功。
再看一个例子:
1 | import re |
执行结果:
1 | None |
虽然字符串中确实存在 Python,但它不是从开头开始的,因此 match() 返回 None。
如果把同样的例子改成 search():
1 | import re |
执行结果:
1 | Python |
这次就能找到内容了。
对比三种常用方法
| 方法 | 作用 | 返回结果 |
|---|---|---|
re.match() | 从字符串开头尝试匹配 | 匹配对象或 None |
re.search() | 搜索第一处匹配内容 | 匹配对象或 None |
re.findall() | 查找所有匹配内容 | 列表 |
可以使用下面的流程图理解它们的区别:
flowchart TD
A["需要查找字符串"] --> B{"希望如何查找?"}
B --> C["从字符串开头匹配"]
B --> D["查找第一处匹配"]
B --> E["查找所有匹配"]
C --> F["re.match()"]
D --> G["re.search()"]
E --> H["re.findall()"]实际工作中,不需要死记硬背所有方法。只要先明确自己需要从哪里开始查找,以及需要一个结果还是多个结果,就更容易选择合适的方法。
编译正则表达式:使用 re.compile()
前面我们一直采用这样的方式进行匹配:
1 | import re |
这种写法很直接:把正则表达式和需要检查的字符串一起传给 re.search()。
但是,假设我们需要检查 100 条服务器日志,每条日志都要使用同一条规则查找数字,应该怎么办?
一种方式是反复调用 re.search(),每次都传入相同的正则表达式。
另一种方式是:先把正则表达式编译成一个可以重复使用的规则对象,再用这个对象检查不同的字符串。
Python 提供了 re.compile() 方法来完成这件事。
先理解什么是编译
在这里,可以先把“编译正则表达式”理解成:将我们写下的匹配规则交给 Python 处理,得到一个可以反复使用的正则表达式对象。
例如:
1 | import re |
这段代码做了两件事:
r"\d+"是我们编写的正则表达式规则。re.compile()将这条规则处理成一个正则表达式对象,并保存到变量pattern中。
这里的 pattern 不再是普通的字符串,而是一个可以调用匹配方法的对象。
可以通过 type() 查看它的类型:
1 | import re |
执行结果:
1 | <class 're.Pattern'> |
看到 re.Pattern,就说明 pattern 保存的是一个正则表达式对象。
注意,这里所说的编译是正则表达式的编译,不是把整个 Python 程序编译成可执行文件。
使用编译后的规则匹配字符串
得到正则表达式对象之后,应该如何使用?
我们可以直接调用对象提供的 search() 方法:
1 | import re |
执行结果:
1 | 01 |
为什么输出的是 01,而不是 85?
因为 search() 返回的是第一处匹配结果。字符串中的 web01 已经包含数字,所以程序找到 01 后就停止搜索了。
如果我们只想提取 CPU 使用率,可以在后续学习字符类、量词等知识后,再编写更加精确的匹配规则。
这里最重要的是理解调用方式的变化。
直接使用模块的方法:
1 | re.search(r"\d+", text) |
先编译,再使用对象的方法:
1 | pattern = re.compile(r"\d+") |
两种方式都可以完成匹配。区别在于,第二种方式把规则单独保存了下来,可以重复使用。
使用同一条规则检查多条日志
现在,我们把编译正则表达式的方式放到实际的运维场景中。
假设需要检查多条服务器日志,并提取每条日志中的第一组数字。
1 | import re |
执行结果:
1 | web01 CPU 使用率 75% 匹配到的数字: 01 |
我们逐步理解这段程序。
首先,re.compile(r"\d+") 创建了一个正则表达式对象,并保存到 pattern 中。
接着,logs 列表保存了三条模拟日志。
然后,for 循环逐条取出日志,每次都调用 pattern.search(log)。
虽然检查了三条不同的日志,但使用的始终是同一个正则表达式对象,不需要在循环内部重新编写匹配规则。
这个例子展示了 re.compile() 的主要使用价值:将匹配规则和待检查的数据分开,让同一条规则可以在多个地方重复使用。
编译后的对象也能使用其他匹配方法
前面学习的 search()、match() 和 findall(),不仅可以通过 re 模块调用,也可以通过编译得到的正则表达式对象调用。
例如:
1 | import re |
执行结果:
1 | 第一处匹配: 01 |
这里需要特别注意:编译正则表达式不会改变匹配规则本身。
pattern.search() 仍然是搜索第一处匹配,pattern.match() 仍然只从字符串开头尝试匹配,pattern.findall() 仍然会返回所有匹配内容。
编译只是提供了另一种组织和使用正则表达式的方式。
re.compile() 一定更快吗
刚接触 re.compile() 时,很容易认为只要使用它,程序运行速度就一定会更快。
实际上,不应该这样理解。
当我们使用 re.search() 等方法时,Python 的 re 模块会缓存近期使用过的一些正则表达式,以减少重复处理规则的开销。因此,对于简单的匹配任务,直接调用模块方法通常已经足够。
显式使用 re.compile() 的主要优势是:
- 规则可以重复使用: 不需要在多个位置反复编写同一条规则。
- 代码更清晰: 可以使用
pattern等变量名明确表示这是一条匹配规则。 - 适合批量处理: 在循环中检查大量日志时,规则可以在循环外统一定义。
- 便于维护: 当匹配规则发生变化时,可以集中修改规则定义。
当然,如果程序确实存在大量正则表达式匹配操作,性能是否改善还要结合实际代码进行测试。
对于初学者来说,先掌握如何定义和复用规则,比过早追求性能优化更加重要。
为什么正则表达式前面经常写字母 r
在前面的代码中,我们经常看到这样的写法:
1 | pattern = r"\d+" |
为什么字符串前面多了一个 r?
因为 Python 普通字符串中的反斜杠 \ 还可能用于表示特殊字符。
例如:
1 | text = "hello\nworld" |
执行结果:
1 | hello |
这里的 \n 表示换行。
但是,正则表达式也大量使用反斜杠来表示特殊规则。如果每次都需要考虑 Python 字符串本身的转义规则,书写起来就容易混淆。
因此,正则表达式通常使用原始字符串:
1 | pattern = r"\d+" |
字符串前面的 r 表示原始字符串,能够让反斜杠按照原始字符串的规则保留下来,而不是把 \d 当作 Python 字符串中的转义序列处理。
对于初学者来说,可以先养成一个习惯:
编写正则表达式时,优先使用 r"..." 的形式。
这并不意味着所有正则表达式字符串都必须以 r 开头,而是这种写法通常更清晰,也更不容易遇到转义问题。
使用正则表达式提取 Linux 日志信息
前面我们提到过 Nginx 日志,现在尝试将正则表达式用于实际的运维场景。
假设我们拿到了一条简化后的访问日志:
1 | log = '192.168.1.12 - - "POST /api/login HTTP/1.1" 500' |
现在,我们想提取其中的 IP 地址和 HTTP 状态码。
可以先使用最简单的方式分别查找:
1 | import re |
执行结果:
1 | 客户端 IP: 192.168.1.12 |
这里的正则表达式稍微复杂了一些,但可以先从整体理解。
r"\d+\.\d+\.\d+\.\d+" 用于查找由四段数字组成、段与段之间使用点号分隔的内容。
需要注意,这只是一个简化的 IP 地址匹配规则,并不能严格验证每一段是否处于合法范围内。
r"\s(\d{3})$" 用于查找字符串末尾的三位状态码,并通过括号保存其中的匹配内容。
这里使用了 group(1),而不是 group()。
group()或group(0):获取整个匹配结果。group(1):获取第一个括号捕获的内容。
例如,整个匹配可能包含状态码前面的空格,但 group(1) 只会返回 500。
括号、\s、\d 和 $ 的具体含义会在后续内容中逐步学习,现在不必一次全部记住。
还需要注意,这个例子假设日志末尾就是状态码。如果日志格式不同,就需要相应调整匹配规则。
初学正则表达式时需要注意什么
正则表达式很灵活,但它并不是越复杂越好。
刚开始学习时,建议注意以下几点。
不要把匹配对象当成字符串。
例如:
1 | import re |
第一行输出的是匹配对象的信息,第二行才是匹配到的字符串。
注意匹配失败的情况。
如果没有匹配到内容,search() 和 match() 都会返回 None。直接调用 result.group() 就可能产生 AttributeError 异常。
所以,在获取匹配内容之前,应该先检查是否匹配成功。
不要认为匹配到了就代表数据一定合法。
例如,简单的 IP 地址正则表达式可能匹配到 999.999.999.999 这样的内容。因此,格式匹配和数据合法性校验是两个不同的问题。
对于复杂的数据校验,应该根据实际需求选择更可靠的验证方式。
理解编译规则与匹配数据的区别。
re.compile() 负责创建正则表达式对象,而 pattern.search(text) 等方法负责使用规则检查具体的字符串。
规则可以重复使用,但每次匹配的结果取决于传入的字符串。
动手练习
现在通过几个小练习检查自己是否理解了今天的内容。
练习:查找字符串中的数字
1 | import re |
请先自己猜测输出结果,再运行程序验证。
参考输出:
1 | ['01', '85'] |
练习:查找第一个匹配内容
1 | import re |
参考输出:
1 | Python |
虽然字符串中出现了两次 Python,但 search() 只返回第一处匹配结果。
练习:比较 match 和 search
1 | import re |
参考输出:
1 | None |
第二行中匹配对象的具体显示内容会因 Python 版本等因素有所不同。
思考一下:为什么同一个字符串,使用两种方法会得到不同结果?
原因就在于 match() 只从字符串开头尝试匹配,而 search() 会在整个字符串中查找。
练习:使用 compile 重复利用规则
创建一个文件 re_compile_demo.py,输入以下代码:
1 | import re |
执行程序:
1 | python3 re_compile_demo.py |
参考输出:
1 | web01 -> 01 |
思考以下问题:
pattern保存的是什么?- 为什么不需要在循环中反复调用
re.compile()? - 如果把
pattern.search(text)改成re.search(r"\d+", text),还能得到匹配结果吗?
答案分别是:pattern 保存编译后的正则表达式对象;同一条规则可以重复使用;直接调用 re.search() 也能完成匹配。
写在最后
本文学习了 Python 内置的 re 模块,了解了正则表达式的基本用途,以及如何使用 match()、search() 和 findall() 查找字符串中的内容。
此外,我们还学习了 re.compile(),知道如何将匹配规则编译成正则表达式对象,并在多条日志或多段文本中重复使用。
通过正则匹配流程图,我们梳理了从编写规则、选择匹配方法,到判断结果和提取内容的基本过程。
最后,我们尝试从 Linux 运维日志中提取客户端 IP 地址和 HTTP 状态码,初步体验了正则表达式在自动化运维中的实际价值。
不过,目前我们只是使用了一些基础匹配规则,还没有系统学习正则表达式中的各种符号。
下一篇将学习正则表达式中的字符类与量词,逐步理解如何匹配数字、字母、指定字符,以及如何规定某种字符出现一次、几次或任意多次。
掌握这些基础之后,我们就能够编写更加灵活的匹配规则,从文本和日志中提取需要的信息。
