07-正则表达式
Python 3.11+
为什么需要正则表达式?
问题场景
你需要从日志中提取所有 IP 地址,或验证用户输入的邮箱、手机号格式:
# ❌ 手写字符串判断:脆弱且难以维护
def validate_email(email: str) -> bool:
if "@" not in email:
return False
parts = email.split("@")
if len(parts) != 2 or "." not in parts[1]:
return False
return True # 无法处理 "a@b." "a@.b" 等边界情况
# ✅ 用正则表达式:简洁,覆盖边界情况
import re
def validate_email(email: str) -> bool:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))正则表达式是一种文本模式描述语言,Python 通过 re 模块提供完整支持。
概念铺垫
Python re 模块的正则引擎是一个**回溯 NFA(非确定有限自动机)**实现,由 Modules/_sre.c(约 1000 行 C 代码)和 Lib/sre_compile.py 组成。理解其内部机制是写出高性能正则表达式的关键。
┌──────────────────────────────────────────────────────────────┐
│ re 模块正则引擎架构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 正则表达式 → 编译 → 模式字节码 → 匹配引擎 │
│ │
│ re.compile(r'\d{4}-\d{2}-\d{2}') │
│ │ │
│ ├→ sre_compile.py(Python) │
│ │ · 解析正则表达式语法 │
│ │ · 生成模式字节码(pattern codes) │
│ │ · 优化:字符集折叠、重复展开 │
│ │ │
│ └→ _sre.c(C 扩展) │
│ · 执行模式字节码 │
│ · 回溯式 NFA 匹配 │
│ · 捕获组管理 │
│ │
│ 内置缓存机制: │
│ · re.match(pattern, text) 会缓存编译结果 │
│ · 缓存大小:_MAXCACHE = 512 个模式 │
│ · 缓存键:pattern type + 实际 pattern + flags │
│ · re.compile() 返回的 Pattern 对象不受缓存限制 │
│ │
│ 回溯 (backtracking) 原理: │
│ · 正则引擎逐个尝试所有可能的匹配路径 │
│ · 遇到歧义(如 .* 后的模式不匹配)时回溯 │
│ · 最坏情况:O(2^n) 指数时间 — 即"灾难性回溯" │
│ │
└──────────────────────────────────────────────────────────────┘L1 理解层:会用
第一部分:正则表达式语法
1.1 元字符
实际场景
在文本搜索、数据验证、日志分析等场景中,需要使用元字符构建灵活的匹配模式。理解元字符是编写正则表达式的基础。
问题:正则表达式有哪些基本元字符?它们各自匹配什么内容?
import re
print(re.search('a.c', 'abc'))
print(re.search('^Hello', 'Hello World'))
print(re.search('world$', 'Hello world'))
print(re.findall('ab*c', ['ac', 'abc', 'abbc']))
print(re.findall('ab+c', ['ac', 'abc', 'abbc']))基本元字符:
| 元字符 | 说明 | 示例 |
|---|---|---|
. | 匹配任意字符(除换行) | a.c 匹配 "abc"、"a1c" |
^ | 匹配字符串开头 | ^Hello 匹配开头的 "Hello" |
$ | 匹配字符串结尾 | world$ 匹配结尾的 "world" |
* | 匹配 0 次或多次 | ab*c 匹配 "ac"、"abc"、"abbc" |
+ | 匹配 1 次或多次 | ab+c 匹配 "abc"、"abbc" |
? | 匹配 0 次或 1 次 | ab?c 匹配 "ac"、"abc" |
{n} | 匹配 n 次 | a{3} 匹配 "aaa" |
{n,m} | 匹配 n 到 m 次 | a{2,4} 匹配 "aa"、"aaa"、"aaaa" |
{n,} | 匹配至少 n 次 | a{2,} 匹配 "aa"、"aaa"... |
[] | 字符集合 | [abc] 匹配 "a"、"b"、"c" |
| ` | ` | 或运算 |
\ | 转义字符 | \. 匹配 "." 字符 |
量词记忆技巧
量词最容易混淆,以下方法帮你轻松记忆:
量词速记图:
┌─────────────────────────────────────────────────────────────┐
│ 量词三大符号:* + ? │
│ │
│ 符号联想记忆: │
│ ──────────────────────────────────────── │
│ * 星星 → 星星可以有很多个 → 0次或多次(可有可无,可多) │
│ + 加号 → 必须加一个 → 1次或多次(至少要有1个) │
│ ? 问号 → 有疑问?要不要? → 0次或1次(要么有,要么没) │
│ │
│ 最小次数口诀: │
│ ──────────────────────────────────────── │
│ * 最小是 0 → 可以不出现 │
│ + 最小是 1 → 必须出现 │
│ ? 最小是 0 或 1 → 有或没有 │
│ │
│ 生活比喻: │
│ ──────────────────────────────────────── │
│ * = 购物券 → 可以不用,也可以用很多张 │
│ + = 门票 → 至少买一张,可以买多张 │
│ ? = 选配 → 可以不选,最多选一个 │
│ │
│ 精确次数 {n}: │
│ ──────────────────────────────────────── │
│ {3} → 恰好3次(必须3个) │
│ {2,4} → 2到4次(最少2,最多4) │
│ {2,} → 至少2次(2个以上) │
│ │
│ ⚠️ 关键:记住"最小次数" │
│ * → 最小0 │
│ + → 最小1 │
│ ? → 最小0(最多1) │
└─────────────────────────────────────────────────────────────┘最简示例对比
import re
# * : 0次或多次 → "ac" 也能匹配(b可以没有)
print(re.findall('ab*c', ['ac', 'abc', 'abbc']))
# ['ac', 'abc', 'abbc'] ← 全部匹配
# + : 1次或多次 → "ac" 不匹配(b至少要1个)
print(re.findall('ab+c', ['ac', 'abc', 'abbc']))
# ['abc', 'abbc'] ← "ac" 不匹配
# ? : 0次或1次 → 只能有一个b或没有
print(re.findall('ab?c', ['ac', 'abc', 'abbc']))
# ['ac', 'abc'] ← "abbc" 不匹配(b超过1个)量词速查:
| 量词 | 最小次数 | 最大次数 | 能否为空 | 举例匹配 |
|---|---|---|---|---|
* | 0 | 无限 | ✅ 能 | ac, abc, abbc |
+ | 1 | 无限 | ❌ 不能 | abc, abbc(不含ac) |
? | 0 | 1 | ✅ 能 | ac, abc(不含abbc) |
{3} | 3 | 3 | ❌ 不能 | aaa(恰好3个) |
{2,4} | 2 | 4 | ❌ 不能 | aa, aaa, aaaa |
{2,} | 2 | 无限 | ❌ 不能 | aa, aaa, aaaa... |
*= 随便多少(包括0)+= 至少一个?= 要么有要么没(最多一个)
1.2 字符类
实际场景
匹配数字、字母、空白字符等特定类型字符时,可以使用预定义字符类简化表达式。自定义字符类可以定义更精确的匹配范围。
问题:\d、\w、\s 分别匹配什么?如何自定义字符集合?
import re
print(re.findall('[abc]', 'abcdef'))
print(re.findall('[a-z]', 'ABCabc'))
print(re.findall('[A-Za-z]', 'ABC123abc'))
print(re.findall('[0-9a-fA-F]', '1A2B3C'))
print(re.findall('[^abc]', 'abcdef'))预定义字符类:
| 字符类 | 说明 | 等价形式 |
|---|---|---|
\d | 数字 | [0-9] |
\D | 非数字 | [^0-9] |
\w | 单词字符(字母、数字、下划线) | [a-zA-Z0-9_] |
\W | 非单词字符 | [^a-zA-Z0-9_] |
\s | 空白字符(空格、制表、换行) | [ \t\n\r\f\v] |
\S | 非空白字符 | [^ \t\n\r\f\v] |
1.3 量词
实际场景
从 HTML 标签提取内容、解析多行文本时,需要选择贪婪或非贪婪模式。贪婪模式可能匹配过多内容,非贪婪模式更精确。
问题:贪婪量词和非贪婪量词有什么区别?何时应该使用非贪婪模式?
┌─────────────────────────────────────────────────────────────┐
│ .* 最常用的组合 │
│ │
│ .* → 贪婪:匹配尽可能多的字符(到最后一个位置) │
│ .*? → 非贪婪:匹配尽可能少的字符(到最近位置) │
└─────────────────────────────────────────────────────────────┘import re
text = "<div>内容</div><div>更多</div>"
# 贪婪模式 .*
print(re.findall('<div>.*</div>', text))
# ['<div>内容</div><div>更多</div>'] ← 匹配到最远的 </div>
# 非贪婪模式 .*?
print(re.findall('<div>.*?</div>', text))
# ['<div>内容</div>', '<div>更多</div>'] ← 匹配到最近的 </div>贪婪与非贪婪对比:
| 贪婪 | 非贪婪 | 说明 |
|---|---|---|
* | *? | 0 次或多次 |
+ | +? | 1 次或多次 |
? | ?? | 0 次或 1 次 |
{n,m} | {n,m}? | n 到 m 次 |
{n,} | {n,}? | 至少 n 次 |
1.4 分组
实际场景
从文本中提取多个部分、解析结构化数据时,分组可以捕获匹配内容。命名分组使代码更清晰,非捕获分组用于匹配但不提取。
问题:如何使用分组提取日期的年、月、日?命名分组有什么优势?
import re
text: str = '2024-03-15'
match: re.Match[str] | None = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)
if match:
print(match.group(0))
print(match.group(1))
print(match.group(2))
print(match.group(3))
print(match.groups())
text: str = '张三: 25岁'
match: re.Match[str] | None = re.search(r'(?P<name>\w+):\s*(?P<age>\d+)岁', text)
if match:
print(match.group('name'))
print(match.group('age'))
print(match.groupdict())非捕获分组:
import re
text: str = 'apple123 banana456'
matches: list[str] = re.findall(r'(?:apple|banana)(\d+)', text)
print(matches)第二部分:re 模块函数
2.1 常用函数
实际场景
根据不同的需求选择合适的 re 函数:查找第一个匹配、查找所有匹配、替换文本、分割字符串等。
问题:search 和 match 有什么区别?何时使用 findall 或 finditer?
import re
text: str = 'Python is great, Python is powerful'
match: re.Match[str] | None = re.search('Python', text)
print(match.group())
match: re.Match[str] | None = re.match('Python', text)
print(match.group())
match: re.Match[str] | None = re.match('is', text)
print(match)
matches: list[str] = re.findall('Python', text)
print(matches)
for match in re.finditer('Python', text):
print(match.start(), match.end())
new_text: str = re.sub('Python', 'Java', text)
print(new_text)
parts: list[str] = re.split(',\s*', text)
print(parts)函数列表:
| 函数 | 说明 | 返回值 |
|---|---|---|
search | 搜索第一个匹配 | Match 对象或 None |
match | 从开头匹配 | Match 对象或 None |
fullmatch | 完整匹配整个字符串 | Match 对象或 None |
findall | 查找所有匹配 | 列表 |
finditer | 查找所有匹配(迭代器) | Match 对象迭代器 |
sub | 替换匹配内容 | 新字符串 |
subn | 替换并返回次数 | (新字符串, 替换次数) |
split | 分割字符串 | 列表 |
compile | 编译正则表达式 | Pattern 对象 |
2.2 compile 预编译
实际场景
当同一个正则表达式需要多次使用时(如批量处理文件、验证大量数据),预编译可以提高效率并增强代码可读性。
问题:compile 预编译有什么优势?何时应该预编译正则表达式?
import re
pattern: re.Pattern[str] = re.compile(r'\d{4}-\d{2}-\d{2}')
text1: str = '日期:2024-03-15'
text2: str = '日期:2025-01-20'
match1: re.Match[str] | None = pattern.search(text1)
match2: re.Match[str] | None = pattern.search(text2)
print(match1.group())
print(match2.group())2.3 标志位
实际场景
匹配时需要忽略大小写、处理多行文本、让 . 匹配换行符等特殊情况,标志位可以修改正则表达式的匹配行为。
问题:re.I、re.M、re.S 标志位分别有什么作用?如何组合多个标志位?
import re
print(re.search('python', 'PYTHON', re.I).group())
text: str = '''第一行
第二行
第三行'''
matches: list[str] = re.findall(r'^第', text, re.M)
print(matches)
text: str = 'Hello\nWorld'
print(re.search('Hello.*World', text, re.S).group())
print(re.search('python', 'PYTHON\nPython', re.I | re.M))常用标志位:
| 标志 | 说明 | 示例 |
|---|---|---|
re.I | 忽略大小写 | re.search('python', 'PYTHON', re.I) |
re.M | 多行模式 | ^ 和 $ 匹配每行开头结尾 |
re.S | 单行模式 | . 匹配包括换行符 |
re.X | 扩展模式 | 允许空格和注释 |
re.A | ASCII 模式 | \w 只匹配 ASCII 字符 |
第三部分:实际应用
3.1 验证邮箱
实际场景
用户注册、表单验证时需要验证邮箱地址格式是否正确。使用正则表达式可以快速判断邮箱格式是否合法。
问题:如何编写一个健壮的邮箱验证正则表达式?
import re
def validate_email(email: str) -> bool:
pattern: str = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
emails: list[str] = [
'user@example.com',
'user.name@example.co.uk',
'user+tag@example.org',
'invalid-email',
'@example.com',
'user@'
]
for email in emails:
result: bool = validate_email(email)
print(f'{email}: {result}')3.2 验证手机号
实际场景
验证中国大陆手机号格式,用于用户身份验证、短信发送前的格式检查。
问题:中国大陆手机号有哪些号段?如何编写手机号验证正则?
import re
def validate_phone(phone: str) -> bool:
pattern: str = r'^1[3-9]\d{9}$'
return bool(re.match(pattern, phone))
phones: list[str] = ['13812345678', '15912345678', '12345678901', '1381234567']
for phone in phones:
result: bool = validate_phone(phone)
print(f'{phone}: {result}')3.3 提取 URL
实际场景
从网页内容、聊天记录、文档中提取所有 URL 链接,用于数据分析、内容审核等场景。
问题:如何从文本中提取所有 http 和 https 链接?
import re
def extract_urls(text: str) -> list[str]:
pattern: str = r'https?://[^\s<>"{}|\\^`\[\]]+'
return re.findall(pattern, text)
text: str = '''
访问 https://www.python.org 获取更多信息
文档地址:https://docs.python.org/3/
API 地址:http://api.example.com/v1
'''
urls: list[str] = extract_urls(text)
print(urls)3.4 文本清洗
实际场景
从网页抓取的文本包含 HTML 标签、多余空白、特殊字符,需要清洗后才能用于后续处理或分析。
问题:如何使用正则表达式清洗文本中的 HTML 标签、多余空白和特殊字符?
import re
def clean_text(text: str) -> str:
text = re.sub(r'<[^>]+>', '', text)
text = re.sub(r'\s+', ' ', text)
text = re.sub(r'[^\w\s\u4e00-\u9fff.,!?;:]', '', text)
return text.strip()
html_text: str = '''
<div class="content">
<p>这是一段<strong>重要</strong>文本。</p>
<p>包含多余空白和特殊字符!@#$</p>
</div>
'''
cleaned: str = clean_text(html_text)
print(cleaned)关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
re.sub(r'<[^>]+>', '', text) | 删除所有 HTML 标签 | [^>]+ 匹配标签内的任意字符(除 >),贪婪但不跨标签,避免误删 > 后的内容 |
re.sub(r'\s+', ' ', text) | 将连续空白替换为单个空格 | \s 包含空格、制表符、换行符,\s+ 一次消除所有连续空白,无需多次替换 |
r'[^\w\s\u4e00-\u9fff.,!?;:]' | 保留字母、空白、中文和常用标点 | 字符类取反([^...]):保留白名单字符;\u4e00-\u9fff 覆盖基本汉字 Unicode 范围 |
text.strip() | 去除首尾空白 | 三步清洗后首尾可能残留空格,strip() 作最终清理 |
L2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
始终用原始字符串 r"..." 写正则 | 避免 \\d 写成 \d 的反斜杠歧义 | r'\d{4}-\d{2}-\d{2}' |
重复使用的正则先 compile() | 编译一次,多次匹配效率更高 | pattern = re.compile(r'\d+') |
有分组时用 findall 注意返回类型 | 无分组返回字符串列表,有分组返回元组列表 | re.findall(r'(\d+)', s) → ['1','2'] |
提取多个字段用命名分组 (?P<name>...) | 比位置索引可读性更高,不怕顺序变动 | match.group('year') |
验证完整字符串用 fullmatch 而非 match | match 只从开头匹配,不要求匹配到结尾 | re.fullmatch(r'\d+', '123abc') → None |
实际应用示例
import re
# 从日志中批量提取 IP 地址
LOG = """
2024-03-15 192.168.1.100 GET /api/users
2024-03-15 10.0.0.25 POST /api/login
"""
IP_PATTERN = re.compile(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b')
ips = IP_PATTERN.findall(LOG)
print(ips) # ['192.168.1.100', '10.0.0.25']
# 命名分组提取日期字段
DATE_PATTERN = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})')
m = DATE_PATTERN.search("今天是 2024-03-15")
if m:
print(m.group('year'), m.group('month'), m.group('day'))反模式:不要这样做
# ❌ 不用原始字符串,反斜杠被 Python 先解释
import re
re.search('\d+', '123') # \d 恰好有效,但 '\n' '\t' 会被转义
re.search('\bword\b', 'word') # \b 会被解释为退格符!
# ✅ 始终加 r
re.search(r'\d+', '123')
re.search(r'\bword\b', 'word')
# ❌ 每次循环都重新 compile
for line in lines:
re.compile(r'\d+').findall(line) # 每次都编译,浪费
# ✅ 在循环外 compile
pattern = re.compile(r'\d+')
for line in lines:
pattern.findall(line)
# ❌ 用正则解析 HTML(不适合嵌套结构)
re.findall(r'<div>(.*?)</div>', html) # 遇到嵌套 <div> 会出错
# ✅ 用 BeautifulSoup 解析 HTML
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')常见陷阱
| 陷阱 | 现象 | 解决方案 |
|---|---|---|
| 忘用原始字符串 | '\bword\b' 匹配退格+字符 | 改用 r'\bword\b' |
match 不等于 fullmatch | re.match(r'\d+', '123abc') 匹配成功 | 全字符串验证用 re.fullmatch 或加 ^...$ |
findall + 分组返回元组 | re.findall(r'(\d+)-(\d+)', s) 返回 [('1','2')] | 知道有分组就用分组提取,或用 finditer |
. 不匹配换行 | 跨行内容匹配失败 | 加 re.S 标志或用 [\s\S]*? |
| 贪婪模式过匹配 | <.*> 匹配从第一个 < 到最后一个 > | 改用 <.*?> 非贪婪,或用 [^>]* |
忘记转义 . | re.match('1.1.1.1', '1X1Y1Z1') 匹配成功 | 匹配字面点号用 r'1\.1\.1\.1' |
适用场景
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| 格式验证(邮箱/手机号) | re.fullmatch() | 全字符串必须符合模式 |
| 从文本提取字段 | re.findall() / 命名分组 | 有分组用命名分组更清晰 |
| 文本替换 | re.sub() | 支持回调函数进行复杂替换 |
| 按模式分割字符串 | re.split() | 比 str.split() 更灵活 |
| 批量文件处理 | re.compile() + 循环 | 先编译,循环内直接调用 |
| 解析 HTML/XML | BeautifulSoup / lxml | 正则不适合嵌套结构 |
L3 专家层:深入
Python 如何实现
re 模块的匹配引擎由 C 扩展 _sre 实现。编译过程将正则表达式转为 SRE(Simple Regular Expression)字节码,引擎逐指令执行回溯匹配:
┌──────────────────────────────────────────────────────────────┐
│ re 模块编译与执行流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. 编译阶段(sre_compile.py): │
│ '\\d{4}-\\d{2}-\\d{2}' │
│ ↓ 解析抽象语法树 │
│ Opcodes: IN, MAX_REPEAT, LITERAL, ... │
│ ↓ 生成字节码 │
│ b'...pattern codes...' │
│ │
│ 2. 匹配阶段(_sre.c,回溯 NFA): │
│ · 扫描文本,在匹配位置执行字节码 │
│ · 遇到量词时记录回溯点 │
│ · 失败时回溯到最近保存点重试 │
│ · 捕获组通过 MARK 指令标记位置 │
│ │
│ 内建缓存(re._compile): │
│ · 全局字典 _cache: Dict[(type, pattern, flags), Pattern] │
│ · 容量:_MAXCACHE = 512 │
│ · 溢出策略:字典满时全部清空(非 LRU) │
│ · re.compile() 显式编译不受缓存限制 │
│ │
│ 回溯 vs DFA(确定性有限自动机): │
│ · Python 用回溯 NFA:支持反向引用、捕获组、非贪婪 │
│ · DFA(如 RE2):O(n) 线性时间,不支持反向引用 │
│ · 回溯代价:最坏 O(2^n),即灾难性回溯 │
│ │
└──────────────────────────────────────────────────────────────┘# 验证:re 模块的缓存机制
import re
# 查看缓存大小
print(f"缓存容量: {re._MAXCACHE}") # 512
# 相同的模式会命中缓存(不需要重新编译)
import timeit
pattern = r'\d{4}-\d{2}-\d{2}'
text = "日期: 2024-03-15"
# 首次调用:需要编译
# 后续调用:命中缓存
# 显式 compile 和隐式缓存对比
pat = re.compile(pattern)
t_compiled = timeit.timeit(lambda: pat.search(text), number=100000)
t_cached = timeit.timeit(lambda: re.search(pattern, text), number=100000)
print(f"compile + search: {t_compiled:.4f}s / 100K")
print(f"re.search (缓存): {t_cached:.4f}s / 100K")
# 两者性能接近(缓存避免了重复编译)# 验证:灾难性回溯示例
import re, time
# 危险模式:(a+)+b 对 "aaaaaaaaac" 需要指数时间
dangerous_pattern = r'(a+)+b'
# 测试不同长度的输入
for n in [10, 15, 20, 25]:
text = 'a' * n + 'c'
start = time.perf_counter()
try:
re.match(dangerous_pattern, text)
except RecursionError:
pass
elapsed = time.perf_counter() - start
print(f"n={n}: {elapsed:.4f}s")
# 输出:时间随 n 指数增长
# n=10: 0.0001s, n=15: 0.001s, n=20: 0.03s, n=25: 1s+
# ✅ 安全替代:(a+)+b → a+b(消除嵌套量词)
safe_pattern = r'a+b'# 验证:正则字节码
import re, sre_compile
# 查看编译后的字节码
pattern = re.compile(r'\d{4}-\d{2}-\d{2}')
# pattern 对象的内部结构
print(f"模式: {pattern.pattern}")
print(f"标志: {pattern.flags}")
print(f"分组数: {pattern.groups}") # 0(无捕获组)
print(f"分组索引: {pattern.groupindex}") # {}(无命名分组)
# 验证 _sre 是 C 扩展
import _sre
print(type(_sre)) # <class 'module'>
print(hasattr(_sre, 'compile')) # True
# 验证 match 对象是 C 类型
m = pattern.search("2024-03-15")
print(type(m)) # <class 're.Match'>
print(hasattr(m, '__dict__')) # False(C 类型,slot 存储)性能考量
| 操作 | 复杂度(优化后) | 最坏情况 | 说明 |
|---|---|---|---|
re.search(pat, s) | O(n) | O(2^n) | n=文本长度,回溯最坏指数 |
re.match(pat, s) | O(n) | O(2^n) | 同 search,但只从开头匹配 |
re.findall(pat, s) | O(n) | O(2^n) | 找到所有不重叠匹配 |
re.sub(pat, repl, s) | O(n) | O(2^n) | 查找 + 替换所有匹配 |
re.compile(pat) | O(m) | O(2^m) | m=模式长度,编译一次 |
re.fullmatch(pat, s) | O(n) | O(2^n) | 全字符串匹配 |
# 验证:re.compile 一次,多次匹配的性能
import re, timeit
pattern = re.compile(r'\d+')
texts = [f"value_{i}" for i in range(1000)]
# 方案 A:每次都 re.search
def search_each_time():
return [re.search(r'\d+', t).group() for t in texts]
# 方案 B:预编译一次
def search_compiled():
return [pattern.search(t).group() for t in texts]
print(f"每次 re.search: {timeit.timeit(search_each_time, number=100):.4f}s / 100 runs")
print(f"compile一次: {timeit.timeit(search_compiled, number=100):.4f}s / 100 runs")知识关联
┌──────────────────────────────────────────────────────────────┐
│ 正则表达式知识关联图 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ re │────→│ _sre │────→│ 回溯 │ │
│ │ 模块 │ │ C 扩展 │ │ NFA │ │
│ └──────────┘ └──────────┘ └───────────┘ │
│ │ │ │ │
│ │ │ ↓ │
│ │ │ ┌───────────┐ │
│ │ │ │ 灾难性 │ │
│ │ │ │ 回溯 │ │
│ │ │ │ O(2^n) │ │
│ │ │ └───────────┘ │
│ │ │
│ ↓ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ sre_ │ │ 正则 │ │ 编译后 │ │
│ │ compile │────→│ 语法 │────→│ 字节码 │ │
│ │ .py │ │ 解析 │ │ 缓存 512 │ │
│ └──────────┘ └──────────┘ └───────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ re │ │ REmoji │ │ Emoji │ │
│ │ 标准库 │────→│ regex │────→│ \p{} │ │
│ │ 替代 │ │ 第三方 │ │ Unicode │ │
│ └──────────┘ └──────────┘ └───────────┘ │
│ │
│ 避免灾难性回溯的原则: │
│ 1. 避免嵌套量词:(a+)+ → a+ │
│ 2. 用原子组:(?>a+) 阻止回溯 │
│ 3. 用占有量词:a++ (regex 第三方库支持) │
│ 4. 长文本用非贪婪:.*? → 限制回溯距离 │
│ 5. 明确起始:以 ^ 或 \A 开头缩小搜索空间 │
│ │
│ Python re 模块局限性: │
│ · 不支持占有量词(a++)和原子组((?>a+)) │
│ · 不支持 Unicode 属性(\p{L} 等) │
│ · 第三方库 regex 支持以上特性,可作为加强版 │
│ │
└──────────────────────────────────────────────────────────────┘本章小结
┌──────────────────────────────────────────────────────────────┐
│ 正则表达式 知识要点 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 基础语法: │
│ . 任意字符(除换行) ^ 开头 $ 结尾 │
│ * 0+次 + 1+次 ? 0/1次 {n,m} n到m次 │
│ [] 字符集 | 或 () 分组 \ 转义 │
│ \d 数字 \w 单词字符 \s 空白(大写取反) │
│ │
│ 贪婪 vs 非贪婪:.* 匹配到最远 / .*? 匹配到最近 │
│ │
│ 核心函数: │
│ search(pat, s) 找第一个匹配 │
│ findall(pat, s) 找所有匹配 │
│ sub(pat, repl, s) 替换 │
│ compile(pat) 预编译,多次使用时推荐 │
│ │
│ 重要原则: │
│ 始终用 r"..." 原始字符串 │
│ 验证完整字符串用 fullmatch 或 ^...$ │
│ 解析 HTML 用 BeautifulSoup,不用正则 │
│ │
│ L3 要点: _sre 回溯 NFA → 模式缓存 512 → 避免灾难性回溯 │
│ │
└──────────────────────────────────────────────────────────────┘