正则不是玄学,是一门有明确语法的小语言;这篇从元字符讲到灾难性回溯,让你既能写对也能写快。
你将学到
re模块的compile/match/search/findall/sub各自语义- 元字符、字符类、量词,以及贪婪与非贪婪的差别
- 分组、命名分组、反向引用
- 邮箱、手机、IP、日期等常用校验正则
re.VERBOSE、re.DOTALL等标志,预编译与避免灾难性回溯的性能建议- 用正则解析日志文件的实战
前置知识
需要掌握字符串方法和文件读取,见 上一篇:文件与路径处理。
re 模块的五个核心函数
区分 match 和 search 是入门第一课:match 只从开头匹配,search 扫描整个串找第一个匹配。
import re
text = "订单号: A12345, 金额: 88"
# match:只从开头尝试,失败就返回 None
print(re.match(r"\d+", text)) # 输出: None(开头是“订”)
print(re.match(r"\D+", text)) # 输出: <re.Match object; span=(0, 3) ...>
# search:全串扫描,找到第一个就返回
m = re.search(r"\d+", text)
print(m.group()) # 输出: 88
# findall:返回所有匹配的字符串列表
print(re.findall(r"\d+", text)) # 输出: ['12345', '88']
# sub:替换所有匹配
print(re.sub(r"\d+", "#", text)) # 输出: 订单号: #, 金额: #
# compile:预编译成 Pattern,重复使用复用编译结果
phone_re = re.compile(r"1[3-9]\d{9}")
print(phone_re.search("联系 13812345678")) # 输出: <re.Match object; span=(3, 14) ...>
Match 对象上:group() 取整个匹配,group(n) 取第 n 组,groups() 取所有组,start()/end()/span() 给位置。
元字符与字符类
# . 任意字符(默认不含换行)
# \d 数字 0-9 \D 非数字
# \w 字母数字下划线 \W 非 \w
# \s 空白 \S 非空白
# ^ 行首 / 串首 $ 行尾 / 串尾
# [] 字符类 | 或 () 分组
print(re.findall(r"\w+", "hello_world 中文 abc123")) # 输出: ['hello_world', 'abc123']
print(re.findall(r"[aeiou]", "hello")) # 输出: ['e', 'o']
print(re.findall(r"[^aeiou\s]", "hello")) # 输出: ['h', 'l', 'l']
print(re.findall(r"cat|dog", "I have a cat and a dog")) # 输出: ['cat', 'dog']
注意 ^ 和 $ 默认匹配整段文本的首尾;配合 re.MULTILINE 才变成“每行”的首尾。
量词:贪婪与非贪婪
html = "<b>加粗</b><i>斜体</i>"
# 贪婪:尽可能多匹配
print(re.findall(r"<.*>", html)) # 输出: ['<b>加粗</b><i>斜体</i>']
# 非贪婪:量词后加 ?,尽可能少匹配
print(re.findall(r"<.*?>", html)) # 输出: ['<b>', '</b>', '<i>', '</i>']
# 常用量词
# * 0 次或多次
# + 1 次或多次
# ? 0 次或 1 次
# {n} 恰好 n 次
# {n,} 至少 n 次
# {n,m} n 到 m 次
解析 HTML 时“贪婪 vs 非贪婪”是最经典的坑。记住:默认贪婪,加 ? 变非贪婪。
分组、命名分组与反向引用
括号 () 既是分组(可被 group(n) 取用),也会被捕获。只想分组不想捕获用 (?:...)。
# 普通分组 + 位置取用
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "2026-10-07")
print(m.group(1), m.group(2), m.group(3)) # 输出: 2026 10 07
# 命名分组 (?P<name>...),可读性更好
m = re.search(r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})", "2026-10-07")
print(m.group("y"), m.groupdict()) # 输出: 2026 {'y': '2026', 'm': '10', 'd': '07'}
# 非捕获分组 (?:...) —— 只分组不占编号
print(re.findall(r"(?:AB)+", "ABABAC")) # 输出: ['ABAB']
# 反向引用 \1 / (?P=name):匹配前面出现过的相同内容
print(re.search(r"(\w+) \1", "hey hey there")) # 输出: hey hey
命名分组 + sub 配合回调或 \g<name>,是做文本重排的利器:
# 把 MM/DD/YYYY 改成 YYYY-MM-DD
print(re.sub(r"(?P<m>\d{2})/(?P<d>\d{2})/(?P<y>\d{4})",
r"\g<y>-\g<m>-\g<d>", "10/07/2026"))
# 输出: 2026-10-07
常用校验正则
# 邮箱(够用版,别追求完美——RFC 里合法的邮箱格式复杂到没人想背)
EMAIL = re.compile(r"^[\w.+-]+@[\w-]+\.[\w.-]+$")
print(bool(EMAIL.match("a.b+c@x.co"))) # 输出: True
# 中国大陆手机号
PHONE = re.compile(r"^1[3-9]\d{9}$")
print(bool(PHONE.match("13812345678"))) # 输出: True
print(bool(PHONE.match("12345678901"))) # 输出: False
# IPv4(每段 0-255,校验范围)
IPV4 = re.compile(
r"^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}"
r"(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$"
)
print(bool(IPV4.match("192.168.1.1"))) # 输出: True
print(bool(IPV4.match("999.1.1.1"))) # 输出: False
# 日期 YYYY-MM-DD(只校验格式,不校验 2 月 30 日这种)
DATE = re.compile(r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$")
print(bool(DATE.match("2026-13-01"))) # 输出: False
实际校验日期请用 datetime.strptime——正则管格式,语义交给专用库。
标志(flags)
# re.IGNORECASE(或 re.I):忽略大小写
print(re.findall(r"hello", "Hello HELLO", re.I)) # 输出: ['Hello', 'HELLO']
# re.DOTALL(或 re.S):让 . 也能匹配换行
print(re.search(r"a.b", "a\nb", re.S)) # 输出: <re.Match object; ...>
# re.MULTILINE(或 re.M):^ $ 匹配每行
print(re.findall(r"^\w+", "foo\nbar", re.M)) # 输出: ['foo', 'bar']
# re.VERBOSE(或 re.X):忽略空白和 # 注释,写复杂正则时可读性大增
pattern = re.compile(r"""
(?P<ip>\d{1,3}(?:\.\d{1,3}){3}) # IP 地址
\s-\s-\s
\[(?P<ts>[^\]]+)\] # 时间戳
\s"(?P<method>\w+)\s(?P<path>\S+) # 请求方法与路径
""", re.VERBOSE)
re.VERBOSE 是长正则的救命稻草——否则你会写出没人看懂的一行天书。
性能提示
# ✅ 循环里反复用的是同一个正则:预编译一次,复用
WORD = re.compile(r"\w+")
for line in lines:
WORD.findall(line) # 不重复编译
# ❌ 每次循环都调用 re.findall,内部会走缓存但仍有开销
for line in lines:
re.findall(r"\w+", line)
re 内部有 LRU 缓存(默认 512 条),所以小脚本里 re.findall 也能接受;但热路径和复杂正则一定要 compile。
灾难性回溯是正则的性能杀手:嵌套量词遇到长的不匹配串时,回溯次数指数级爆炸。
# ❌ 危险:(a+)+ 这种嵌套量词,遇到不匹配的长串会指数级回溯
bad = re.compile(r"(a+)+$")
# 输入 "aaaaaaaaaaaaaaaaaaaaaaaaaaX" 会让它卡死几秒到几分钟
# ✅ 用原子/占有式思路改写,或者干脆用字符串方法
good = re.compile(r"a+$") # 语义相同的场景下别套嵌套量词
# ✅ Python 3.11+ 支持占有量词 ++ *+ ?+,匹配后不回溯
no_backtrack = re.compile(r"a++$")
原则:能用简单结构就别用嵌套量词;能用字符串方法(in、startswith、str.split)就别上正则。
实战:解析 Nginx 访问日志
import re
LOG_RE = re.compile(r"""
(?P<ip>\d{1,3}(?:\.\d{1,3}){3}) # 客户端 IP
\s-\s-\s
\[(?P<time>[^\]]+)\] # 时间,如 07/Oct/2026:10:00:00 +0800
\s"(?P<method>[A-Z]+)\s(?P<path>\S+)\s[^"]*" # 请求行
\s(?P<status>\d{3})\s # 状态码
(?P<size>\d+) # 响应字节数
""", re.VERBOSE)
raw = '''192.168.1.1 - - [07/Oct/2026:10:00:00 +0800] "GET /index.html HTTP/1.1" 200 1024
10.0.0.5 - - [07/Oct/2026:10:00:01 +0800] "POST /api/login HTTP/1.1" 401 0
192.168.1.1 - - [07/Oct/2026:10:00:02 +0800] "GET /favicon.ico HTTP/1.1" 404 0'''
stats = {}
total_bytes = 0
for line in raw.splitlines():
m = LOG_RE.search(line)
if not m:
continue # 格式不符就跳过,别让一行坏数据炸掉整个解析
d = m.groupdict()
stats[d["status"]] = stats.get(d["status"], 0) + 1
total_bytes += int(d["size"])
print(stats) # 输出: {'200': 1, '401': 1, '404': 1}
print(total_bytes) # 输出: 1024
解析非结构化文本时,正则 + 命名分组 + groupdict() 的组合比手工 split 稳定得多。
常见坑
# ❌ 用 search 却以为它从开头匹配,把 “a1b2” 里的数字也捞出来
re.search(r"\d+", "a1b2").group() # 输出: 1(不是从开头,是第一个数字)
# ✅ 需要从开头就锚定,用 match 或 ^
re.match(r"\d+", "a1b2") # 输出: None
# ❌ 忘记转义点号,. 会匹配任意字符
re.findall(r"1.5", "125 1-5 1.5") # 输出: ['125', '1-5', '1.5']
# ✅ 想要字面量点号就转义
re.findall(r"1\.5", "125 1-5 1.5") # 输出: ['1.5']
# ❌ 用户输入直接拼进正则,元字符引发意外甚至注入
user = "a.b"
re.search(user, "axb") # 输出: 匹配成功!. 被当成任意字符
# ✅ 用 re.escape 转义用户输入
re.search(re.escape(user), "axb") # 输出: None
# ❌ 贪婪量词解析成对标签,一口吞掉中间所有
re.findall(r"<.*>", "<b>x</b>") # 输出: ['<b>x</b>']
# ✅ 非贪婪,或者用更精确的字符类
re.findall(r"<.*?>", "<b>x</b>") # 输出: ['<b>', '</b>']
小结
match从开头匹配,search全串扫描,别再搞混。- 默认贪婪,量词后加
?变非贪婪;嵌套量词((a+)+)是灾难性回溯的重灾区。 - 优先用命名分组
(?P<name>...)+groupdict(),代码自解释。 - 常用校验用现成正则,但日期等语义校验交给
datetime。 - 热路径预编译;
re.VERBOSE让复杂正则可维护;用户输入一律re.escape。 - 能用字符串方法解决的,别动用正则。
延伸阅读
- 官方文档:
re模块,注意re.Match对象 - 下一篇 数据处理 json/csv/dataclasses 处理结构化数据,和正则处理非结构化文本互补
上一篇:文件与路径处理 · 下一篇:数据处理 json/csv/dataclasses
文章回复
0 条公开回复