4、Python正则模块:re模块核心函数与编译正则对象

正则表达式这东西,说白了就是文本处理的瑞士军刀。我在运维日志分析这行干了这么多年,可以负责任地告诉你——不会正则,你连日志的毛都摸不着。今天咱们就把 Python 的 re 模块掰开揉碎了讲清楚。

4.1 先搞明白:正则到底在干嘛?

想象一下,你面前有 10 万行 Nginx 日志。你想找出所有 5xx 错误,还想知道每个错误对应的 URL。用手翻?别闹了。正则就是帮你用一套「模式字符串」去匹配、提取、替换文本的工具。

Python 的 re 模块提供了几个核心函数,我平时用得最多的就是这四个:match、search、findall、sub。再加上编译正则对象,基本能覆盖 90% 的日志处理场景。

核心概念:正则不是 Python 独有的,但 Python 的 re 模块把正则的威力封装得特别顺手。你只要记住——模式字符串 + 目标字符串 = 匹配结果。

4.2 match:从开头死磕

re.match() 这个函数,它的脾气很倔——只从字符串的开头开始匹配。开头对不上,直接返回 None,后面写得再花哨也没用。

import re

log_line = "2024-01-15 10:30:45 ERROR [app] Connection timeout"
pattern = r"\d{4}-\d{2}-\d{2}"  # 匹配日期

result = re.match(pattern, log_line)
if result:
    print(f"匹配到: {result.group()}")
else:
    print("没匹配到")

# 输出: 匹配到: 2024-01-15

我在项目里遇到过一种情况:有人用 match 去匹配日志中间的错误码,结果死活匹配不上。为啥?因为 match 只看开头。你想想看,日志开头通常是时间戳,错误码在中间,用 match 当然不行。这时候就该换 search 了。

注意:match 只匹配开头。如果你不确定目标文本在字符串的什么位置,别用 match,用 search。

4.3 search:满世界找第一个

re.search() 就灵活多了。它会扫描整个字符串,找到第一个匹配的位置就停下来。返回的也是一个匹配对象,用法和 match 一样。

log_line = "2024-01-15 10:30:45 ERROR [app] HTTP 500 - Internal Server Error"
pattern = r"HTTP \d{3}"  # 匹配 HTTP 状态码

result = re.search(pattern, log_line)
if result:
    print(f"找到状态码: {result.group()}")
else:
    print("没找到")

# 输出: 找到状态码: HTTP 500

我个人习惯是:只要不是确定要从开头匹配,一律用 search。它比 match 宽容得多,不容易踩坑。

4.4 findall:一网打尽

有时候你需要的不只是第一个匹配,而是所有匹配。比如从一整天的日志里提取所有 IP 地址。这时候 re.findall() 就派上用场了。

log_text = """
192.168.1.1 - - [15/Jan/2024:10:30:45] "GET /api/users HTTP/1.1" 200
10.0.0.5 - - [15/Jan/2024:10:31:12] "POST /api/login HTTP/1.1" 401
172.16.0.3 - - [15/Jan/2024:10:32:01] "GET /api/data HTTP/1.1" 500
"""

pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"  # 匹配 IP 地址
ip_list = re.findall(pattern, log_text)

print(f"共找到 {len(ip_list)} 个 IP 地址:")
for ip in ip_list:
    print(f"  - {ip}")

# 输出:
# 共找到 3 个 IP 地址:
#   - 192.168.1.1
#   - 10.0.0.5
#   - 172.16.0.3

findall 返回的是一个列表。如果正则里没有分组,就返回匹配到的完整字符串列表。如果有分组,就返回元组列表。这个细节我吃过亏——有一次我写了个带分组的正则,以为返回的是字符串列表,结果遍历的时候直接报错。

小技巧:用 findall 的时候,如果正则里有分组,返回的是元组列表。想要完整匹配?用 ?: 把分组变成非捕获组,比如 (?:\d{3})。

4.5 sub:替换的艺术

日志分析不只是提取,有时候还需要替换。比如把日志里的敏感信息脱敏,或者把时间格式统一。这时候 re.sub() 就是你的好帮手。

log_line = "用户 张三 登录失败,IP: 192.168.1.100,时间: 2024-01-15 10:30:45"

# 把 IP 地址替换成 ***.***.***.***
pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
replacement = "***.***.***.***"

safe_log = re.sub(pattern, replacement, log_line)
print(safe_log)

# 输出: 用户 张三 登录失败,IP: ***.***.***.***,时间: 2024-01-15 10:30:45

sub 的第四个参数 count 可以控制替换次数。默认是 0(全部替换)。我曾经在替换日志里的密码时忘了设 count,结果把同一行里所有看起来像密码的东西全换了,搞得调试了半天。嗯,这里要注意——count 设成 1 只替换第一个匹配,更安全。

4.6 编译正则对象:性能与复用

如果你要在循环里反复用同一个正则,每次都调用 re.match() 或 re.search() 其实挺浪费的。Python 每次都会重新编译正则表达式。这时候就该用 re.compile() 了。

import re

# 编译一次,多次使用
pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}")

log_lines = [
    "2024-01-15 10:30:45 ERROR ...",
    "2024-01-15 10:31:12 INFO ...",
    "2024-01-15 10:32:01 WARN ...",
]

for line in log_lines:
    match = pattern.search(line)
    if match:
        print(f"时间戳: {match.group()}")

# 输出:
# 时间戳: 2024-01-15 10:30:45
# 时间戳: 2024-01-15 10:31:12
# 时间戳: 2024-01-15 10:32:01

编译后的正则对象有和 re 模块一样的方法:match()、search()、findall()、sub()。用法完全一样,只是不用再传 pattern 参数了。

性能对比:在循环 10 万次的情况下,编译后的正则比每次都调用 re.match() 快 3-5 倍。日志分析动辄几十万行,这个优化值得做。

4.7 四个核心函数对比

为了方便你快速查阅,我把这四个函数的区别整理成了表格:

函数 匹配位置 返回结果 适用场景
re.match() 只从开头 匹配对象或 None 检查字符串是否以某模式开头
re.search() 扫描整个字符串 第一个匹配对象或 None 查找第一个匹配项
re.findall() 扫描整个字符串 列表(所有匹配) 提取所有匹配项
re.sub() 扫描整个字符串 替换后的字符串 文本替换/脱敏

4.8 知识体系总览

下面这张图把本章的核心知识点串起来了。你可以把它当成一张「正则模块速查地图」:

Python re 模块核心函数与编译正则对象知识体系 Python re 模块核心知识体系 re 模块 re.match() re.search() re.findall() re.sub() re.compile() → 编译正则对象 pattern.match() pattern.search() pattern.findall() pattern.sub() 核心要点 • match 只从开头匹配,search 扫描全文找第一个,findall 找所有,sub 做替换 • 编译正则对象 (re.compile) 在循环中性能提升 3-5 倍,日志分析场景强烈推荐 • 四个核心函数 + 编译对象 = 覆盖 90% 的日志处理需求

4.9 避坑指南

最后,分享几个我踩过的坑:

  • match 不是 search:我曾经在写日志解析器时,用 match 去匹配日志中间的错误码,结果返回永远是 None。排查了半天才发现问题。记住:match 只看开头。
  • findall 的分组陷阱:正则里一旦有分组,findall 返回的就是元组列表。如果你只想要完整匹配,用 (?:...) 非捕获组。
  • sub 的 count 参数:默认替换所有匹配。如果你只想替换第一个,记得设 count=1。我在脱敏日志时吃过这个亏。
  • 编译正则的 flag:re.compile() 可以传 flag,比如 re.IGNORECASE。编译时设好 flag,后面调用就不用再传了。

我的建议:写日志分析脚本时,先把正则编译好,再用编译对象的方法去匹配。代码更清晰,性能也更好。养成这个习惯,你会感谢自己的。