3、正则表达式入门:元字符、量词、分组与捕获、Python re模块实战、性能优化技巧

正则表达式,说白了就是一门「用符号描述文本模式」的语言。我刚开始接触时也觉得它像天书,一堆乱七八糟的符号。但后来在日志分析中天天跟它打交道,才发现——这玩意儿是真香。

你想想看,运维日志里动辄几万行,你要从中找出所有IP地址、提取错误码、过滤时间戳。手动翻?不现实。用字符串函数硬写?代码又臭又长。正则表达式就是干这个的。

3.1 元字符:正则的「字母表」

元字符是正则表达式最基本的构建块。它们不匹配字面字符,而是匹配某一类字符。

元字符 含义 示例
. 匹配任意单个字符(除换行符) a.c 匹配 "abc"、"a1c"
\d 匹配数字,等价于 [0-9] \d\d\d 匹配 "123"
\w 匹配字母、数字、下划线 \w+ 匹配 "hello_123"
\s 匹配空白字符(空格、制表符、换行) name\s+age 匹配 "name age"
^ 匹配字符串开头 ^ERROR 匹配以 "ERROR" 开头的行
$ 匹配字符串结尾 timeout$ 匹配以 "timeout" 结尾的行
[] 字符集,匹配括号内任意一个字符 [aeiou] 匹配任意元音字母
[^] 否定字符集,匹配不在括号内的字符 [^0-9] 匹配非数字字符
我的经验:在日志分析中,^$ 用得特别多。比如你要过滤所有以 "FATAL" 开头的行,直接 ^FATAL 就搞定了。我曾经在排查线上故障时,靠这个从 50 万行日志里 3 秒定位到关键错误。

3.2 量词:控制重复次数

光有元字符还不够,你还需要告诉正则「这个字符要出现几次」。量词就是干这个的。

量词 含义 示例
* 匹配前一个字符 0 次或多次 ab*c 匹配 "ac"、"abc"、"abbc"
+ 匹配前一个字符 1 次或多次 ab+c 匹配 "abc"、"abbc",不匹配 "ac"
? 匹配前一个字符 0 次或 1 次 colou?r 匹配 "color" 和 "colour"
{n} 精确匹配 n 次 \d{4} 匹配 "2024"
{n,} 至少匹配 n 次 \d{2,} 匹配 "12"、"123"、"1234"
{n,m} 匹配 n 到 m 次 \d{2,4} 匹配 "12"、"123"、"1234"
避坑指南:我曾经在写日志时间戳提取时,用了 \d* 去匹配数字,结果匹配到了空字符串。因为 * 允许 0 次匹配。后来我改成 \d+,问题就解决了。记住:能用 + 就别用 *,除非你真的需要匹配 0 次。

3.3 分组与捕获:提取关键信息

正则不只是用来「匹配」的,更重要的是「提取」。分组就是用来干这个的。

用圆括号 () 把一部分模式包起来,就形成了一个分组。分组有两个作用:一是改变优先级,二是捕获匹配的内容。

import re

# 提取日志中的 IP 地址和端口
log_line = "192.168.1.1:8080 - - [10/Oct/2024]"
pattern = r'(\d+\.\d+\.\d+\.\d+):(\d+)'

match = re.search(pattern, log_line)
if match:
    ip = match.group(1)   # "192.168.1.1"
    port = match.group(2) # "8080"
    print(f"IP: {ip}, Port: {port}")

我个人习惯给重要的分组起个名字,用 (?P<name>...) 语法。这样代码可读性会好很多。

pattern = r'(?P<ip>\d+\.\d+\.\d+\.\d+):(?P<port>\d+)'
match = re.search(pattern, log_line)
if match:
    print(match.group('ip'))   # 更清晰
    print(match.group('port'))
小技巧:如果你只想分组但不想捕获(比如用于 | 或运算),可以用 (?:...)。这在性能上有好处,因为 Python 不需要保存捕获内容。我在处理海量日志时,都会用非捕获分组来提升速度。

3.4 Python re 模块实战

Python 的 re 模块是正则的主力军。常用的就四个函数:

函数 作用 返回类型
re.search() 扫描整个字符串,找到第一个匹配 Match 对象或 None
re.match() 从字符串开头匹配 Match 对象或 None
re.findall() 找到所有匹配,返回列表 列表
re.sub() 替换匹配的内容 字符串

来看一个完整的日志分析实战:

import re

# 模拟 Nginx 访问日志
log_data = """
192.168.1.1 - - [10/Oct/2024:13:55:36 +0800] "GET /api/users HTTP/1.1" 200 1234
10.0.0.2 - - [10/Oct/2024:13:56:01 +0800] "POST /api/login HTTP/1.1" 401 56
192.168.1.1 - - [10/Oct/2024:13:57:22 +0800] "GET /api/orders HTTP/1.1" 500 789
"""

# 提取所有状态码为 5xx 的错误请求
pattern = r'(\d+\.\d+\.\d+\.\d+).*?"(\w+) (\S+).*?" (\d{3})'
errors = []

for line in log_data.strip().split('\n'):
    match = re.search(pattern, line)
    if match and match.group(4).startswith('5'):
        errors.append({
            'ip': match.group(1),
            'method': match.group(2),
            'path': match.group(3),
            'status': match.group(4)
        })

print(f"发现 {len(errors)} 个 5xx 错误")
for err in errors:
    print(f"{err['ip']} - {err['method']} {err['path']} -> {err['status']}")
注意:re.match() 只从字符串开头匹配,而 re.search() 扫描整个字符串。我见过不少新手用 re.match() 去匹配不在开头的模式,结果死活匹配不上。如果你不确定位置,就用 re.search()

3.5 性能优化技巧

正则表达式虽然强大,但写不好会非常慢。我在处理每天几 GB 的日志时,踩过不少坑。下面是我总结的几个优化点:

3.5.1 预编译正则

如果你要多次使用同一个正则,一定要预编译。这能省掉每次解析模式的开销。

import re

# 不好的写法:每次循环都编译
for line in log_lines:
    match = re.search(r'\d+\.\d+\.\d+\.\d+', line)
    # ...

# 好的写法:只编译一次
ip_pattern = re.compile(r'\d+\.\d+\.\d+\.\d+')
for line in log_lines:
    match = ip_pattern.search(line)
    # ...

3.5.2 避免回溯灾难

为什么会慢?很多时候是回溯导致的。比如 .* 这种贪婪匹配,在复杂模式下会疯狂回溯。

我曾经踩过的坑:.* 去匹配 HTML 标签,结果一个 100KB 的页面跑了 30 秒。后来改成 [^<>]*,瞬间完成。记住:能用字符集就别用 .*,能用非贪婪 .*? 就别用贪婪 .*

3.5.3 使用原始字符串

Python 中写正则一定要用 r'' 原始字符串。不然你要写一堆反斜杠转义,代码又丑又容易错。

# 错误:需要转义反斜杠
pattern = "\\d+\\.\\d+\\.\\d+\\.\\d+"

# 正确:原始字符串
pattern = r"\d+\.\d+\.\d+\.\d+"

3.5.4 锚定优化

如果你知道模式一定在行首或行尾,加上 ^$。这能让正则引擎快速失败,不用扫描整个字符串。

# 慢:扫描整行
pattern = r'ERROR.*timeout'

# 快:锚定行首
pattern = r'^ERROR.*timeout'
我的习惯:写正则时先想清楚「这个模式最可能出现在哪里」。如果是在行首,加 ^;如果是在特定位置,用 re.search() 而不是 re.findall()。这些小细节在百万行日志上能差出几十倍的速度。
正则表达式知识体系 正则表达式 元字符 . \d \w \s ^ $ [] 量词 * + ? {n,m} 分组与捕获 () (?P<name>) 性能优化 预编译 避免回溯 字符类 边界匹配 贪婪匹配 非贪婪匹配 捕获分组 非捕获分组 预编译 锚定优化 Python re 模块 search() match() findall() sub()

正则表达式这东西,说白了就是「用空间换时间」。你花 10 分钟写一个正则,可能省下 10 个小时的手工查找。但也要注意,正则不是万能的。如果日志格式特别复杂,或者你需要做上下文关联分析,那还是老老实实写解析器吧。

嗯,这一章的内容就到这里。记住:多练、多踩坑、多总结。正则的功力都是在实战中磨出来的。

专注资料整理