深色模式
正则表达式实战
摘要:正则在运维中的主要用途是「从非结构化文本里提取结构化字段」。本文用 Python
re演示提取 IP、时间、日志级别、URL 参数等常见场景,讲清贪婪/非贪婪、分组命名、断言这些容易混淆的概念,以及灾难性回溯这个性能陷阱。
适用环境
bash
python3 --version
which grep egrep awk
# 在线调试推荐 regex101.com(选 Python 或 PCRE 语法)1
2
3
2
3
操作步骤
一、先掌握最小可用集
text
. 任意单个字符(不含换行)
\d 数字,\w 单词字符,\s 空白
* 0 次或多次 + 1 次或多次 ? 0 或 1 次
{n,m} 重复 n 到 m 次
[] 字符集合,如 [0-9a-f]
^ $ 行首/行尾
| 或
() 分组,可提取1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
二、提取 IP 与时间
python
import re
line = '2026-10-09 14:03:11 [ERROR] client 10.0.1.25:54321 GET /api/order?id=8891 failed'
# 提取 IPv4(够用版:不校验每段 <=255,运维场景通常足够)
m = re.search(r"\b\d{1,3}(?:\.\d{1,3}){3}\b", line)
print(m.group()) # 10.0.1.25
# 提取时间戳(命名分组,便于后续按名取值)
m = re.search(r"(?P<date>\d{4}-\d{2}-\d{2})\s+(?P<time>\d{2}:\d{2}:\d{2})", line)
print(m.group("date"), m.group("time")) # 2026-10-09 14:03:11
# 提取日志级别
m = re.search(r"\[(?P<level>[A-Z]+)\]", line)
print(m.group("level")) # ERROR1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(?:...) 是非捕获分组——只用于分组匹配,但不会占用捕获编号,性能也更好。
三、结构化解析整行日志
python
LOG_RE = re.compile(
r"^(?P<date>\d{4}-\d{2}-\d{2})\s+"
r"(?P<time>\d{2}:\d{2}:\d{2})\s+"
r"\[(?P<level>\w+)\]\s+"
r"client\s+(?P<ip>\d{1,3}(?:\.\d{1,3}){3}):(?P<port>\d+)\s+"
r"(?P<method>[A-Z]+)\s+(?P<path>\S+)"
r"(?:\s+(?P<msg>.*))?$"
)
m = LOG_RE.match(line)
if m:
print(m.groupdict())1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
匹配不到时 m 为 None,脚本里必须先判断再 group(),否则抛 AttributeError。
四、贪婪 vs 非贪婪
这是最容易踩的坑:
python
html = '<div>a</div><div>b</div>'
# 贪婪:尽可能多地匹配,一直吃到最后一个 </div>
re.search(r"<div>.*</div>", html).group()
# '<div>a</div><div>b</div>'
# 非贪婪(加 ?):吃到第一个就停
re.search(r"<div>.*?</div>", html).group()
# '<div>a</div>'1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
提取引号内内容同理:
python
s = 'name="app" env="prod"'
re.findall(r'"(.*)"', s) # ['app" env="prod'] ← 贪婪,错了
re.findall(r'"(.*?)"', s) # ['app', 'prod'] ← 正确1
2
3
2
3
五、断言:匹配但不消费
python
# 提取金额数字,但不包含后面的单位
re.findall(r"\d+(?=MB)", "used 120MB free 880MB") # ['120', '880']
# 只匹配前面是 error: 的数字
re.findall(r"(?<=error: )\d+", "error: 500, ok: 200") # ['500']
# 负向断言:不包含 test 的主机名
hosts = ["web-01", "web-test-02", "web-03"]
[h for h in hosts if re.search(r"^(?!.*test)", h)] # ['web-01', 'web-03']1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
六、Shell 里的正则
bash
# grep -E 使用扩展正则
grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}' app.log
# 提取并输出(grep -oP 支持 Perl 正则,含非贪婪与断言)
grep -oP 'client \K[0-9.]+(?=:)' app.log
# sed 反向引用提取子串
echo "ip=10.0.1.5 port=80" | sed -nE 's/.*ip=([0-9.]+).*port=([0-9]+).*/ip:\1 port:\2/p'1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
七、性能陷阱:灾难性回溯
python
import re, time
# 危险模式:嵌套量词,遇到不匹配的长串会指数级爆炸
bad = re.compile(r"^(a+)+$")
s = "a" * 30 + "!"
t = time.time()
try:
bad.match(s) # 会卡住极长时间
except Exception:
pass
print("耗时", time.time() - t)
# 安全写法:避免嵌套量词,或改用明确的字符集合
good = re.compile(r"^a+$")
good.match(s)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意
正则处理用户输入或超长日志行时,务必避免 (a+)+、(.*)+ 这类嵌套量词,否则一条特定构造的输入就能让脚本 CPU 跑满。这就是 ReDoS(正则拒绝服务)。
八、调试与验证
bash
# Python 里打印编译后的模式,确认转义正确
python3 -c "import re; print(re.compile(r'\d+\.\d+\.\d+\.\d+').pattern)"
# 用 findall 快速验证提取结果是否符合预期
python3 - <<'PY'
import re
sample = open('/var/log/app/app.log').readline()
print(re.findall(r'\[(\w+)\]', sample))
PY1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
建议做法:先用少量样本验证正则,再应用到全量数据;正则写复杂时应拆成多步,可读性与可维护性都更好。
验证
- [ ] IP、时间、级别三类字段都能正确提取
- [ ] 非贪婪例子能正确区分「第一个」与「最后一个」
- [ ]
groupdict()输出的字段完整且无None异常 - [ ] 用构造的长字符串验证了没有灾难性回溯
bash
python3 -c "
import re
l='2026-10-09 14:03:11 [ERROR] client 10.0.1.25:54321 GET /api/order?id=8891 failed'
print(re.search(r'(?P<ip>\d{1,3}(?:\.\d{1,3}){3})', l).group('ip'))
"1
2
3
4
5
2
3
4
5
常见坑
- 忘记原始字符串:Python 里应写
r"\d+",否则\d会被当成转义序列。 - 贪婪吃掉多余内容:提取引号、标签内文本时必加
?。 - 用正则解析 HTML/JSON:嵌套结构用正则几乎必然出错,应使用专门的解析器。
- 匹配结果未判空:
m.group()在m is None时崩溃。 - 正则过于复杂无人能改:一行超长正则应拆分成多步匹配,并写注释说明意图。