Regex compile (正则表达式编译)
下面是一份 项目级的《Python 正则表达式编译与最佳实践》,涵盖:
- 正则编译最佳方式
- 性能优化
- 项目中推荐的使用模式
- 常见错误避免
- 提供完整可用的 utils.regex 工具模块
全部兼容 Python 3.12 / BasedPyright,可直接加入你的项目。
Python 的正则模块 re 工作方式:
| 写法 | 是否每次都重新编译正则? | 性能 |
|---|---|---|
| re.search(pattern, text) | ✅ 是 | ❌ 较慢 |
| compiled = re.compile(pattern); compiled.search(text) | ❌ 否(复用) | ✅ 快 |
在 循环 / 高频调用 / 大规模数据处理 / HTTP请求爬虫 / 定时任务 中,推荐:
- 正则提前编译
- 重复复用
- 不要在循环中写 re.search(pattern, text)
PAT_STOCK_CODE = re.compile(r"^(00|60)\d+")
PAT_EQUAL = re.compile(r"=(.+)$")
PAT_DATE_CN = re.compile(r"(\d{4})年(\d{2})月(\d{2})日")
PAT_MULTI = re.compile(r"foo.*bar", re.DOTALL)
| Flag | 含义 | 使用场景 |
|---|---|---|
| re.I | 忽略大小写 | login、email |
| re.M | 多行 | 批量解析日志 |
| re.S | dot 匹配换行(.->匹配任意字符) | 抓取 HTML 块 |
| re.X | 允许空白注释 | 长 regex 增强可读性 |
| re.A | ASCII 模式 | 性能优化 |
| re.U | Unicode 模式 | 处理中文/emoji |
❌ 错误
"\d+"
```py
✅ 正确
```py
r"\d+"
❌ 错误
for item in items:
if re.search(r"\d+", item): ...
✅ 正确
PAT_NUM = re.compile(r"\d+")
for item in items:
if PAT_NUM.search(item): ...
比如:
替换固定内容
s.replace("abc", "def")
分割固定字符
s.split(",")
只有当:
- 动态匹配
- 用到逻辑 OR
- 异类字符处理
- 提取结构化文本
才使用 regex。
示例:匹配 YYYY-MM-DD
PAT_DATE = re.compile(
r"""
^ # 行开始
(\d{4}) # 年
- # 分隔符
(0[1-9]|1[0-2]) # 月
- # 分隔符
(0[1-9]|[12]\d|3[01]) # 日
$
""",
re.X
)
PAT_KV = re.compile(r"[?&](\w+)=([^&]+)")
PAT_STOCK = re.compile(r"^(00|60)\d{4}$")
PAT_TAG = re.compile(r"<[^>]+>")
PAT_DATE_CN = re.compile(r"(\d{4})年(\d{1,2})月(\d{1,2})日")
import re
from typing import Iterable
def compile_pattern(pattern: str, flags=0) -> re.Pattern:
"""统一的正则编译封装"""
return re.compile(pattern, flags)
def regex_search(pattern: re.Pattern, text: str) -> str | None:
"""返回捕获组 1 的内容"""
m = pattern.search(text)
return m.group(1) if m else None
def regex_split(pattern: re.Pattern, text: str) -> list[str]:
"""正则分割封装"""
return pattern.split(text)
def regex_findall(pattern: re.Pattern, text: str) -> list[str]:
"""批量查找匹配"""
return pattern.findall(text)
def regex_sub(pattern: re.Pattern, repl: str, text: str) -> str:
"""替换"""
return pattern.sub(repl, text)
TITLE_EXCLUDE = re.compile(
r"板块局部异动|持续活跃|持续下挫|持续走强|触发二次临停|概念股表现活跃",
)
DATE_CN = re.compile(r"(\d{4})年(\d{2})月(\d{2})日")
使用:
DATE_CN.sub(r"\1-\2-\3", s)
模块级别定义:
PAT_CODE = re.compile(r"^\d+$")
for m in PAT_CODE.finditer(text):
...
❌ 回溯非常慢
.*</div>
✅ 限制范围
[^<]+
你可能想做的是:
把一个字符串列表(多个正则模式)一次性编译成正则对象,并统一匹配。
Python 的 re.compile() 只能接受 单个字符串,不能直接对 list 编译。
但有两种最佳实践方式:
- 把多个模式合成一个正则再 compile(推荐)
- 对每个模式分别 compile 成多个正则对象
下面分别讲述最佳实践。
适用于:
- 多个关键词任意一个匹配即可
- 大量重复匹配
- 性能需求高
- 项目中常用于过滤标题、关键词触发、黑名单匹配
比如:
patterns = [
"持续活跃",
"持续下挫",
"反复走强",
]
🔥 一次性编译:
import re
def compile_pattern_list(patterns: list[str]) -> re.Pattern:
combined = "|".join(map(re.escape, patterns))
return re.compile(combined)
使用:
TITLE_EXCLUDE = compile_pattern_list(patterns)
if TITLE_EXCLUDE.search(title):
print("命中排除项")
🔥 优点
- 仅编译一次 -> 性能高
- 正则对象可复用
- 使用简单 (pattern.search())
- 支持大量模式(几十、几百都没问题)
- 防止特殊字符冲突(因为使用了 re.escape)
⚠️ 注意
如果要写复杂模式,不需要 escape,可以改成:
combined = "|".join(patterns)
适用于:
- 每个模式处理逻辑不一样
- 模式之间不能合并
- 需要知道命中的是哪个 pattern
import re
patterns = [
r"^00\d+",
r"^60\d+",
r"ST",
]
compiled_list = [re.compile(p) for p in patterns]
for pat in compiled_list:
if pat.search(text):
print("命中:", pat.pattern)
⏱ 性能对比(重要)
| 模式数量 | 合并编译 | 分别编译 |
|---|---|---|
| 3 个 | 差不多 | 差不多 |
| 20 个 | 快 | 较慢 |
| 100 个 | 明显更快(1 次 regex engine) | 100 次匹配循环 |
| 500 个 | 仍很快 | 性能明显下降 |
如果你的项目里:
- 大量文本需要过滤
- 模式数量多(>20)
- 高频调用(循环、异步、爬虫)
👉 推荐使用合并编译(方法 1)
放在 utils/regex.py 中:
import re
from functools import lru_cache
@lru_cache(maxsize=None)
def compile_patterns(patterns: tuple[str, ...]) -> re.Pattern:
"""把 list 编译为一个超大正则(带缓存)"""
combined = "|".join(map(re.escape, patterns))
return re.compile(combined)
使用:
patterns = ("持续活跃", "跌势扩大", "开盘走强")
TITLE_EXCLUDE = compile_patterns(patterns)
if TITLE_EXCLUDE.search(title):
...
优点:
- list 变成 tuple -> 可缓存
- 多次调用不会重复编译
- 项目级性能提升显著
你的原代码:
title_exclude_list = [...很多关键词...]
title_exclude_string = "|".join(title_exclude_list)
title_exclude = re.compile(title_exclude_string)
最佳优化:
from utils.regex import compile_patterns
TITLE_EXCLUDE = compile_patterns(tuple(title_exclude_list))
if TITLE_EXCLUDE.search(title):
return False
- 自动 escape
- 性能更好
- 代码更优雅
- patterns 可缓存
你可能想做的是:
把一个字符串列表(多个正则模式)一次性编译成正则对象,并统一匹配。
Python 的 re.compile() 只能接受 单个字符串,不能直接对 list 编译。
但有两种最佳实践方式:
- 把多个模式合成一个正则再 compile(推荐)
- 对每个模式分别 compile 成多个正则对象
下面分别讲述最佳实践。
适用于:
- 多个关键词任意一个匹配即可
- 大量重复匹配
- 性能需求高
- 项目中常用于过滤标题、关键词触发、黑名单匹配
比如:
patterns = [
"持续活跃",
"持续下挫",
"反复走强",
]
🔥 一次性编译:
import re
def compile_pattern_list(patterns: list[str]) -> re.Pattern:
combined = "|".join(map(re.escape, patterns))
return re.compile(combined)
使用:
TITLE_EXCLUDE = compile_pattern_list(patterns)
if TITLE_EXCLUDE.search(title):
print("命中排除项")
- 仅编译一次 -> 性能高
- 正则对象可复用
- 使用简单 (pattern.search())
- 支持大量模式(几十、几百都没问题)
- 防止特殊字符冲突(因为使用了 re.escape)
如果要写复杂模式,不需要 escape,可以改成:
combined = "|".join(patterns)
适用于:
- 每个模式处理逻辑不一样
- 模式之间不能合并
- 需要知道命中的是哪个 pattern
import re
patterns = [
r"^00\d+",
r"^60\d+",
r"ST",
]
compiled_list = [re.compile(p) for p in patterns]
for pat in compiled_list:
if pat.search(text):
print("命中:", pat.pattern)
| 模式数量 | 合并编译 | 分别编译 |
|---|---|---|
| 3 个 | 差不多 | 差不多 |
| 20 个 | 快 | 较慢 |
| 100 个 | 明显更快(1 次 regex engine) | 100 次匹配循环 |
| 500 个 | 仍很快 | 性能明显下降 |
如果你的项目里:
- 大量文本需要过滤
- 模式数量多(>20)
- 高频调用(循环、异步、爬虫)
👉 推荐使用合并编译(方法 1)
放在 utils/regex.py 中:
import re
from functools import lru_cache
@lru_cache(maxsize=None)
def compile_patterns(patterns: tuple[str, ...]) -> re.Pattern:
"""把 list 编译为一个超大正则(带缓存)"""
combined = "|".join(map(re.escape, patterns))
return re.compile(combined)
使用:
patterns = ("持续活跃", "跌势扩大", "开盘走强")
TITLE_EXCLUDE = compile_patterns(patterns)
if TITLE_EXCLUDE.search(title):
...
优点:
- list 变成 tuple -> 可缓存
- 多次调用不会重复编译
- 项目级性能提升显著
你的原代码:
title_exclude_list = [...很多关键词...]
title_exclude_string = "|".join(title_exclude_list)
title_exclude = re.compile(title_exclude_string)
最佳优化:
from utils.regex import compile_patterns
TITLE_EXCLUDE = compile_patterns(tuple(title_exclude_list))
if TITLE_EXCLUDE.search(title):
return False
- 自动 escape
- 性能更好
- 代码更优雅
- patterns 可缓存