3. Python re 模块:五个核心函数实战
正则表达式在日志解析里有多重要?我这么说吧——你处理的每一条日志,几乎都要过一遍正则。Python 的 re 模块提供了五个核心函数,搞懂它们,你就能搞定 90% 的日志解析场景。
这五个函数分别是:re.match、re.search、re.findall、re.sub、re.compile。它们各有各的脾气,用错了地方,轻则匹配不到数据,重则性能崩盘。我踩过的坑,今天一次性给你讲清楚。
核心要点:这五个函数不是互相替代的关系,而是各司其职。选对函数,比写对正则更重要。
3.1 re.match:从开头开始,别想偷懒
re.match 只从字符串的开头匹配。如果开头不匹配,直接返回 None。很多人刚学的时候以为它和 re.search 一样,结果踩了坑。
我举个例子。假设你有一条日志:
2024-01-15 10:30:45 ERROR [app.py:42] 数据库连接超时
你想提取日期。用 re.match 没问题,因为日期就在开头:
import re
log = "2024-01-15 10:30:45 ERROR [app.py:42] 数据库连接超时"
pattern = r"\d{4}-\d{2}-\d{2}"
match = re.match(pattern, log)
if match:
print(match.group()) # 输出:2024-01-15
但如果你要提取错误级别 ERROR,它不在开头,re.match 就匹配不到。这时候就得用 re.search。
我曾经犯过的错:有一次我写日志解析脚本,用 re.match 去匹配日志中间的 IP 地址,结果跑了半天一条都没匹配到。排查了半小时才发现是函数选错了。记住:re.match 只看开头,不看中间。
3.2 re.search:扫描全文,找到第一个就收工
re.search 会扫描整个字符串,返回第一个匹配结果。它不挑位置,只要字符串里有就行。
还是刚才那条日志,提取 ERROR:
pattern = r"ERROR|WARNING|INFO|DEBUG"
match = re.search(pattern, log)
if match:
print(match.group()) # 输出:ERROR
你想想看,如果日志里有多个 ERROR,re.search 也只返回第一个。要拿全部,得用 re.findall。
我的习惯:在日志解析里,re.search 是我用得最多的函数。因为日志结构复杂,字段位置不固定,re.search 最灵活。但要注意——它只返回第一个匹配,别指望它帮你批量提取。
3.3 re.findall:全都要,一个不落
re.findall 返回所有匹配结果,以列表形式返回。如果正则里有分组,返回的是元组列表。
假设你要从一堆日志里提取所有 IP 地址:
logs = """
192.168.1.1 - - [15/Jan/2024:10:30:45] "GET /api/users"
10.0.0.5 - - [15/Jan/2024:10:31:12] "POST /api/login"
172.16.0.10 - - [15/Jan/2024:10:32:01] "GET /api/data"
"""
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
ips = re.findall(pattern, logs)
print(ips)
# 输出:['192.168.1.1', '10.0.0.5', '172.16.0.10']
如果正则里有分组,比如你想同时提取 IP 和时间:
pattern = r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*?\[(.*?)\]"
results = re.findall(pattern, logs)
print(results)
# 输出:[('192.168.1.1', '15/Jan/2024:10:30:45'), ('10.0.0.5', '15/Jan/2024:10:31:12'), ('172.16.0.10', '15/Jan/2024:10:32:01')]
性能提醒:如果日志文件很大(比如几百 MB),re.findall 会一次性把所有结果加载到内存。我遇到过内存爆掉的案例。这时候建议逐行读取,或者用 re.finditer 返回迭代器。
3.4 re.sub:替换的艺术
re.sub 用于替换匹配到的内容。它在日志清洗和脱敏场景里特别有用。
比如你要把日志里的 IP 地址替换成 ***.***.***.***:
log_line = "用户 192.168.1.1 在 10:30:45 登录系统"
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
cleaned = re.sub(pattern, "***.***.***.***", log_line)
print(cleaned)
# 输出:用户 ***.***.***.*** 在 10:30:45 登录系统
re.sub 还支持用函数做动态替换。我做过一个需求:把日志里的时间戳统一格式:
def convert_time(match):
from datetime import datetime
old_time = match.group()
dt = datetime.strptime(old_time, "%Y-%m-%d %H:%M:%S")
return dt.strftime("%Y/%m/%d %H:%M:%S")
log = "2024-01-15 10:30:45 ERROR 连接超时"
pattern = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"
result = re.sub(pattern, convert_time, log)
print(result)
# 输出:2024/01/15 10:30:45 ERROR 连接超时
避坑指南:re.sub 的替换字符串里,\1、\2 可以引用分组。但如果你用的是 Python 3.6+,建议用 \g<1>、\g<name> 这种写法,可读性更好,也不容易出错。
3.5 re.compile:预编译,性能翻倍
re.compile 把正则表达式编译成一个 Pattern 对象。之后你可以反复用它来匹配,不用每次都重新解析正则。
我为什么强调这个?因为正则编译是有开销的。如果你在循环里反复用同一个正则,不编译的话,性能差距可能达到 10 倍以上。
看个对比:
import re
import time
logs = ["2024-01-15 10:30:45 ERROR msg1"] * 100000
# 不编译
start = time.time()
for log in logs:
re.search(r"\d{4}-\d{2}-\d{2}", log)
print("不编译耗时:", time.time() - start)
# 编译
start = time.time()
pattern = re.compile(r"\d{4}-\d{2}-\d{2}")
for log in logs:
pattern.search(log)
print("编译后耗时:", time.time() - start)
在我机器上,不编译跑了 0.8 秒,编译后只用了 0.1 秒。差距 8 倍。你想想看,如果每天处理几千万条日志,这个优化能省多少时间。
我的最佳实践:在日志解析系统里,我会在模块加载时就把所有正则编译好,存成一个字典。运行时直接取用,既快又清晰。
# 日志解析规则预编译
PATTERNS = {
"timestamp": re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"),
"ip": re.compile(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"),
"error_level": re.compile(r"ERROR|WARNING|INFO|DEBUG"),
"request_url": re.compile(r'"(GET|POST|PUT|DELETE) (\S+)'),
}
def parse_log_line(line):
timestamp = PATTERNS["timestamp"].search(line)
ip = PATTERNS["ip"].search(line)
level = PATTERNS["error_level"].search(line)
url = PATTERNS["request_url"].search(line)
return {
"timestamp": timestamp.group() if timestamp else None,
"ip": ip.group() if ip else None,
"level": level.group() if level else None,
"url": url.group(2) if url else None,
}
3.6 五个函数对比速查表
| 函数 | 匹配位置 | 返回类型 | 典型场景 | 性能注意 |
|---|---|---|---|---|
re.match |
仅开头 | Match 或 None | 校验日志行首格式 | 快,但限制大 |
re.search |
全文第一个 | Match 或 None | 提取单个字段 | 适中 |
re.findall |
全文所有 | 列表 / 元组列表 | 批量提取 IP、时间 | 大文件注意内存 |
re.sub |
全文所有 | 字符串 | 脱敏、格式转换 | 函数替换略慢 |
re.compile |
— | Pattern 对象 | 高频匹配优化 | 预编译提升明显 |
我曾经踩过的坑:有一次线上日志解析服务突然变慢,排查发现是有人在循环里写了 re.search(r"...", line),没有预编译。同一个正则每行日志都编译一次,百万行日志就是百万次编译。改成 re.compile 后,CPU 使用率直接降了 70%。