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 中,这些分组会以元组形式返回。

我在项目中遇到过:有一次需要从百万行 Nginx 日志中提取所有 5xx 错误的请求路径。我用了一个带捕获分组的正则,一行代码就把所有错误路径捞出来了。如果没有分组,你得写一堆字符串切割逻辑,又慢又容易出错。

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 片段,用了 \{.*\},结果把整个日志行都吞了,因为第一个 { 和最后一个 } 之间隔了太多内容。改成 \{.*?\} 后,才正确提取出每个独立的 JSON 对象。记住:处理结构化数据时,非贪婪模式往往是你的朋友。

3.5 知识体系总览

下面这张图把本章的核心知识点串起来了。你可以把它当作正则学习的「导航图」:

正则表达式核心知识体系 正则表达式基础 元字符 量词 分组与捕获 贪婪与非贪婪 字符类 位置锚定 * + ? {n,m} 捕获分组 非捕获分组 命名分组 贪婪模式 非贪婪模式 核心原则:元字符定类型,量词控次数 分组做提取,贪婪/非贪婪决定匹配范围

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+ 开始,慢慢加入分组和量词,你会发现自己越来越顺手。