Skip to content

07-正则表达式

Python 3.11+


为什么需要正则表达式?

问题场景

你需要从日志中提取所有 IP 地址,或验证用户输入的邮箱、手机号格式:

python
# ❌ 手写字符串判断:脆弱且难以维护
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 元字符

实际场景

在文本搜索、数据验证、日志分析等场景中,需要使用元字符构建灵活的匹配模式。理解元字符是编写正则表达式的基础。

问题:正则表达式有哪些基本元字符?它们各自匹配什么内容?

python
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)                                            │
└─────────────────────────────────────────────────────────────┘

最简示例对比

python
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
?01✅ 能ac, abc(不含abbc
{3}33❌ 不能aaa(恰好3个)
{2,4}24❌ 不能aa, aaa, aaaa
{2,}2无限❌ 不能aa, aaa, aaaa...
  • * = 随便多少(包括0)
  • + = 至少一个
  • ? = 要么有要么没(最多一个)

1.2 字符类

实际场景

匹配数字、字母、空白字符等特定类型字符时,可以使用预定义字符类简化表达式。自定义字符类可以定义更精确的匹配范围。

问题:\d、\w、\s 分别匹配什么?如何自定义字符集合?

python
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 标签提取内容、解析多行文本时,需要选择贪婪或非贪婪模式。贪婪模式可能匹配过多内容,非贪婪模式更精确。

问题:贪婪量词和非贪婪量词有什么区别?何时应该使用非贪婪模式?

┌─────────────────────────────────────────────────────────────┐
│  .*  最常用的组合                                             │
│                                                              │
│  .*   → 贪婪:匹配尽可能多的字符(到最后一个位置)             │
│  .*?  → 非贪婪:匹配尽可能少的字符(到最近位置)               │
└─────────────────────────────────────────────────────────────┘
python
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 分组

实际场景

从文本中提取多个部分、解析结构化数据时,分组可以捕获匹配内容。命名分组使代码更清晰,非捕获分组用于匹配但不提取。

问题:如何使用分组提取日期的年、月、日?命名分组有什么优势?

python
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())

非捕获分组:

python
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?

python
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 预编译有什么优势?何时应该预编译正则表达式?

python
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 标志位分别有什么作用?如何组合多个标志位?

python
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.AASCII 模式\w 只匹配 ASCII 字符

第三部分:实际应用

3.1 验证邮箱

实际场景

用户注册、表单验证时需要验证邮箱地址格式是否正确。使用正则表达式可以快速判断邮箱格式是否合法。

问题:如何编写一个健壮的邮箱验证正则表达式?

python
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 验证手机号

实际场景

验证中国大陆手机号格式,用于用户身份验证、短信发送前的格式检查。

问题:中国大陆手机号有哪些号段?如何编写手机号验证正则?

python
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 链接?

python
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 标签、多余空白和特殊字符?

python
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 而非 matchmatch 只从开头匹配,不要求匹配到结尾re.fullmatch(r'\d+', '123abc') → None

实际应用示例

python
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
# ❌ 不用原始字符串,反斜杠被 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 不等于 fullmatchre.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/XMLBeautifulSoup / 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),即灾难性回溯                       │
│                                                              │
└──────────────────────────────────────────────────────────────┘
python
# 验证: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")
# 两者性能接近(缓存避免了重复编译)
python
# 验证:灾难性回溯示例
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'
python
# 验证:正则字节码
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)全字符串匹配
python
# 验证: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 → 避免灾难性回溯    │
│                                                              │
└──────────────────────────────────────────────────────────────┘