正则表达式基础:元字符、量词、分组与捕获、贪婪与非贪婪模式
说实话,正则表达式这东西,刚接触时觉得像天书。但做日志分析绕不开它。我当年第一次写日志解析脚本,就是被一堆乱七八糟的时间戳格式逼着去啃正则的。啃完之后发现,嗯,真香。
这一章咱们就把正则的底子打牢。你不需要背下所有语法,但核心的几个概念必须吃透:元字符、量词、分组与捕获、还有那个坑过无数人的贪婪与非贪婪。
2.1 元字符:正则世界的字母表
元字符就是有特殊含义的字符。说白了,它们不是匹配自己,而是匹配某一类东西。
| 元字符 | 含义 | 举个栗子 |
|---|---|---|
. |
匹配任意单个字符(换行符除外) | a.c 能匹配 abc、a1c,但不匹配 ac |
\d |
匹配一个数字,等价于 [0-9] |
\d\d\d 匹配三位数字 |
\w |
匹配一个字母、数字或下划线 | \w+ 匹配一个单词 |
\s |
匹配一个空白字符(空格、制表符、换行) | name\s+age 匹配中间有空白 |
^ |
匹配字符串开头 | ^ERROR 只匹配开头的 ERROR |
$ |
匹配字符串结尾 | done$ 只匹配结尾的 done |
[] |
字符集,匹配括号内任意一个字符 | [aeiou] 匹配任意元音字母 |
[^] |
否定字符集,匹配不在括号内的字符 | [^0-9] 匹配非数字 |
我的小习惯:写正则时,我总爱先用 . 占位,调试通了再换成精确的元字符。这样能快速验证整体结构对不对。
2.2 量词:控制重复次数
量词解决的是「这个字符出现几次」的问题。没有量词,你写 \d\d\d\d\d 匹配五位数,那匹配二十位数得写到手抽筋。
| 量词 | 含义 | 示例 |
|---|---|---|
* |
0 次或多次 | ab*c 匹配 ac、abc、abbc |
+ |
1 次或多次 | ab+c 匹配 abc、abbc,不匹配 ac |
? |
0 次或 1 次 | colou?r 匹配 color 和 colour |
{n} |
恰好 n 次 | \d{4} 匹配四位数字 |
{n,} |
至少 n 次 | \d{2,} 匹配至少两位数字 |
{n,m} |
n 到 m 次 | \d{2,4} 匹配 2 到 4 位数字 |
避坑指南:我曾经在解析 Nginx 日志时,用 .* 去匹配请求路径,结果把整行都吞掉了。后来才发现,* 和 + 默认是贪婪的——它们会尽可能多匹配。这个问题我们马上讲。
2.3 分组与捕获:把匹配结果「拎」出来
分组用圆括号 () 表示。它有两个作用:一是把多个字符当成一个整体来用量词修饰,二是把匹配到的内容「捕获」下来供后续使用。
举个例子,你要从日志里提取 IP 和端口:
192.168.1.1:8080
正则可以写成:
(\d+\.\d+\.\d+\.\d+):(\d+)
这里两个括号就是两个捕获组。第一个组拿到 IP,第二个组拿到端口。在 Python 里用 re.match() 或 re.search() 之后,通过 .group(1) 和 .group(2) 就能取出来。
我个人的经验:写日志解析规则时,我习惯给每个要提取的字段都加上命名分组,比如 (?P<ip>\d+\.\d+\.\d+\.\d+)。这样代码可读性高很多,三个月后回来看还能秒懂。
命名分组的语法是 (?P<name>...)。Python 里用 .group('name') 获取,比数序号靠谱多了。
2.4 贪婪与非贪婪:一个字符引发的血案
这是正则里最容易踩的坑,没有之一。
贪婪模式是默认行为:量词会尽可能多地匹配字符。非贪婪模式则在量词后面加个 ?,让它尽可能少地匹配。
看个例子。假设有字符串:
<div>内容1</div><div>内容2</div>
用贪婪模式 <div>.*</div> 匹配,结果是什么?
它会匹配整个字符串,从第一个 <div> 一直吃到最后一个 </div>。因为 .* 太贪心了,它把中间的 </div><div> 也当成了自己的「菜」。
用非贪婪模式 <div>.*?</div> 呢?
它会匹配到 <div>内容1</div> 就停下来。因为 .*? 说「够吃就行」,碰到第一个 </div> 就收手了。
我曾经踩过的坑:有一次写日志清洗脚本,要提取中括号里的内容。日志长这样:[INFO] [2024-01-01] [模块A] 启动成功。我用 \[(.*)\] 去匹配,结果只拿到一个结果——从第一个 [ 到最后一个 ] 的全部内容。改成 \[(.*?)\] 才正确提取出三个独立字段。那一次排查花了我半小时,从此我对非贪婪模式刻骨铭心。
2.5 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以把它当成正则基础的地图,以后写正则时对照着看。
2.6 实战:用正则提取日志中的关键字段
光说不练假把式。咱们拿一条典型的应用日志来练手:
2024-06-15 14:30:22,123 [http-nio-8080-exec-3] ERROR com.example.service.OrderService - 订单处理失败: orderId=ORD20240615001, userId=U10086, 原因: 库存不足
我们要提取:时间、日志级别、线程名、类名、订单ID、用户ID、错误原因。
正则可以这样写:
(?P<timestamp>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d{3})\s+
\[(?P<thread>[^\]]+)\]\s+
(?P<level>ERROR|INFO|WARN|DEBUG)\s+
(?P<class>\S+)\s*-\s*
订单处理失败:\s*
orderId=(?P<order_id>\w+),\s*
userId=(?P<user_id>\w+),\s*
原因:\s*(?P<reason>.+)$
拆解一下:
\d{4}-\d{2}-\d{2}匹配日期,\d{2}:\d{2}:\d{2},\d{3}匹配时间+毫秒[^\]]+匹配中括号内的线程名,用否定字符集避免跨出括号ERROR|INFO|WARN|DEBUG用|做多选匹配\S+匹配非空白字符的类名- 最后的
.+$匹配到行尾,把错误原因全部抓下来
在 Python 里用起来就是:
import re
log_line = "2024-06-15 14:30:22,123 [http-nio-8080-exec-3] ERROR com.example.service.OrderService - 订单处理失败: orderId=ORD20240615001, userId=U10086, 原因: 库存不足"
pattern = r'(?P<timestamp>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d{3})\s+\[(?P<thread>[^\]]+)\]\s+(?P<level>ERROR|INFO|WARN|DEBUG)\s+(?P<class>\S+)\s*-\s*订单处理失败:\s*orderId=(?P<order_id>\w+),\s*userId=(?P<user_id>\w+),\s*原因:\s*(?P<reason>.+)$'
match = re.match(pattern, log_line)
if match:
print(f"时间: {match.group('timestamp')}")
print(f"线程: {match.group('thread')}")
print(f"级别: {match.group('level')}")
print(f"类名: {match.group('class')}")
print(f"订单ID: {match.group('order_id')}")
print(f"用户ID: {match.group('user_id')}")
print(f"原因: {match.group('reason')}")
输出结果:
时间: 2024-06-15 14:30:22,123
线程: http-nio-8080-exec-3
级别: ERROR
类名: com.example.service.OrderService
订单ID: ORD20240615001
用户ID: U10086
原因: 库存不足
注意:实际生产环境中的日志格式可能更复杂。比如时间戳可能有时区,类名可能包含内部类(带 $ 符号),错误原因可能跨行。我建议你在写正则之前,先拿 100 条真实日志样本跑一遍,看看有没有边界情况。我曾经就因为没考虑到错误信息里包含换行符,导致线上少采集了 30% 的异常日志——那场面,相当尴尬。
好了,正则基础就讲到这里。元字符让你知道「匹配什么」,量词告诉你「匹配多少次」,分组帮你「提取出来」,非贪婪模式帮你「别吃太多」。这四个概念吃透了,日志解析的正则部分你就掌握了 80%。剩下的就是多练、多踩坑、多总结。