4、Python正则模块:re模块核心函数与编译正则对象
正则表达式这东西,说白了就是文本处理的瑞士军刀。我在运维日志分析这行干了这么多年,可以负责任地告诉你——不会正则,你连日志的毛都摸不着。今天咱们就把 Python 的 re 模块掰开揉碎了讲清楚。
4.1 先搞明白:正则到底在干嘛?
想象一下,你面前有 10 万行 Nginx 日志。你想找出所有 5xx 错误,还想知道每个错误对应的 URL。用手翻?别闹了。正则就是帮你用一套「模式字符串」去匹配、提取、替换文本的工具。
Python 的 re 模块提供了几个核心函数,我平时用得最多的就是这四个:match、search、findall、sub。再加上编译正则对象,基本能覆盖 90% 的日志处理场景。
核心概念:正则不是 Python 独有的,但 Python 的 re 模块把正则的威力封装得特别顺手。你只要记住——模式字符串 + 目标字符串 = 匹配结果。
4.2 match:从开头死磕
re.match() 这个函数,它的脾气很倔——只从字符串的开头开始匹配。开头对不上,直接返回 None,后面写得再花哨也没用。
import re
log_line = "2024-01-15 10:30:45 ERROR [app] Connection timeout"
pattern = r"\d{4}-\d{2}-\d{2}" # 匹配日期
result = re.match(pattern, log_line)
if result:
print(f"匹配到: {result.group()}")
else:
print("没匹配到")
# 输出: 匹配到: 2024-01-15
我在项目里遇到过一种情况:有人用 match 去匹配日志中间的错误码,结果死活匹配不上。为啥?因为 match 只看开头。你想想看,日志开头通常是时间戳,错误码在中间,用 match 当然不行。这时候就该换 search 了。
注意:match 只匹配开头。如果你不确定目标文本在字符串的什么位置,别用 match,用 search。
4.3 search:满世界找第一个
re.search() 就灵活多了。它会扫描整个字符串,找到第一个匹配的位置就停下来。返回的也是一个匹配对象,用法和 match 一样。
log_line = "2024-01-15 10:30:45 ERROR [app] HTTP 500 - Internal Server Error"
pattern = r"HTTP \d{3}" # 匹配 HTTP 状态码
result = re.search(pattern, log_line)
if result:
print(f"找到状态码: {result.group()}")
else:
print("没找到")
# 输出: 找到状态码: HTTP 500
我个人习惯是:只要不是确定要从开头匹配,一律用 search。它比 match 宽容得多,不容易踩坑。
4.4 findall:一网打尽
有时候你需要的不只是第一个匹配,而是所有匹配。比如从一整天的日志里提取所有 IP 地址。这时候 re.findall() 就派上用场了。
log_text = """
192.168.1.1 - - [15/Jan/2024:10:30:45] "GET /api/users HTTP/1.1" 200
10.0.0.5 - - [15/Jan/2024:10:31:12] "POST /api/login HTTP/1.1" 401
172.16.0.3 - - [15/Jan/2024:10:32:01] "GET /api/data HTTP/1.1" 500
"""
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" # 匹配 IP 地址
ip_list = re.findall(pattern, log_text)
print(f"共找到 {len(ip_list)} 个 IP 地址:")
for ip in ip_list:
print(f" - {ip}")
# 输出:
# 共找到 3 个 IP 地址:
# - 192.168.1.1
# - 10.0.0.5
# - 172.16.0.3
findall 返回的是一个列表。如果正则里没有分组,就返回匹配到的完整字符串列表。如果有分组,就返回元组列表。这个细节我吃过亏——有一次我写了个带分组的正则,以为返回的是字符串列表,结果遍历的时候直接报错。
小技巧:用 findall 的时候,如果正则里有分组,返回的是元组列表。想要完整匹配?用 ?: 把分组变成非捕获组,比如 (?:\d{3})。
4.5 sub:替换的艺术
日志分析不只是提取,有时候还需要替换。比如把日志里的敏感信息脱敏,或者把时间格式统一。这时候 re.sub() 就是你的好帮手。
log_line = "用户 张三 登录失败,IP: 192.168.1.100,时间: 2024-01-15 10:30:45"
# 把 IP 地址替换成 ***.***.***.***
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
replacement = "***.***.***.***"
safe_log = re.sub(pattern, replacement, log_line)
print(safe_log)
# 输出: 用户 张三 登录失败,IP: ***.***.***.***,时间: 2024-01-15 10:30:45
sub 的第四个参数 count 可以控制替换次数。默认是 0(全部替换)。我曾经在替换日志里的密码时忘了设 count,结果把同一行里所有看起来像密码的东西全换了,搞得调试了半天。嗯,这里要注意——count 设成 1 只替换第一个匹配,更安全。
4.6 编译正则对象:性能与复用
如果你要在循环里反复用同一个正则,每次都调用 re.match() 或 re.search() 其实挺浪费的。Python 每次都会重新编译正则表达式。这时候就该用 re.compile() 了。
import re
# 编译一次,多次使用
pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}")
log_lines = [
"2024-01-15 10:30:45 ERROR ...",
"2024-01-15 10:31:12 INFO ...",
"2024-01-15 10:32:01 WARN ...",
]
for line in log_lines:
match = pattern.search(line)
if match:
print(f"时间戳: {match.group()}")
# 输出:
# 时间戳: 2024-01-15 10:30:45
# 时间戳: 2024-01-15 10:31:12
# 时间戳: 2024-01-15 10:32:01
编译后的正则对象有和 re 模块一样的方法:match()、search()、findall()、sub()。用法完全一样,只是不用再传 pattern 参数了。
性能对比:在循环 10 万次的情况下,编译后的正则比每次都调用 re.match() 快 3-5 倍。日志分析动辄几十万行,这个优化值得做。
4.7 四个核心函数对比
为了方便你快速查阅,我把这四个函数的区别整理成了表格:
| 函数 | 匹配位置 | 返回结果 | 适用场景 |
|---|---|---|---|
re.match() |
只从开头 | 匹配对象或 None | 检查字符串是否以某模式开头 |
re.search() |
扫描整个字符串 | 第一个匹配对象或 None | 查找第一个匹配项 |
re.findall() |
扫描整个字符串 | 列表(所有匹配) | 提取所有匹配项 |
re.sub() |
扫描整个字符串 | 替换后的字符串 | 文本替换/脱敏 |
4.8 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以把它当成一张「正则模块速查地图」:
4.9 避坑指南
最后,分享几个我踩过的坑:
- match 不是 search:我曾经在写日志解析器时,用
match去匹配日志中间的错误码,结果返回永远是 None。排查了半天才发现问题。记住:match只看开头。 - findall 的分组陷阱:正则里一旦有分组,
findall返回的就是元组列表。如果你只想要完整匹配,用(?:...)非捕获组。 - sub 的 count 参数:默认替换所有匹配。如果你只想替换第一个,记得设
count=1。我在脱敏日志时吃过这个亏。 - 编译正则的 flag:
re.compile()可以传 flag,比如re.IGNORECASE。编译时设好 flag,后面调用就不用再传了。
我的建议:写日志分析脚本时,先把正则编译好,再用编译对象的方法去匹配。代码更清晰,性能也更好。养成这个习惯,你会感谢自己的。