第3章 正则表达式入门:元字符、字符类、量词、分组与捕获
3.1 从一道真实需求说起
我记得刚入行那会儿,接到一个任务:从一堆服务器日志里找出所有异常 IP。
我吭哧吭哧写了三四十行字符串判断,一跑就漏数据。后来老同事瞟了一眼,甩过来一行正则,三秒搞定。那一刻我就明白了——正则表达式,是内容识别规则配置的必修课。
说白了,正则就是一门用符号来描述文本模式的"小语言"。你给它一个模式,它帮你在文本里精准命中目标。本章我们把四个核心概念讲透:元字符、字符类、量词、分组与捕获。
3.2 元字符 — 正则的"标点符号"
元字符是正则表达式的"标点"。它们不表字面意义,而是代表一类逻辑。我在项目中遇到过不少新人把 . 当句号用,结果匹配出奇怪的结果。嗯,这里要注意。
| 元字符 | 含义 | 示例 | 匹配结果 |
|---|---|---|---|
. | 匹配除换行外的任意字符 | a.c | "abc"、"a2c" |
^ | 匹配字符串开头 | ^abc | "abc123" |
$ | 匹配字符串结尾 | abc$ | "123abc" |
\ | 转义,让元字符变普通 | \. | "."(句号本身) |
| | 逻辑或 | a|b | "a" 或 "b" |
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} 搞定八成场景。但要精确校验,还得结合分组和边界符。后面实战会讲到。
3.3 字符类 — 匹配一组字符中的某一个
字符类用方括号 [...] 包裹,匹配其中任意一个字符。你想想看,如果没有字符类,写"匹配一个数字"就得用 0|1|2|3|4|5|6|7|8|9,那得多痛苦。
[abc]→ 匹配 a 或 b 或 c[^abc]→ 匹配除了 a、b、c 之外的字符[a-z]→ 匹配任意小写字母[0-9]等价于\d
常用预定义字符类(其实就是字符类的快捷写法):
| 预定义类 | 等价字符类 | 说明 |
|---|---|---|
\d | [0-9] | 数字 |
\w | [a-zA-Z0-9_] | 单词字符(含下划线) |
\s | [ \t\n\r\f] | 空白字符 |
\D | [^0-9] | 非数字 |
实战经验:我在配置日志识别规则时,经常用 [0-9a-fA-F] 来匹配 MAC 地址或十六进制串。比写一大串 \d|[a-f] 干净得多。
3.4 量词 — 控制匹配重复次数
量词解决的是"这个字符要重复多少次"的问题。默认情况下,量词都是贪婪的——能多匹配绝不少匹配。我曾经因为在日志采集规则里用了贪婪量词,导致一行日志被错误合并,排查了大半天。后来加上 ? 改成懒惰模式,问题瞬间解决。
| 量词 | 含义 | 示例 | 匹配 "abbbc" |
|---|---|---|---|
* | 0次或多次 | ab*c | "ac"、"abc"、"abbbc" |
+ | 1次或多次 | ab+c | "abc"、"abbbc"(不匹配 "ac") |
? | 0次或1次 | ab?c | "ac"、"abc" |
{n} | 恰好 n 次 | a{3} | "aaa" |
{n,} | 至少 n 次 | a{2,} | "aa"、"aaa" |
{n,m} | n 到 m 次 | a{2,4} | "aa"、"aaa"、"aaaa" |
? 变成懒惰模式。例如 a+? 在 "aaa" 中只匹配 "a"。内容识别规则中,如果想提取最短匹配片段,一定记得用懒惰量词。
3.5 分组与捕获 — 提取并复用匹配内容
分组用圆括号 () 实现。它有两个作用:一是把多个字符当成一个整体应用量词,二是把匹配的内容捕获下来供后续使用。我个人习惯在复杂规则中尽量用命名捕获,可读性会好很多。
基本分组
(ab)+ → 匹配 "ab"、"abab"、"ababab"
(\d{4})-(\d{2})-(\d{2}) → 捕获年、月、日
非捕获分组 (?:)
只想分组不想捕获时用 (?:),性能更好,也不会占用捕获编号。
(?:\d{3}-)?\d{8} → 匹配带区号或不带区号的电话
回溯引用 \1 \2
在同一个正则中引用前面捕获的内容,常用于匹配成对标签或重复词。
<(\w+)>.*</\1> → 匹配 <div>...</div> 等成对标签
命名捕获 (?P<name>...)
Python 中常用,用名字而非编号引用,规则维护时一目了然。
(?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})
3.6 综合实战:从日志中提取错误码和 IP
假设我们有这样一行日志:
2024-09-15 14:23:45 ERROR 192.168.1.100 "Connection timeout" code=5003
目标是提取出:IP 地址和错误码。
我推荐的正则方案(Python 语法):
import re
log = '2024-09-15 14:23:45 ERROR 192.168.1.100 "Connection timeout" code=5003'
pattern = r'(?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*code=(?P<errcode>\d+)'
match = re.search(pattern, log)
if match:
print(match.group('ip')) # 192.168.1.100
print(match.group('errcode')) # 5003
为什么这样写?
- 命名捕获让后续取值代码自解释
.*跳过中间干扰内容(注意这里用贪婪没问题,因为后面有code=兜底)\d+精准匹配数字错误码
把这个模式用到你的内容识别规则里,无论是 Nginx 日志、业务错误栈还是安全告警,都能快速抽取出关键字段。
正则入门不难,关键在于多写、多测、多踩坑。我在刚开始配置规则时,一个简单的邮箱正则都要改七八版。别怕,试多了自然就熟了。把元字符、字符类、量词、分组这四块吃透,你已经能解决 80% 的内容识别场景了。