4、Python re模块:re.match、re.search、re.findall、re.sub、re.compile实战
日志分析说白了就是跟字符串打交道。你想想看,几万行日志里,我们要找的往往就是那几个特定的模式——IP地址、时间戳、错误码。这时候,正则表达式就是最趁手的工具。
Python 的 re 模块提供了几个核心方法,我几乎每天都在用。今天咱们就把 re.match、re.search、re.findall、re.sub 和 re.compile 这几个家伙彻底搞明白。
核心观点:正则不是背出来的,是用出来的。掌握这五个方法,你能处理 90% 的日志提取场景。
4.1 re.match —— 从开头开始匹配
re.match 只从字符串的起始位置进行匹配。如果开头不匹配,直接返回 None。这一点很多人踩过坑。
我记得有一次排查线上问题,同事用 re.match 去匹配日志里的时间戳,结果日志行前面有空格,死活匹配不上。折腾了半小时才发现问题。
import re
log_line = "2024-01-15 10:30:45 ERROR 连接超时"
# 从开头匹配日期
result = re.match(r'\d{4}-\d{2}-\d{2}', log_line)
if result:
print(f"匹配到日期: {result.group()}") # 输出: 2024-01-15
# 匹配 ERROR —— 注意,这行会失败
result2 = re.match(r'ERROR', log_line)
print(result2) # 输出: None
避坑指南:我曾经在写日志解析脚本时,用 re.match 去匹配行中间的 IP 地址,结果全返回 None。后来才意识到,re.match 只看开头。如果你要匹配字符串中间的内容,请用 re.search。
4.2 re.search —— 搜索整个字符串
re.search 会扫描整个字符串,找到第一个匹配的位置就返回。这才是我们日常用得最多的方法。
log_line = "2024-01-15 10:30:45 [ERROR] 连接超时,IP: 192.168.1.100"
# 搜索 IP 地址
result = re.search(r'\d+\.\d+\.\d+\.\d+', log_line)
if result:
print(f"找到 IP: {result.group()}") # 输出: 192.168.1.100
# 搜索错误级别
result2 = re.search(r'\[(ERROR|WARN|INFO)\]', log_line)
if result2:
print(f"日志级别: {result2.group(1)}") # 输出: ERROR
我的习惯:在不确定日志格式是否严格对齐时,我优先用 re.search 而不是 re.match。这样即使前面有空格或前缀,也能找到目标内容。
4.3 re.findall —— 找到所有匹配
当你要从日志里提取所有符合条件的内容时,re.findall 就是你的首选。它返回一个列表,里面是所有匹配的字符串。
log_text = """
2024-01-15 10:30:45 [ERROR] 连接超时
2024-01-15 10:31:02 [WARN] 重试第1次
2024-01-15 10:31:15 [ERROR] 重试失败
2024-01-15 10:32:00 [INFO] 服务恢复
"""
# 提取所有 ERROR 日志
errors = re.findall(r'\[ERROR\].*', log_text)
print(errors)
# 输出: ['[ERROR] 连接超时', '[ERROR] 重试失败']
# 提取所有时间戳
timestamps = re.findall(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}', log_text)
print(timestamps)
# 输出: ['2024-01-15 10:30:45', '2024-01-15 10:31:02', ...]
如果正则里有分组,re.findall 会返回元组列表。这个特性在提取结构化数据时特别好用。
# 带分组的提取
pattern = r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) \[(\w+)\] (.*)'
results = re.findall(pattern, log_text)
for date, time, level, msg in results:
print(f"{date} {time} [{level}] {msg}")
4.4 re.sub —— 替换匹配内容
日志清洗和脱敏是运维的日常。比如把日志里的 IP 地址替换掉,或者把敏感信息抹掉。re.sub 就是干这个的。
log_line = "用户 192.168.1.100 在 10:30:45 登录系统"
# 脱敏 IP 地址
cleaned = re.sub(r'\d+\.\d+\.\d+\.\d+', '***.***.***.***', log_line)
print(cleaned)
# 输出: 用户 ***.***.***.*** 在 10:30:45 登录系统
# 替换时间格式
formatted = re.sub(r'(\d{2}):(\d{2}):(\d{2})', r'\1时\2分\3秒', log_line)
print(formatted)
# 输出: 用户 192.168.1.100 在 10时30分45秒 登录系统
实战经验:我在做日志脱敏时,经常用 re.sub 配合回调函数。比如只替换 IP 的后两段,保留前两段用于排查。这样既保护了隐私,又不影响问题定位。
4.5 re.compile —— 预编译正则
如果你要在循环里反复使用同一个正则,每次调用都重新编译一次,性能会很差。re.compile 可以把正则表达式预编译成一个对象,反复使用。
import re
# 预编译正则
ip_pattern = re.compile(r'\d+\.\d+\.\d+\.\d+')
error_pattern = re.compile(r'\[ERROR\].*')
# 在循环中反复使用
log_lines = [
"2024-01-15 10:30:45 [ERROR] 连接超时 IP: 192.168.1.100",
"2024-01-15 10:31:02 [WARN] 重试第1次",
"2024-01-15 10:31:15 [ERROR] 重试失败 IP: 192.168.1.101",
]
for line in log_lines:
ip_match = ip_pattern.search(line)
if ip_match:
print(f"发现 IP: {ip_match.group()}")
if error_pattern.match(line):
print(f"错误日志: {line}")
| 方法 | 作用 | 返回值 | 典型场景 |
|---|---|---|---|
re.match |
从开头匹配 | match 对象 / None | 校验日志行首格式 |
re.search |
搜索第一个匹配 | match 对象 / None | 提取关键字段 |
re.findall |
返回所有匹配 | 列表 | 批量提取数据 |
re.sub |
替换匹配内容 | 新字符串 | 日志脱敏、清洗 |
re.compile |
预编译正则 | Pattern 对象 | 高频重复匹配 |
性能建议:如果你要处理几十万行日志,一定要用 re.compile。我做过测试,预编译后速度能提升 3-5 倍。说白了,就是一次编译,多次使用,省去了重复解析正则表达式的开销。
4.6 综合实战:日志解析器
咱们把今天学的串起来,写一个简单的日志解析器。这个脚本能提取日志里的时间、级别、消息和 IP 地址。
import re
class LogParser:
def __init__(self):
# 预编译所有正则
self.patterns = {
'timestamp': re.compile(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'),
'level': re.compile(r'\[(ERROR|WARN|INFO|DEBUG)\]'),
'ip': re.compile(r'\d+\.\d+\.\d+\.\d+'),
'message': re.compile(r'\] (.*?)(?: IP:|$)')
}
def parse_line(self, line):
result = {}
# 提取时间戳
ts = self.patterns['timestamp'].search(line)
if ts:
result['time'] = ts.group()
# 提取日志级别
level = self.patterns['level'].search(line)
if level:
result['level'] = level.group(1)
# 提取 IP
ip = self.patterns['ip'].search(line)
if ip:
result['ip'] = ip.group()
# 提取消息内容
msg = self.patterns['message'].search(line)
if msg:
result['message'] = msg.group(1).strip()
return result
# 使用示例
parser = LogParser()
log_line = "2024-01-15 10:30:45 [ERROR] 连接超时 IP: 192.168.1.100"
parsed = parser.parse_line(log_line)
print(parsed)
# 输出: {'time': '2024-01-15 10:30:45', 'level': 'ERROR', 'ip': '192.168.1.100', 'message': '连接超时'}
总结一下:今天这五个方法,说白了就是正则的五个基本操作——匹配开头、搜索、找全部、替换、预编译。你想想看,日志分析里无非就是这些操作。掌握了它们,你就能从日志里提取出任何你想要的信息。