4、正则表达式入门:re模块基础、常用模式匹配、分组与捕获、性能优化技巧
正则表达式,说白了就是一门「字符串匹配的艺术」。我刚开始接触它的时候,觉得这玩意儿跟天书似的,满屏的符号看得人眼花。但后来在日志监控里用得多了,才发现它真的是运维工程师的瑞士军刀——没有它,你根本没法从海量日志里快速捞出想要的信息。
今天咱们就把它掰开揉碎了讲清楚。你不需要背下所有语法,掌握核心套路就够了。
4.1 re模块基础:先跑起来再说
Python 的 re 模块是内置的,直接 import 就能用。我个人习惯先写一个最简单的匹配试试手:
import re
log_line = "2025-04-01 10:23:45 ERROR [main] Connection timeout"
pattern = r"ERROR"
result = re.search(pattern, log_line)
if result:
print("匹配到了!")
这里有个小细节:字符串前面的 r 表示原始字符串,意思是告诉 Python 「别转义里面的反斜杠」。我在项目中见过不少新人因为忘了加 r,结果 \d 被解释成了普通字符 d,排查了半天。
常用的三个函数你得记住:
re.search():扫描整个字符串,找到第一个匹配就返回re.match():从字符串开头开始匹配,开头对不上就返回 Nonere.findall():找到所有匹配,返回列表
举个例子,你想想看:
text = "2025-04-01 10:23:45 ERROR [main] Connection timeout"
print(re.match(r"ERROR", text)) # None,因为开头是日期
print(re.search(r"ERROR", text)) # 匹配成功
print(re.findall(r"\d+", text)) # ['2025', '04', '01', '10', '23', '45']
findall 在日志分析里特别常用,比如你想提取所有 IP 地址,一行代码就搞定。
4.2 常用模式匹配:这些符号你得认识
正则表达式的核心就是「元字符」。我把它分成三类来讲,这样好记:
4.2.1 字符类
| 模式 | 含义 | 示例 |
|---|---|---|
. | 匹配任意单个字符(除了换行) | a.b 匹配 aab、a1b |
\d | 匹配数字,等价于 [0-9] | \d{3} 匹配三位数字 |
\w | 匹配字母、数字、下划线 | \w+ 匹配一个单词 |
\s | 匹配空白字符(空格、制表符等) | \s+ 匹配连续空白 |
[abc] | 匹配 a、b、c 中的任意一个 | [0-9a-f] 匹配十六进制字符 |
嗯,这里要注意:. 默认不匹配换行,如果你要匹配包括换行在内的所有字符,得用 re.DOTALL 标志。我曾经在处理多行日志时踩过这个坑,死活匹配不上,后来才发现是换行符在作怪。
4.2.2 量词
| 模式 | 含义 | 示例 |
|---|---|---|
* | 匹配前一个字符 0 次或多次 | ab* 匹配 a、ab、abb |
+ | 匹配前一个字符 1 次或多次 | ab+ 匹配 ab、abb,不匹配 a |
? | 匹配前一个字符 0 次或 1 次 | ab? 匹配 a、ab |
{n,m} | 匹配 n 到 m 次 | \d{2,4} 匹配 2~4 位数字 |
我建议你记住一个口诀:「* 贪婪,+ 贪婪,? 最小匹配」。默认情况下,* 和 + 会尽可能多地匹配字符,这就是「贪婪模式」。如果你想要最小匹配,就在后面加个 ?。
举个例子:
text = "<div>内容1</div><div>内容2</div>"
print(re.findall(r"<div>.*</div>", text)) # 贪婪:匹配整个字符串
print(re.findall(r"<div>.*?</div>", text)) # 非贪婪:匹配两个 div
4.2.3 边界与分组
| 模式 | 含义 |
|---|---|
^ | 匹配字符串开头 |
$ | 匹配字符串结尾 |
\b | 匹配单词边界 |
| | 或操作,匹配左边或右边 |
比如你想匹配以 ERROR 开头的行:^ERROR。或者匹配以 timeout 结尾的行:timeout$。这些在日志过滤里非常实用。
4.3 分组与捕获:提取你真正想要的东西
分组是正则表达式里最强大的功能之一。说白了,就是用括号 () 把一部分模式包起来,然后单独提取出来。
我举个例子,假设你要从日志里提取时间戳和日志级别:
log = "2025-04-01 10:23:45 ERROR [main] Connection timeout"
pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (\w+)"
match = re.search(pattern, log)
if match:
print("时间:", match.group(1)) # 2025-04-01 10:23:45
print("级别:", match.group(2)) # ERROR
这里 group(0) 是整个匹配结果,group(1) 是第一个括号里的内容,以此类推。我在做日志解析时,经常用这种方式把一行日志拆成多个字段,然后塞进数据库里做分析。
小技巧:如果你只想分组,不想捕获(也就是不占用内存),可以用 (?:...)。比如 (?:\d{4})-\d{2},第一个分组只用于匹配,不保存结果。这在性能敏感的场景下很有用。
还有一个很实用的东西叫「命名分组」,语法是 (?P<name>...):
pattern = r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2})"
match = re.search(pattern, log)
print(match.group("date")) # 2025-04-01
print(match.group("time")) # 10:23:45
命名分组的好处是,你不用数括号的顺序了,直接按名字取。代码可读性一下子提升不少。
4.4 性能优化技巧:别让正则拖慢你的系统
正则表达式虽然好用,但写不好会非常慢。我在监控系统里见过一个案例,一个正则把 CPU 跑到了 100%,原因是「灾难性回溯」。说白了就是模式写得太复杂,导致引擎在尝试所有可能的匹配路径时爆炸了。
下面这几个优化技巧,是我这些年总结出来的:
4.4.1 预编译正则
如果你要多次使用同一个正则,一定要预编译:
# 不推荐:每次调用都编译一次
for line in log_lines:
if re.search(r"\d{4}-\d{2}-\d{2}", line):
pass
# 推荐:只编译一次
pattern = re.compile(r"\d{4}-\d{2}-\d{2}")
for line in log_lines:
if pattern.search(line):
pass
预编译能提升 10%~30% 的性能,尤其是在处理几十万行日志时,差距非常明显。
4.4.2 避免贪婪匹配
前面说过,.* 是贪婪的,它会一直匹配到字符串末尾,然后回溯。如果你知道要匹配的内容不长,用 .*? 或者更精确的字符类:
# 慢:贪婪匹配 + 回溯
pattern = r"<div>(.*)</div>"
# 快:非贪婪
pattern = r"<div>(.*?)</div>"
# 更快:精确匹配
pattern = r"<div>([^<]*)</div>"
第三种写法用了 [^<]*,意思是「匹配任意不是 < 的字符」。这样引擎就不用回溯了,性能最好。
4.4.3 使用原子组和占有量词
Python 的 re 模块不支持原子组((?>...))和占有量词(*+、++),但你可以用 re 的替代库 regex 来获得这些功能。如果你必须用标准库,那就尽量用 [^...] 来替代 .*。
避坑指南:我曾经在处理一个复杂的日志解析任务时,写了一个嵌套很深的正则,结果一跑就卡死。后来用 re.DEBUG 标志打印了编译过程,才发现回溯次数达到了几百万次。解决方案很简单:把一个大正则拆成多个小正则,分步匹配。
4.4.4 善用 flags 参数
几个常用的 flags:
re.IGNORECASE:忽略大小写,省得你写[Ee][Rr][Rr][Oo][Rr]re.MULTILINE:让^和$匹配每一行的开头和结尾re.DOTALL:让.匹配换行符
比如你想匹配多行日志中的某个模式:
pattern = re.compile(r"^ERROR.*", re.MULTILINE)
这样就能匹配每一行以 ERROR 开头的日志了。
4.5 实战:一个日志解析的例子
最后,咱们来个综合的例子。假设你有这样的日志:
2025-04-01 10:23:45 ERROR [main] Connection timeout to 192.168.1.1:8080
2025-04-01 10:23:46 WARN [worker] Retry attempt 1/3
2025-04-01 10:23:47 INFO [main] Reconnected successfully
你想提取每一行的日期、时间、级别、线程名、消息和 IP 地址。可以这样写:
import re
pattern = re.compile(
r"(?P<date>\d{4}-\d{2}-\d{2}) "
r"(?P<time>\d{2}:\d{2}:\d{2}) "
r"(?P<level>\w+) "
r"\[(?P<thread>\w+)\] "
r"(?P<message>.*?)(?P<ip>\d+\.\d+\.\d+\.\d+)?"
)
for line in log_lines:
match = pattern.search(line)
if match:
print(match.groupdict())
输出结果:
{'date': '2025-04-01', 'time': '10:23:45', 'level': 'ERROR',
'thread': 'main', 'message': 'Connection timeout to ', 'ip': '192.168.1.1'}
你看,一行正则就把日志拆得清清楚楚。这就是正则表达式的魅力——用最小的代码量,解决最复杂的问题。
好了,正则入门就讲到这里。下一节咱们会把这些技巧用在真正的日志监控系统里,到时候你就知道这些知识有多实用了。