3、正则表达式基础:元字符、量词、分组与捕获、贪婪与非贪婪模式
正则表达式这东西,说白了就是一门「模式匹配」的语言。我刚开始接触日志分析时,总觉得它晦涩难懂,一堆符号挤在一起像天书。但后来我发现,只要你把核心的几个概念吃透,正则就是一把瑞士军刀——能帮你从海量日志里精准捞出想要的信息。
这一节,我们就来拆解正则的四大核心模块:元字符、量词、分组与捕获、贪婪与非贪婪。嗯,别被这些术语吓到,我们一个一个过。
3.1 元字符:正则的「字母表」
元字符是正则表达式最基本的构建块。它们不匹配字面字符,而是匹配某一类字符或位置。我个人习惯把元字符分成三组来记,这样不容易乱。
3.1.1 字符类元字符
| 元字符 | 含义 | 示例 | 匹配结果 |
|---|---|---|---|
. |
匹配除换行符外的任意单个字符 | a.b |
匹配 acb、a1b,不匹配 ab |
\d |
匹配任意数字(0-9) | \d\d |
匹配 42、07 |
\w |
匹配字母、数字、下划线 | \w+ |
匹配 error_1 |
\s |
匹配空白字符(空格、制表符、换行) | error\s\d |
匹配 error 5 |
[abc] |
匹配方括号中的任意一个字符 | [aeiou] |
匹配任意元音字母 |
[^abc] |
匹配不在方括号中的任意字符 | [^0-9] |
匹配非数字字符 |
\d+ 和 \w+ 是我用得最多的两个元字符组合。比如提取IP地址中的数字段,或者匹配错误码中的字母数字串,它们几乎能覆盖80%的场景。
3.1.2 位置锚定元字符
这类元字符不匹配字符,而是匹配位置。你想想看,有时候你需要的不是「什么内容」,而是「内容在哪儿」。
^—— 匹配字符串的开头。比如^ERROR只匹配以 ERROR 开头的行。$—— 匹配字符串的结尾。比如timeout$只匹配以 timeout 结尾的行。\b—— 匹配单词边界。比如\bcat\b匹配独立的单词 cat,不会匹配到 catalog 里的 cat。
^\d{2}:\d{2},结果发现匹配到了行中间的时间。后来才意识到,我忘了加 ^ 锚定行首。记住:不加锚定,正则会在字符串任意位置尝试匹配。
3.2 量词:控制重复次数
量词解决的是「匹配多少次」的问题。没有量词,你只能写 \d\d\d\d 来匹配四位数字,有了量词,一行 \d{4} 就搞定了。
| 量词 | 含义 | 示例 | 匹配结果 |
|---|---|---|---|
* |
匹配前一个元素 0 次或多次 | ab*c |
匹配 ac、abc、abbc |
+ |
匹配前一个元素 1 次或多次 | ab+c |
匹配 abc、abbc,不匹配 ac |
? |
匹配前一个元素 0 次或 1 次 | colou?r |
匹配 color 和 colour |
{n} |
匹配恰好 n 次 | \d{4} |
匹配 2024,不匹配 123 |
{n,} |
匹配至少 n 次 | \d{2,} |
匹配 12、12345 |
{n,m} |
匹配 n 到 m 次 | \d{2,4} |
匹配 12、123、1234 |
* 星号——零到多+ 加号——一到多? 问号——零或一{} 花括号——精确控次数
3.3 分组与捕获:提取关键信息
分组是正则里最实用的功能之一。它用圆括号 () 把一部分模式包起来,既能改变优先级,又能把匹配到的内容「捕获」下来单独使用。
3.3.1 捕获分组
假设你有一条日志:2024-03-15 14:30:22 ERROR [DB-001] Connection timeout。你想分别提取日期、时间、错误码和消息。用捕获分组可以这样写:
(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2}:\d{2})\s(\w+)\s\[(\w+-\d+)\]\s(.+)
匹配后,你可以通过 \1、\2 等反向引用获取每个分组的内容。在 Python 的 re.findall 或 re.match 中,这些分组会以元组形式返回。
3.3.2 非捕获分组
有时候你只想分组,不想捕获。比如你只想用 | 做选择,但不需要保存匹配结果。这时候用 (?:...):
(?:ERROR|WARNING|INFO):\s(.+)
这个正则只捕获后面的消息内容,前面的日志级别不会被保存。性能上比捕获分组略好,尤其是在大量匹配时。
3.3.3 命名分组
Python 和部分工具支持命名分组,语法是 (?P<name>...)。我个人非常推荐在复杂正则中使用命名分组,可读性会好很多:
(?P<date>\d{4}-\d{2}-\d{2})\s(?P<time>\d{2}:\d{2}:\d{2})\s(?P<level>\w+)
这样你就能通过 match.group('date') 直接获取日期,不用去数第几个括号了。
3.4 贪婪与非贪婪模式
这是正则新手最容易踩坑的地方。说白了,贪婪模式就是「能多匹配就多匹配」,非贪婪模式就是「能少匹配就少匹配」。
3.4.1 默认是贪婪的
看个例子。字符串是 <div>content1</div><div>content2</div>,你用正则 <div>.*</div> 去匹配。猜猜结果是什么?
答案是:它会匹配整个字符串,从第一个 <div> 一直吃到最后一个 </div>。因为 .* 默认是贪婪的,它会尽可能多地匹配字符。
3.4.2 非贪婪:加个问号
在量词后面加个 ?,就变成非贪婪模式了。同样的字符串,用 <div>.*?</div> 匹配,结果会是两个独立的匹配:
- 第一个:
<div>content1</div> - 第二个:
<div>content2</div>
为什么会这样?因为 .*? 在匹配到第一个 </div> 时就停下来了,不再继续往后吃。
| 模式 | 写法 | 行为 | 适用场景 |
|---|---|---|---|
| 贪婪 | .*、.+、.{2,} |
尽可能多匹配 | 提取整段内容,不关心内部结构 |
| 非贪婪 | .*?、.+?、.{2,}? |
尽可能少匹配 | 提取 HTML 标签、JSON 字段等有明确边界的内容 |
\{.*\},结果把整个日志行都吞了,因为第一个 { 和最后一个 } 之间隔了太多内容。改成 \{.*?\} 后,才正确提取出每个独立的 JSON 对象。记住:处理结构化数据时,非贪婪模式往往是你的朋友。
3.5 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以把它当作正则学习的「导航图」:
3.6 实战:用正则解析一行日志
理论讲完了,我们拿一条真实日志练练手。假设你有这样一行 Nginx 访问日志:
192.168.1.100 - - [15/Mar/2024:14:30:22 +0800] "GET /api/users HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
你想提取 IP、时间、请求方法、路径、状态码和响应大小。一个完整的正则可能是:
^(\d+\.\d+\.\d+\.\d+)\s.*?\[(.*?)\]\s"(\w+)\s(.*?)\s.*?"\s(\d+)\s(\d+)
分组对应关系:
\1—— IP 地址:192.168.1.100\2—— 时间戳:15/Mar/2024:14:30:22 +0800\3—— HTTP 方法:GET\4—— 请求路径:/api/users\5—— 状态码:200\6—— 响应大小:1234
正则这东西,光看是学不会的。我建议你打开终端或者在线正则测试工具,拿几行真实的日志数据,亲手写几个正则试试。从最简单的 \d+ 开始,慢慢加入分组和量词,你会发现自己越来越顺手。