第3章 日志格式解析:正则表达式入门归纳总结
搞自动化审计日志巡检,逃不开一个基本功——正则表达式。
说白了,就是教Python怎么从一堆文本里“揪”出你想要的信息。我刚开始做日志解析的时候,手写字符串切片,又慢又容易崩。后来老老实实用正则,效率直接翻倍。
3.1 为什么非用正则不可?
审计日志的内容通常长这样:
2025-04-01 14:23:45,123 [INFO] user=admin action=LOGIN result=SUCCESS ip=192.168.1.10
你想提取时间、用户、动作、结果、IP。用split()?日志格式一变就全废。用正则?写一次,管它空格多一个少一个,照抓不误。
我在项目里遇到过好几次:客户说“日志格式跟文档一样”,结果实际日志里多了个制表符……正则的\s+一次搞定。
小提示:正则不是万能的。超长日志、嵌套结构,还是得走解析器。但审计日志多数是结构化文本,正则已经够用。
3.2 正则表达式的核心“零件”
记住这张表,后面直接套用。
| 元字符 | 含义 | 示例 |
|---|---|---|
. | 匹配任意单个字符(换行除外) | a.b → 匹配 a1b、aXb |
\d | 数字 [0-9] | \d{4} → 四位数字 |
\w | 字母、数字、下划线 | \w+ → 一个单词 |
\s | 空白字符(空格、制表、换行) | \s+ → 连续空白 |
^ | 行开头 | ^ERROR → 行首的ERROR |
$ | 行结尾 | SUCCESS$ → 行尾的SUCCESS |
* | 前一个字符出现 0 次或多次 | ab*c → ac, abc, abbc |
+ | 前一个字符出现 1 次或多次 | ab+c → abc, abbc |
? | 前一个字符出现 0 次或 1 次 | https? → http 或 https |
{n,m} | 重复 n 到 m 次 | \d{2,4} → 2~4位数字 |
[...] | 字符集,匹配其中任意一个 | [aeiou] → 任何一个元音 |
(...) | 分组,用于提取或引用 | (\d{4})-(\d{2}) → 年-月 |
| | 或 | error|warning → error 或 warning |
核心口诀:圆括号提取,方括号选一个,反斜杠转义或特殊含义。记牢这三句,实战够用。
3.3 Python 正则三步走
Python 的 re 模块,我日常只用三个函数:
re.search()—— 在整段文本里找第一处匹配。适合判断“有没有”。re.findall()—— 找出所有匹配,返回列表。适合批量提取。re.sub()—— 替换。我用来做日志脱敏,比如把IP换成***。
看个例子:提取上面那条日志的时间、用户和IP。
import re
log = "2025-04-01 14:23:45,123 [INFO] user=admin action=LOGIN result=SUCCESS ip=192.168.1.10"
# 写法一:一个个抓
time_match = re.search(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})', log)
user_match = re.search(r'user=(\w+)', log)
ip_match = re.search(r'ip=([\d.]+)', log)
print(f"时间: {time_match.group(1)}")
print(f"用户: {user_match.group(1)}")
print(f"IP: {ip_match.group(1)}")
运行结果:
时间: 2025-04-01 14:23:45,123
用户: admin
IP: 192.168.1.10
我个人的编写习惯:正则前面加
r(原始字符串),省得转义搞晕自己。比如 r"\d+" 而不是 "\\d+"。
如果想一行代码全提取,用 findall + 分组:
pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) .*? user=(\w+) .*? ip=([\d.]+)'
result = re.findall(pattern, log)
print(result) # [('2025-04-01 14:23:45,123', 'admin', '192.168.1.10')]
3.4 常见的坑,我替你踩过了
- 忘加
r前缀:"\n"会被当成换行。我一晚上没查出 bug,后来发现是 Python 把\n解析成了换行符。 - 点号不匹配换行:默认
.不包括\n。跨行日志?用re.DOTALL模式。 - 贪婪 vs 非贪婪:
.*会一直匹配到最后。想停在一个地方?用.*?。我曾经拿它匹配日志里的[INFO] ... [END],不加问号直接把整个日志吞了。
注意:正则如果写得太复杂,性能会爆炸。审计日志一天几十万条,re.findall 里嵌套回溯,我见过跑 10 分钟没结果的案例。简洁优先,不行就用 re.compile 预编译。
3.5 本章知识体系
下面这张图,帮你把正则的核心逻辑串起来。
3.6 实战:解析真实审计日志
来一个完整的例子。假设我们有这么几条审计日志:
2025-04-01 09:12:33,456 [WARN] 用户=张三 动作=文件删除 文件=/tmp/test.log
2025-04-01 09:13:01,789 [ERROR] 用户=李四 动作=登录失败 来源IP=10.0.0.5
2025-04-01 09:15:22,012 [INFO] 用户=王五 动作=创建用户 目标=admin
我要提取时间、级别、用户、动作,并按级别统计条数。
import re
logs = [
"2025-04-01 09:12:33,456 [WARN] 用户=张三 动作=文件删除 文件=/tmp/test.log",
"2025-04-01 09:13:01,789 [ERROR] 用户=李四 动作=登录失败 来源IP=10.0.0.5",
"2025-04-01 09:15:22,012 [INFO] 用户=王五 动作=创建用户 目标=admin",
]
pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) \[(\w+)\]\s+用户=(\S+)\s+动作=(\S+)'
count = {"ERROR": 0, "WARN": 0, "INFO": 0}
for log in logs:
m = re.search(pattern, log)
if m:
time, level, user, action = m.groups()
print(f"{time} | {level:5s} | {user:6s} | {action}")
if level in count:
count[level] += 1
print("\n统计:", count)
输出:
2025-04-01 09:12:33,456 | WARN | 张三 | 文件删除
2025-04-01 09:13:01,789 | ERROR | 李四 | 登录失败
2025-04-01 09:15:22,012 | INFO | 王五 | 创建用户
统计: {'ERROR': 1, 'WARN': 1, 'INFO': 1}
注意:正则里的\S+匹配非空白字符,比\w+更通用,因为用户名里可能有中文(虽然\w在Python 3里也支持Unicode,但\S更安全)。