3. Python re 模块:五个核心函数实战

正则表达式在日志解析里有多重要?我这么说吧——你处理的每一条日志,几乎都要过一遍正则。Python 的 re 模块提供了五个核心函数,搞懂它们,你就能搞定 90% 的日志解析场景。

这五个函数分别是:re.match、re.search、re.findall、re.sub、re.compile。它们各有各的脾气,用错了地方,轻则匹配不到数据,重则性能崩盘。我踩过的坑,今天一次性给你讲清楚。

核心要点:这五个函数不是互相替代的关系,而是各司其职。选对函数,比写对正则更重要。

Python re 模块五个核心函数 re 模块核心函数 re.match re.search re.findall re.sub re.compile 从字符串开头匹配 返回 match 对象 扫描整个字符串 返回第一个匹配 返回所有匹配 返回列表或元组 替换匹配内容 支持函数替换 预编译正则对象 提升重复匹配性能 典型使用场景 日志行首校验 提取关键字段 批量提取 IP/时间 脱敏/清洗日志 高频匹配优化 💡 选对函数 = 代码可读性 + 性能 + 正确性

3.1 re.match:从开头开始,别想偷懒

re.match 只从字符串的开头匹配。如果开头不匹配,直接返回 None。很多人刚学的时候以为它和 re.search 一样,结果踩了坑。

我举个例子。假设你有一条日志:

2024-01-15 10:30:45 ERROR [app.py:42] 数据库连接超时

你想提取日期。用 re.match 没问题,因为日期就在开头:

import re

log = "2024-01-15 10:30:45 ERROR [app.py:42] 数据库连接超时"
pattern = r"\d{4}-\d{2}-\d{2}"

match = re.match(pattern, log)
if match:
    print(match.group())  # 输出:2024-01-15

但如果你要提取错误级别 ERROR,它不在开头,re.match 就匹配不到。这时候就得用 re.search。

我曾经犯过的错:有一次我写日志解析脚本,用 re.match 去匹配日志中间的 IP 地址,结果跑了半天一条都没匹配到。排查了半小时才发现是函数选错了。记住:re.match 只看开头,不看中间。

3.2 re.search:扫描全文,找到第一个就收工

re.search 会扫描整个字符串,返回第一个匹配结果。它不挑位置,只要字符串里有就行。

还是刚才那条日志,提取 ERROR:

pattern = r"ERROR|WARNING|INFO|DEBUG"
match = re.search(pattern, log)
if match:
    print(match.group())  # 输出:ERROR

你想想看,如果日志里有多个 ERROR,re.search 也只返回第一个。要拿全部,得用 re.findall。

我的习惯:在日志解析里,re.search 是我用得最多的函数。因为日志结构复杂,字段位置不固定,re.search 最灵活。但要注意——它只返回第一个匹配,别指望它帮你批量提取。

3.3 re.findall:全都要,一个不落

re.findall 返回所有匹配结果,以列表形式返回。如果正则里有分组,返回的是元组列表。

假设你要从一堆日志里提取所有 IP 地址:

logs = """
192.168.1.1 - - [15/Jan/2024:10:30:45] "GET /api/users"
10.0.0.5 - - [15/Jan/2024:10:31:12] "POST /api/login"
172.16.0.10 - - [15/Jan/2024:10:32:01] "GET /api/data"
"""

pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
ips = re.findall(pattern, logs)
print(ips)
# 输出:['192.168.1.1', '10.0.0.5', '172.16.0.10']

如果正则里有分组,比如你想同时提取 IP 和时间:

pattern = r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*?\[(.*?)\]"
results = re.findall(pattern, logs)
print(results)
# 输出:[('192.168.1.1', '15/Jan/2024:10:30:45'), ('10.0.0.5', '15/Jan/2024:10:31:12'), ('172.16.0.10', '15/Jan/2024:10:32:01')]

性能提醒:如果日志文件很大(比如几百 MB),re.findall 会一次性把所有结果加载到内存。我遇到过内存爆掉的案例。这时候建议逐行读取,或者用 re.finditer 返回迭代器。

3.4 re.sub:替换的艺术

re.sub 用于替换匹配到的内容。它在日志清洗和脱敏场景里特别有用。

比如你要把日志里的 IP 地址替换成 ***.***.***.***:

log_line = "用户 192.168.1.1 在 10:30:45 登录系统"
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
cleaned = re.sub(pattern, "***.***.***.***", log_line)
print(cleaned)
# 输出:用户 ***.***.***.*** 在 10:30:45 登录系统

re.sub 还支持用函数做动态替换。我做过一个需求:把日志里的时间戳统一格式:

def convert_time(match):
    from datetime import datetime
    old_time = match.group()
    dt = datetime.strptime(old_time, "%Y-%m-%d %H:%M:%S")
    return dt.strftime("%Y/%m/%d %H:%M:%S")

log = "2024-01-15 10:30:45 ERROR 连接超时"
pattern = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"
result = re.sub(pattern, convert_time, log)
print(result)
# 输出:2024/01/15 10:30:45 ERROR 连接超时

避坑指南:re.sub 的替换字符串里,\1、\2 可以引用分组。但如果你用的是 Python 3.6+,建议用 \g<1>、\g<name> 这种写法,可读性更好,也不容易出错。

3.5 re.compile:预编译,性能翻倍

re.compile 把正则表达式编译成一个 Pattern 对象。之后你可以反复用它来匹配,不用每次都重新解析正则。

我为什么强调这个?因为正则编译是有开销的。如果你在循环里反复用同一个正则,不编译的话,性能差距可能达到 10 倍以上。

看个对比:

import re
import time

logs = ["2024-01-15 10:30:45 ERROR msg1"] * 100000

# 不编译
start = time.time()
for log in logs:
    re.search(r"\d{4}-\d{2}-\d{2}", log)
print("不编译耗时:", time.time() - start)

# 编译
start = time.time()
pattern = re.compile(r"\d{4}-\d{2}-\d{2}")
for log in logs:
    pattern.search(log)
print("编译后耗时:", time.time() - start)

在我机器上,不编译跑了 0.8 秒,编译后只用了 0.1 秒。差距 8 倍。你想想看,如果每天处理几千万条日志,这个优化能省多少时间。

我的最佳实践:在日志解析系统里,我会在模块加载时就把所有正则编译好,存成一个字典。运行时直接取用,既快又清晰。

# 日志解析规则预编译
PATTERNS = {
    "timestamp": re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"),
    "ip": re.compile(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"),
    "error_level": re.compile(r"ERROR|WARNING|INFO|DEBUG"),
    "request_url": re.compile(r'"(GET|POST|PUT|DELETE) (\S+)'),
}

def parse_log_line(line):
    timestamp = PATTERNS["timestamp"].search(line)
    ip = PATTERNS["ip"].search(line)
    level = PATTERNS["error_level"].search(line)
    url = PATTERNS["request_url"].search(line)
    return {
        "timestamp": timestamp.group() if timestamp else None,
        "ip": ip.group() if ip else None,
        "level": level.group() if level else None,
        "url": url.group(2) if url else None,
    }

3.6 五个函数对比速查表

函数 匹配位置 返回类型 典型场景 性能注意
re.match 仅开头 Match 或 None 校验日志行首格式 快,但限制大
re.search 全文第一个 Match 或 None 提取单个字段 适中
re.findall 全文所有 列表 / 元组列表 批量提取 IP、时间 大文件注意内存
re.sub 全文所有 字符串 脱敏、格式转换 函数替换略慢
re.compile — Pattern 对象 高频匹配优化 预编译提升明显

我曾经踩过的坑:有一次线上日志解析服务突然变慢,排查发现是有人在循环里写了 re.search(r"...", line),没有预编译。同一个正则每行日志都编译一次,百万行日志就是百万次编译。改成 re.compile 后,CPU 使用率直接降了 70%。