> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Python 正则表达式完全指南
- URL: https://blog.vercanti.com/python-zheng-ze-biao-da-shi-wan-quan-zhi-nan/
- Published: 2026-08-28T14:34:36.000Z
- Updated: 2026-08-28T14:56:56.000Z
- Description: 相关文档：内置函数完全参考(/python-nei-zhi-han-shu-wan-quan-can-kao/) | 数据类型(/python-nei-zhi-shu-ju-lei-xing-wan-quan-can-kao/) | 装饰器与函数高级(/python-zhuang-shi-qi-yu-han-shu-gao-ji-yong-fa/) 1. 正则表达式基础语法(#%E4%B8%80%E3%80%81%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%9F%BA%E7%A1%80%E8%AF%AD
- Author: yellowdog
- Tags: Python, 基础

> 官方文档：<https://docs.python.org/3/library/re.html>  
> 适用版本：Python 3.12（2026-05-07 核实）

相关文档：[内置函数完全参考](https://blog.vercanti.com/python-nei-zhi-han-shu-wan-quan-can-kao/) | [数据类型](https://blog.vercanti.com/python-nei-zhi-shu-ju-lei-xing-wan-quan-can-kao/) | [装饰器与函数高级](https://blog.vercanti.com/python-zhuang-shi-qi-yu-han-shu-gao-ji-yong-fa/)

---

## 目录

1. [正则表达式基础语法](#%E4%B8%80%E3%80%81%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95)
2. [修饰符（Flags）](#%E4%BA%8C%E3%80%81%E4%BF%AE%E9%A5%B0%E7%AC%A6%EF%BC%88flags%EF%BC%89)
3. [re 模块所有函数](#%E4%B8%89%E3%80%81re-%E6%A8%A1%E5%9D%97%E6%89%80%E6%9C%89%E5%87%BD%E6%95%B0)
4. [Pattern 编译对象](#%E5%9B%9B%E3%80%81pattern-%E7%BC%96%E8%AF%91%E5%AF%B9%E8%B1%A1)
5. [Match 对象](#%E4%BA%94%E3%80%81match-%E5%AF%B9%E8%B1%A1)
6. [贪婪与非贪婪](#%E5%85%AD%E3%80%81%E8%B4%AA%E5%A9%AA%E4%B8%8E%E9%9D%9E%E8%B4%AA%E5%A9%AA)
7. [零宽断言](#%E4%B8%83%E3%80%81%E9%9B%B6%E5%AE%BD%E6%96%AD%E8%A8%80)
8. [分组高级用法](#%E5%85%AB%E3%80%81%E5%88%86%E7%BB%84%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95)
9. [实战案例](#%E4%B9%9D%E3%80%81%E5%AE%9E%E6%88%98%E6%A1%88%E4%BE%8B)
10. [性能优化](#%E5%8D%81%E3%80%81%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96)
11. [最佳实践](#%E5%8D%81%E4%B8%80%E3%80%81%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5)
12. [常见陷阱与注意事项](#%E5%8D%81%E4%BA%8C%E3%80%81%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1%E4%B8%8E%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)

---

## 一、正则表达式基础语法

### 1.1 普通字符

普通字符匹配其自身。字母、数字、下划线等非特殊字符直接匹配对应字符。

### 1.2 元字符总览

以下字符在正则中具有特殊含义，匹配字面量时需用反斜杠转义：

```
. ^ $ * + ? { } [ ] \ | ( )

```

### 1.3 字符类

| 语法            | 说明                                           | 示例                 |
| ------------- | -------------------------------------------- | ------------------ |
| .             | 匹配除换行符 \\n 以外的任意字符（re.DOTALL 时包括换行）          | a.c 匹配 abc、a1c     |
| \[abc\]       | 字符集，匹配括号内任意一个字符                              | \[abc\] 匹配 a、b 或 c |
| \[^abc\]      | 取反字符集，匹配括号内字符以外的任意字符                         | \[^abc\] 不匹配 a、b、c |
| \[a-z\]       | 字符范围，匹配 a 到 z 任意字符                           | \[0-9\] 匹配任意数字     |
| \[a-zA-Z0-9\] | 多范围组合                                        | 匹配字母和数字            |
| \\d           | 等价于 \[0-9\]，匹配数字（Unicode 模式下匹配所有 Unicode 数字） | \\d+ 匹配一串数字        |
| \\D           | 等价于 \[^\\d\]，匹配非数字                           |                    |
| \\w           | 等价于 \[a-zA-Z0-9\_\]，匹配单词字符                   |                    |
| \\W           | 等价于 \[^\\w\]，匹配非单词字符                         |                    |
| \\s           | 匹配空白字符：空格、制表符、换行符等                           |                    |
| \\S           | 匹配非空白字符                                      |                    |
| \\b           | 单词边界（零宽断言）                                   | \\bword\\b 精确匹配单词  |
| \\B           | 非单词边界                                        |                    |

### 1.4 量词

| 语法    | 说明               | 等价写法  |
| ----- | ---------------- | ----- |
| \*    | 匹配前一个元素 0 次或多次   | {0,}  |
| +     | 匹配前一个元素 1 次或多次   | {1,}  |
| ?     | 匹配前一个元素 0 次或 1 次 | {0,1} |
| {n}   | 精确匹配 n 次         |       |
| {n,}  | 匹配至少 n 次         |       |
| {,m}  | 匹配最多 m 次         |       |
| {n,m} | 匹配 n 到 m 次       |       |

所有量词默认贪婪，在量词后加 `?` 变为非贪婪（懒惰）模式：

| 语法     | 说明         |
| ------ | ---------- |
| \*?    | 非贪婪，尽可能少匹配 |
| +?     | 非贪婪        |
| ??     | 非贪婪        |
| {n,m}? | 非贪婪        |

### 1.5 锚点（位置断言）

| 语法  | 说明                                      |
| --- | --------------------------------------- |
| ^   | 匹配字符串开头（re.MULTILINE 时匹配每行开头）           |
| $   | 匹配字符串结尾（re.MULTILINE 时匹配每行结尾，允许结尾有 \\n） |
| \\A | 仅匹配整个字符串的开头，不受 re.MULTILINE 影响          |
| \\Z | 仅匹配整个字符串的结尾，不受 re.MULTILINE 影响          |
| \\b | 单词边界，\\w 与 \\W 之间的位置（包括字符串开头/结尾）        |
| \\B | 非单词边界                                   |

### 1.6 分组与反向引用

| 语法                | 说明                          |
| ----------------- | --------------------------- |
| (pattern)         | 捕获分组，捕获匹配内容，可用 \\1、\\2 反向引用 |
| (?:pattern)       | 非捕获分组，仅分组不捕获                |
| (?P<name>pattern) | 命名捕获分组                      |
| (?P=name)         | 命名反向引用，引用已命名的分组             |
| \\1、\\2...        | 数字反向引用，引用第 n 个捕获分组          |
| (?#comment)       | 注释，不参与匹配                    |

### 1.7 零宽断言

| 语法           | 说明                        |
| ------------ | ------------------------- |
| (?=pattern)  | 正向先行断言：当前位置右侧能匹配 pattern  |
| (?!pattern)  | 负向先行断言：当前位置右侧不能匹配 pattern |
| (?<=pattern) | 正向后行断言：当前位置左侧能匹配 pattern  |
| (?<!pattern) | 负向后行断言：当前位置左侧不能匹配 pattern |

### 1.8 条件分组

| 语法               | 说明                                |
| ---------------- | --------------------------------- |
| (?(id)yes\|no)   | 若第 id 个分组已匹配，使用 yes 模式，否则使用 no 模式 |
| (?(name)yes\|no) | 命名分组版本                            |

### 1.9 特殊转义序列

| 语法        | 说明                     |
| --------- | ---------------------- |
| \\n       | 换行符                    |
| \\t       | 制表符                    |
| \\r       | 回车符                    |
| \\f       | 换页符                    |
| \\v       | 垂直制表符                  |
| \\xhh     | 十六进制字符                 |
| \\uhhhh   | Unicode 字符（Unicode 模式） |
| \\N{name} | Unicode 字符名称           |

---

## 二、修饰符（Flags）

修饰符可以通过两种方式设置：

1. 函数参数：`re.compile(pattern, re.IGNORECASE)`
2. 内联语法：`(?i)` 放在模式内部（局部生效）

| 修饰符常量         | 简写   | 内联语法 | 说明                                       |
| ------------- | ---- | ---- | ---------------------------------------- |
| re.IGNORECASE | re.I | (?i) | 忽略大小写匹配                                  |
| re.MULTILINE  | re.M | (?m) | ^ 和 $ 匹配每行开头和结尾                          |
| re.DOTALL     | re.S | (?s) | . 匹配包括 \\n 在内的所有字符                       |
| re.VERBOSE    | re.X | (?x) | 允许写带空格和注释的可读性更高的正则                       |
| re.ASCII      | re.A | (?a) | 使 \\w、\\d、\\s 只匹配 ASCII 字符               |
| re.UNICODE    | re.U | (?u) | 使 \\w、\\d、\\s 匹配 Unicode 字符（Python 3 默认） |
| re.LOCALE     | re.L | (?L) | 使 \\w、\\b、\\s 根据当前 locale 设置（不推荐，已过时）    |
| re.NOFLAG     | —    | —    | 无标志，值为 0                                 |

多个修饰符可以用 `|` 组合：`re.I | re.M | re.S`

### 修饰符详细说明

#### re.IGNORECASE（re.I）

```python
import re

# 大小写不敏感匹配
pattern = re.compile(r'hello', re.I)
print(pattern.findall('Hello HELLO hello'))  # ['Hello', 'HELLO', 'hello']

# 内联写法，只在部分模式中生效
result = re.findall(r'(?i)hello', 'Hello HELLO hello')
print(result)  # ['Hello', 'HELLO', 'hello']

```

#### re.MULTILINE（re.M）

```python
import re

text = "first line\nsecond line\nthird line"

# 不加 MULTILINE，^ 只匹配字符串开头
print(re.findall(r'^\w+', text))         # ['first']

# 加 MULTILINE，^ 匹配每行开头
print(re.findall(r'^\w+', text, re.M))  # ['first', 'second', 'third']

# $ 同理
print(re.findall(r'\w+$', text, re.M))  # ['line', 'line', 'line']

```

#### re.DOTALL（re.S）

```python
import re

html = "<div>\n  content\n</div>"

# 不加 DOTALL，. 不匹配换行
print(re.findall(r'<div>.*</div>', html))        # []

# 加 DOTALL，. 匹配所有字符包括换行
print(re.findall(r'<div>.*</div>', html, re.S))  # ['<div>\n  content\n</div>']

```

#### re.VERBOSE（re.X）

```python
import re

# 可读性更高的正则，空格和 # 注释被忽略
email_pattern = re.compile(r"""
    [\w.+-]+        # 用户名部分（字母、数字、.、+、-）
    @               # @ 符号
    [\w-]+          # 域名主体
    (?:\.[\w-]+)*   # 子域名（可选，可多个）
    \.              # 点号
    [a-zA-Z]{2,}    # 顶级域名
""", re.VERBOSE)

print(email_pattern.findall('user@example.com test@sub.domain.org'))
# ['user@example.com', 'test@sub.domain.org']

```

#### re.ASCII（re.A）

```python
import re

text = "hello 你好 123 ١٢٣"  # ١٢٣ 是阿拉伯数字

# 默认 Unicode 模式，\d 匹配所有 Unicode 数字
print(re.findall(r'\d+', text))           # ['123', '١٢٣']

# ASCII 模式，\d 只匹配 0-9
print(re.findall(r'\d+', text, re.A))    # ['123']

# \w 同理
print(re.findall(r'\w+', text))          # ['hello', '你好', '123', '١٢٣']
print(re.findall(r'\w+', text, re.A))   # ['hello', '123']

```

---

## 三、re 模块所有函数

### 3.1 re.compile()

将正则表达式字符串编译为 Pattern 对象，供反复使用。

**函数签名：**

```python
re.compile(pattern, flags=0)

```

**参数表：**

| 参数名     | 类型          | 默认值 | 说明            |
| ------- | ----------- | --- | ------------- |
| pattern | str 或 bytes | 必填  | 正则表达式字符串      |
| flags   | int         | 0   | 修饰符，多个用 \| 组合 |

**返回值：** `re.Pattern` 对象

```python
import re

# 基本编译
pattern = re.compile(r'\d+')
print(pattern.findall('abc123def456'))  # ['123', '456']

# 带修饰符
pattern = re.compile(r'hello', re.I | re.M)

# 编译后的 pattern 可重复使用，性能更好
email_re = re.compile(r'[\w.+-]+@[\w-]+\.[\w.]+')
urls = ['user@example.com', 'bad-email', 'admin@test.org']
for url in urls:
    if email_re.match(url):
        print(f"{url} 是有效邮箱")

```

### 3.2 re.match()

从字符串的**起始位置**尝试匹配，若起始位置不匹配则返回 `None`。

**函数签名：**

```python
re.match(pattern, string, flags=0)

```

**参数表：**

| 参数名     | 类型          | 默认值 | 说明       |
| ------- | ----------- | --- | -------- |
| pattern | str         | 必填  | 正则表达式字符串 |
| string  | str 或 bytes | 必填  | 待匹配的字符串  |
| flags   | int         | 0   | 修饰符      |

**返回值：** 匹配成功返回 `Match` 对象，失败返回 `None`

```python
import re

# match 只从起始位置匹配
m = re.match(r'\d+', '123abc')
print(m.group())   # '123'

m = re.match(r'\d+', 'abc123')
print(m)           # None，因为起始位置不是数字

# 注意：match 不需要匹配整个字符串
m = re.match(r'\d+', '123abc456')
print(m.group())   # '123'，只匹配到第一段数字

# 若要匹配整个字符串，使用 re.fullmatch 或在模式末尾加 $
m = re.match(r'\d+$', '123abc')
print(m)           # None

```

### 3.3 re.fullmatch()

要求正则表达式匹配**整个字符串**。

**函数签名：**

```python
re.fullmatch(pattern, string, flags=0)

```

**参数表：**

| 参数名     | 类型          | 默认值 | 说明       |
| ------- | ----------- | --- | -------- |
| pattern | str         | 必填  | 正则表达式字符串 |
| string  | str 或 bytes | 必填  | 待匹配的字符串  |
| flags   | int         | 0   | 修饰符      |

**返回值：** 整个字符串匹配返回 `Match` 对象，否则返回 `None`

```python
import re

# fullmatch 要求整个字符串匹配
m = re.fullmatch(r'\d+', '12345')
print(m.group())    # '12345'

m = re.fullmatch(r'\d+', '123abc')
print(m)            # None，因为 'abc' 部分不匹配

# 常用于输入验证
def is_valid_phone(phone):
    return re.fullmatch(r'1[3-9]\d{9}', phone) is not None

print(is_valid_phone('13812345678'))  # True
print(is_valid_phone('138123456789')) # False（太长）
print(is_valid_phone('23812345678'))  # False（不以1开头）

```

### 3.4 re.search()

扫描整个字符串，找到**第一个**匹配的位置。

**函数签名：**

```python
re.search(pattern, string, flags=0)

```

**参数表：**

| 参数名     | 类型          | 默认值 | 说明       |
| ------- | ----------- | --- | -------- |
| pattern | str         | 必填  | 正则表达式字符串 |
| string  | str 或 bytes | 必填  | 待匹配的字符串  |
| flags   | int         | 0   | 修饰符      |

**返回值：** 找到返回 `Match` 对象，未找到返回 `None`

```python
import re

# search 扫描整个字符串
m = re.search(r'\d+', 'abc123def456')
print(m.group())    # '123'，返回第一个匹配

m = re.search(r'\d+', 'no digits here')
print(m)            # None

# match vs search 对比
text = 'abc123'
print(re.match(r'\d+', text))   # None（起始位置不是数字）
print(re.search(r'\d+', text))  # <re.Match object>（找到 '123'）

# 使用 search 查找首个匹配
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', 'Today is 2026-03-05')
if m:
    print(m.group('year'))   # '2026'
    print(m.group('month'))  # '03'
    print(m.group('day'))    # '05'

```

### 3.5 re.findall()

找到字符串中**所有**非重叠匹配，返回列表。

**函数签名：**

```python
re.findall(pattern, string, flags=0)

```

**参数表：**

| 参数名     | 类型          | 默认值 | 说明       |
| ------- | ----------- | --- | -------- |
| pattern | str         | 必填  | 正则表达式字符串 |
| string  | str 或 bytes | 必填  | 待匹配的字符串  |
| flags   | int         | 0   | 修饰符      |

**返回值：**

- 无捕获分组：返回匹配字符串列表
- 一个捕获分组：返回捕获内容字符串列表
- 多个捕获分组：返回元组列表，每个元组对应一次匹配的各分组内容

```python
import re

# 无分组：返回完整匹配列表
result = re.findall(r'\d+', 'abc123def456ghi789')
print(result)  # ['123', '456', '789']

# 一个分组：返回分组内容列表
result = re.findall(r'(\d+)', 'abc123def456')
print(result)  # ['123', '456']

# 多个分组：返回元组列表
result = re.findall(r'(\w+)=(\w+)', 'a=1 b=2 c=3')
print(result)  # [('a', '1'), ('b', '2'), ('c', '3')]

# 非捕获分组 (?:) 不影响返回结构
result = re.findall(r'(?:\d{3})-(\d{4})', '010-1234 021-5678')
print(result)  # ['1234', '5678']

# 空匹配
result = re.findall(r'\d*', 'a1b2')
print(result)  # ['', '1', '', '2', '']（\d* 可匹配空字符串）

```

### 3.6 re.finditer()

找到所有非重叠匹配，返回迭代器（每个元素为 `Match` 对象）。

**函数签名：**

```python
re.finditer(pattern, string, flags=0)

```

**参数表：**

| 参数名     | 类型          | 默认值 | 说明       |
| ------- | ----------- | --- | -------- |
| pattern | str         | 必填  | 正则表达式字符串 |
| string  | str 或 bytes | 必填  | 待匹配的字符串  |
| flags   | int         | 0   | 修饰符      |

**返回值：** 迭代器，每次产出一个 `Match` 对象

```python
import re

# finditer 返回迭代器，内存效率更高
text = 'abc123def456ghi789'
for m in re.finditer(r'\d+', text):
    print(f"匹配: {m.group()}, 位置: {m.start()}-{m.end()}")
# 匹配: 123, 位置: 3-6
# 匹配: 456, 位置: 9-12
# 匹配: 789, 位置: 15-18

# 比 findall 更灵活，可获取位置信息
pattern = re.compile(r'(\w+)@(\w+)\.(\w+)')
for m in pattern.finditer('user@example.com admin@test.org'):
    print(f"完整: {m.group()}, 用户: {m.group(1)}, 域名: {m.group(2)}")

```

### 3.7 re.sub()

将字符串中匹配的部分替换为指定内容。

**函数签名：**

```python
re.sub(pattern, repl, string, count=0, flags=0)

```

**参数表：**

| 参数名     | 类型             | 默认值 | 说明                                                             |
| ------- | -------------- | --- | -------------------------------------------------------------- |
| pattern | str            | 必填  | 正则表达式字符串                                                       |
| repl    | str 或 callable | 必填  | 替换内容。字符串时可用 \\1、\\g<1>、\\g<name> 引用分组；可调用对象接收 Match 对象，返回替换字符串 |
| string  | str 或 bytes    | 必填  | 原始字符串                                                          |
| count   | int            | 0   | 最多替换次数，0 表示替换全部                                                |
| flags   | int            | 0   | 修饰符                                                            |

**返回值：** 替换后的字符串

```python
import re

# 基本替换
result = re.sub(r'\d+', 'NUM', 'abc123def456')
print(result)  # 'abcNUMdefNUM'

# count 限制替换次数
result = re.sub(r'\d+', 'NUM', 'abc123def456ghi789', count=2)
print(result)  # 'abcNUMdefNUMghi789'

# 使用分组引用（\1 或 \g<1>）
result = re.sub(r'(\w+)\s(\w+)', r'\2 \1', 'hello world')
print(result)  # 'world hello'

# 使用命名分组引用（\g<name>）
result = re.sub(
    r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})',
    r'\g<day>/\g<month>/\g<year>',
    '2026-03-05'
)
print(result)  # '05/03/2026'

# repl 为可调用对象
def uppercase_match(m):
    return m.group().upper()

result = re.sub(r'[a-z]+', uppercase_match, 'abc123def456')
print(result)  # 'ABC123DEF456'

# 更复杂的替换逻辑
def add_thousands_sep(m):
    num = int(m.group())
    return f"{num:,}"

result = re.sub(r'\d+', add_thousands_sep, 'price: 1234567 count: 89012')
print(result)  # 'price: 1,234,567 count: 89,012'

```

### 3.8 re.subn()

与 `re.sub()` 相同，但同时返回替换次数。

**函数签名：**

```python
re.subn(pattern, repl, string, count=0, flags=0)

```

**参数表：**

| 参数名     | 类型             | 默认值 | 说明                    |
| ------- | -------------- | --- | --------------------- |
| pattern | str            | 必填  | 正则表达式字符串              |
| repl    | str 或 callable | 必填  | 替换内容（同 re.sub 的 repl） |
| string  | str 或 bytes    | 必填  | 原始字符串                 |
| count   | int            | 0   | 最多替换次数                |
| flags   | int            | 0   | 修饰符                   |

**返回值：** `(new_string, number_of_subs_made)` 元组

```python
import re

result, count = re.subn(r'\d+', 'NUM', 'abc123def456ghi789')
print(result)  # 'abcNUMdefNUMghiNUM'
print(count)   # 3

# 判断是否发生了替换
new_text, n = re.subn(r'foo', 'bar', 'hello world')
if n == 0:
    print("没有发生替换")

```

### 3.9 re.split()

用匹配的子字符串分割字符串。

**函数签名：**

```python
re.split(pattern, string, maxsplit=0, flags=0)

```

**参数表：**

| 参数名      | 类型          | 默认值 | 说明             |
| -------- | ----------- | --- | -------------- |
| pattern  | str         | 必填  | 分隔符的正则表达式      |
| string   | str 或 bytes | 必填  | 待分割的字符串        |
| maxsplit | int         | 0   | 最大分割次数，0 表示不限制 |
| flags    | int         | 0   | 修饰符            |

**返回值：** 分割后的字符串列表

```python
import re

# 基本分割
result = re.split(r'\s+', 'one   two\tthree\nfour')
print(result)  # ['one', 'two', 'three', 'four']

# maxsplit 限制分割次数
result = re.split(r'\s+', 'one two three four', maxsplit=2)
print(result)  # ['one', 'two', 'three four']

# 分隔符含捕获分组时，分隔符也会出现在结果中
result = re.split(r'(\s+)', 'one two three')
print(result)  # ['one', ' ', 'two', ' ', 'three']

# 使用非捕获分组则不保留分隔符
result = re.split(r'(?:\s+)', 'one two three')
print(result)  # ['one', 'two', 'three']

# 按多种分隔符分割
result = re.split(r'[,;|]+', 'a,b;;c|d')
print(result)  # ['a', 'b', 'c', 'd']

# 字符串开头或结尾匹配时，结果中会有空字符串
result = re.split(r',', ',a,b,')
print(result)  # ['', 'a', 'b', '']

```

### 3.10 re.escape()

对字符串中的所有非字母数字字符进行转义，使其可作为字面量正则模式使用。

**函数签名：**

```python
re.escape(pattern)

```

**参数表：**

| 参数名     | 类型          | 默认值 | 说明       |
| ------- | ----------- | --- | -------- |
| pattern | str 或 bytes | 必填  | 需要转义的字符串 |

**返回值：** 转义后的字符串

```python
import re

# 转义特殊字符
user_input = 'price: $1.99 (special offer)'
escaped = re.escape(user_input)
print(escaped)  # 'price\:\ \$1\.99\ \(special\ offer\)'

# 安全地将用户输入用于正则
def search_literal(text, query):
    """安全地搜索字面量字符串"""
    pattern = re.escape(query)
    return re.findall(pattern, text, re.I)

text = 'The price is $1.99, not $2.99'
print(search_literal(text, '$1.99'))  # ['$1.99']

# 构建动态正则
keywords = ['c++', 'c#', '.net', 'node.js']
pattern = '|'.join(re.escape(kw) for kw in keywords)
print(re.findall(pattern, 'I like c++ and c# but not .net', re.I))
# ['c++', 'c#', '.net']

```

### 3.11 re.purge()

清除正则表达式的内部缓存。

**函数签名：**

```python
re.purge()

```

**参数表：** 无参数

**说明：** Python 会缓存最近编译的正则表达式（默认缓存 512 个）。`re.purge()` 用于清除该缓存，释放内存。通常不需要手动调用。

```python
import re

# 使用大量不同的临时正则后，手动清除缓存
for i in range(1000):
    re.findall(f'pattern_{i}', 'test string')

re.purge()  # 清除缓存，释放内存

```

---

## 四、Pattern 编译对象

`re.compile()` 返回的 `Pattern` 对象拥有与 `re` 模块顶层函数相同的方法，但不需要再传入 `pattern` 参数，且性能更好。

### Pattern 对象的属性

| 属性                 | 类型   | 说明               |
| ------------------ | ---- | ---------------- |
| pattern.pattern    | str  | 编译时使用的正则表达式字符串   |
| pattern.flags      | int  | 编译时使用的修饰符（包含默认值） |
| pattern.groups     | int  | 模式中捕获分组的数量       |
| pattern.groupindex | dict | 命名分组名称到分组编号的映射字典 |

### Pattern 对象的方法

所有方法与顶层函数签名相同，但省略第一个 `pattern` 参数。

| 方法                                             | 说明               |
| ---------------------------------------------- | ---------------- |
| pattern.match(string\[, pos\[, endpos\]\])     | 从 pos 开始匹配（默认 0） |
| pattern.fullmatch(string\[, pos\[, endpos\]\]) | 全串匹配             |
| pattern.search(string\[, pos\[, endpos\]\])    | 搜索第一个匹配          |
| pattern.findall(string\[, pos\[, endpos\]\])   | 找所有匹配            |
| pattern.finditer(string\[, pos\[, endpos\]\])  | 找所有匹配（迭代器）       |
| pattern.sub(repl, string, count=0)             | 替换               |
| pattern.subn(repl, string, count=0)            | 替换并返回次数          |
| pattern.split(string, maxsplit=0)              | 分割               |

**pos 和 endpos 参数说明：**

| 参数名    | 类型  | 默认值         | 说明                              |
| ------ | --- | ----------- | ------------------------------- |
| pos    | int | 0           | 从字符串的哪个索引位置开始匹配，相当于对字符串切片但不影响 ^ |
| endpos | int | len(string) | 匹配到字符串的哪个索引位置结束                 |

```python
import re

pattern = re.compile(r'\d+')

# 查看 Pattern 属性
print(pattern.pattern)     # '\d+'
print(pattern.flags)       # 32（默认 re.UNICODE）
print(pattern.groups)      # 0

# 带命名分组的 Pattern
p = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})')
print(p.groups)            # 2
print(p.groupindex)        # {'year': 1, 'month': 2}

# 使用 pos 和 endpos
text = 'abc123def456'
print(pattern.findall(text, 3, 9))   # ['123']（只在索引 3-9 范围内查找）
print(pattern.search(text, 6))        # 在 'def456' 中找，找到 '456'

# pos 不影响 ^ 的行为
p = re.compile(r'^\d+')
text = 'abc123'
print(p.match(text, 3))    # None（pos=3 处是 '1'，但 ^ 仍锚定字符串开头）

```

---

## 五、Match 对象

匹配成功后，`re.match()`、`re.search()`、`re.finditer()` 返回 `Match` 对象。

### Match 对象的属性

| 属性          | 类型         | 说明                |
| ----------- | ---------- | ----------------- |
| m.re        | Pattern    | 产生此匹配的 Pattern 对象 |
| m.string    | str        | 传入的原始字符串          |
| m.pos       | int        | 搜索开始位置            |
| m.endpos    | int        | 搜索结束位置            |
| m.lastindex | int 或 None | 最后一个参与匹配的捕获分组编号   |
| m.lastgroup | str 或 None | 最后一个参与匹配的命名分组名称   |

### 5.1 m.group()

**函数签名：**

```python
m.group([group1, ...])

```

**参数表：**

| 参数名         | 类型        | 默认值 | 说明                                |
| ----------- | --------- | --- | --------------------------------- |
| group1, ... | int 或 str | 0   | 分组编号或命名分组名称。0 或缺省表示整个匹配；多个参数时返回元组 |

**返回值：** 单个参数时返回字符串（分组未参与匹配时返回 `None`）；多个参数时返回元组

```python
import re

m = re.match(r'(\d{4})-(\d{2})-(\d{2})', '2026-03-05')

print(m.group())     # '2026-03-05'（整个匹配）
print(m.group(0))    # '2026-03-05'（同上）
print(m.group(1))    # '2026'（第一个分组）
print(m.group(2))    # '03'
print(m.group(3))    # '05'
print(m.group(1, 3)) # ('2026', '05')（多个参数返回元组）

# 命名分组
m = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})', '2026-03')
print(m.group('year'))   # '2026'
print(m.group('month'))  # '03'

# 未参与匹配的分组返回 None
m = re.match(r'(\d+)|([a-z]+)', '123')
print(m.group(1))  # '123'
print(m.group(2))  # None（第二个分组未参与匹配）

```

### 5.2 m.groups()

**函数签名：**

```python
m.groups(default=None)

```

**参数表：**

| 参数名     | 类型  | 默认值  | 说明           |
| ------- | --- | ---- | ------------ |
| default | any | None | 未参与匹配的分组的默认值 |

**返回值：** 包含所有捕获分组内容的元组

```python
import re

m = re.match(r'(\d{4})-(\d{2})-(\d{2})', '2026-03-05')
print(m.groups())         # ('2026', '03', '05')

# default 参数处理未匹配分组
m = re.match(r'(\d+)\.?(\d*)', '123')
print(m.groups())         # ('123', '')
print(m.groups('N/A'))    # ('123', '')（空字符串不是 None，不替换）

m = re.match(r'(\d+)|([a-z]+)', '123')
print(m.groups())         # ('123', None)
print(m.groups('N/A'))    # ('123', 'N/A')

```

### 5.3 m.groupdict()

**函数签名：**

```python
m.groupdict(default=None)

```

**参数表：**

| 参数名     | 类型  | 默认值  | 说明             |
| ------- | --- | ---- | -------------- |
| default | any | None | 未参与匹配的命名分组的默认值 |

**返回值：** 包含所有命名分组的字典，键为分组名

```python
import re

m = re.match(
    r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})',
    '2026-03-05'
)
print(m.groupdict())
# {'year': '2026', 'month': '03', 'day': '05'}

# 含未匹配命名分组时使用 default
m = re.match(r'(?P<num>\d+)|(?P<word>[a-z]+)', '123')
print(m.groupdict())           # {'num': '123', 'word': None}
print(m.groupdict('N/A'))      # {'num': '123', 'word': 'N/A'}

```

### 5.4 m.start() / m.end() / m.span()

**函数签名：**

```python
m.start([group])
m.end([group])
m.span([group])

```

**参数表（三者相同）：**

| 参数名   | 类型        | 默认值 | 说明                   |
| ----- | --------- | --- | -------------------- |
| group | int 或 str | 0   | 分组编号或命名分组名称，0 表示整个匹配 |

**返回值：**

- `start()`：匹配开始的索引
- `end()`：匹配结束的索引（不含）
- `span()`：`(start, end)` 元组

```python
import re

text = 'hello 2026-03-05 world'
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)

print(m.start())    # 6（整个匹配开始位置）
print(m.end())      # 16（整个匹配结束位置）
print(m.span())     # (6, 16)

print(m.start(1))   # 6（第一个分组开始位置）
print(m.end(1))     # 10
print(m.span(1))    # (6, 10)

# 验证：原始字符串切片
print(text[m.start():m.end()])     # '2026-03-05'
print(text[m.start(2):m.end(2)])   # '03'

# 未参与匹配的分组返回 -1
m = re.match(r'(\d+)|([a-z]+)', '123')
print(m.start(2))   # -1
print(m.end(2))     # -1
print(m.span(2))    # (-1, -1)

```

### 5.5 m.expand()

**函数签名：**

```python
m.expand(template)

```

**参数表：**

| 参数名      | 类型  | 默认值 | 说明                                  |
| -------- | --- | --- | ----------------------------------- |
| template | str | 必填  | 模板字符串，可使用 \\1、\\g<1>、\\g<name> 引用分组 |

**返回值：** 替换分组引用后的字符串

```python
import re

m = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-03-05')

print(m.expand(r'\g<day>/\g<month>/\g<year>'))  # '05/03/2026'
print(m.expand(r'\1年\2月\3日'))                 # '2026年03月05日'

```

---

## 六、贪婪与非贪婪

### 6.1 贪婪模式（默认）

量词默认是贪婪的，尽可能多地匹配字符。

```python
import re

html = '<b>bold</b> and <i>italic</i>'

# 贪婪：匹配从第一个 < 到最后一个 >
print(re.findall(r'<.+>', html))
# ['<b>bold</b> and <i>italic</i>']（全部匹配）

# 解释：.+ 尽可能多匹配，直到整个字符串都能满足模式

```

### 6.2 非贪婪模式

在量词后加 `?`，尽可能少地匹配字符。

```python
import re

html = '<b>bold</b> and <i>italic</i>'

# 非贪婪：每次找最短的匹配
print(re.findall(r'<.+?>', html))
# ['<b>', '</b>', '<i>', '</i>']

# 数量对比
print(re.findall(r'".*"', '"a" and "b"'))    # ['"a" and "b"']（贪婪）
print(re.findall(r'".*?"', '"a" and "b"'))   # ['"a"', '"b"']（非贪婪）

```

### 6.3 回溯机制

理解贪婪模式的工作原理有助于优化正则：

```python
import re

# 贪婪匹配的工作过程：
# 模式：a.*b，字符串：aXXbXXb
# 1. .* 先吃掉整个字符串 aXXbXXb
# 2. 尝试匹配末尾 b，失败
# 3. 回吐一个字符，尝试匹配...直到成功
# 最终匹配到 aXXbXXb（贪婪，找最长匹配）

text = 'aXXbXXb'
print(re.search(r'a.*b', text).group())    # 'aXXbXXb'
print(re.search(r'a.*?b', text).group())   # 'aXXb'（非贪婪，找最短）

# 固化量词（possessive quantifier）：Python 3.11+ 支持原子组 (?>...)
# 可避免不必要的回溯（见"原子组"章节）

```

---

## 七、零宽断言

零宽断言（Zero-Width Assertion）匹配的是位置而非字符，不消耗字符。

### 7.1 正向先行断言 `(?=pattern)`

当前位置右侧能匹配 pattern 时成立。

```python
import re

# 匹配后面跟着 "元" 的数字
text = '苹果5元，香蕉3元，橙子12元'
prices = re.findall(r'\d+(?=元)', text)
print(prices)  # ['5', '3', '12']

# 密码验证：必须包含数字（先行断言同时满足多个条件）
def is_strong_password(pwd):
    has_digit = bool(re.search(r'(?=.*\d)', pwd))
    has_upper = bool(re.search(r'(?=.*[A-Z])', pwd))
    has_lower = bool(re.search(r'(?=.*[a-z])', pwd))
    has_special = bool(re.search(r'(?=.*[!@#$%^&*])', pwd))
    return all([has_digit, has_upper, has_lower, has_special, len(pwd) >= 8])

# 或者用单个正则
password_pattern = re.compile(
    r'^(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*]).{8,}$'
)
print(bool(password_pattern.match('MyPass@1')))   # True
print(bool(password_pattern.match('mypassword'))) # False

```

### 7.2 负向先行断言 `(?!pattern)`

当前位置右侧不能匹配 pattern 时成立。

```python
import re

# 匹配不以 .min.js 结尾的 .js 文件
files = ['main.js', 'main.min.js', 'app.js', 'vendor.min.js']
for f in files:
    if re.search(r'\.js(?!\.min)', f):  # 注意这里逻辑需调整
        pass

# 正确写法：文件名不包含 .min 的 .js 文件
for f in files:
    if re.match(r'(?!.*\.min).*\.js$', f):
        print(f)  # main.js, app.js

# 匹配不跟在 "un" 后面的 "happy"
text = 'I am happy and unhappy'
result = re.findall(r'(?<!un)happy', text)
print(result)  # ['happy']（只匹配独立的 happy）

```

### 7.3 正向后行断言 `(?<=pattern)`

当前位置左侧能匹配 pattern 时成立。pattern 必须是固定宽度（不能用 `*`、`+`）。

```python
import re

# 匹配 $ 后面的数字
text = 'Price: $19.99, Cost: $5.00'
prices = re.findall(r'(?<=\$)\d+\.\d{2}', text)
print(prices)  # ['19.99', '5.00']

# 提取引号中的内容（正向后行断言）
text = 'name="Alice" role="admin"'
values = re.findall(r'(?<==")[^"]+', text)
print(values)  # ['Alice', 'admin']

# 提取 http/https 后的域名
urls = 'Visit http://example.com or https://test.org'
domains = re.findall(r'(?<=https?://)[a-zA-Z0-9.-]+', urls)
print(domains)  # ['example.com', 'test.org']

```

### 7.4 负向后行断言 `(?<!pattern)`

当前位置左侧不能匹配 pattern 时成立。

```python
import re

# 匹配不在小数点后的数字
text = '3 items cost $12.50 each'
nums = re.findall(r'(?<!\.\d*)(?<!\$)\d+', text)  # 这类需求更适合专门逻辑

# 更实用的例子：匹配不在 HTML 标签内的文字
# 提取非转义的引号内容
text = r'say "hello" and \"world\"'
result = re.findall(r'(?<!\\)"([^"]*?)(?<!\\)"', text)
print(result)  # ['hello']

# Python 3.11+ 后行断言支持可变宽度
# 之前版本后行断言必须固定宽度

```

### 7.5 断言组合使用

```python
import re

# 同时使用多个断言
# 匹配前有 $ 且后有 USD 的数字
text = '$100 USD and €200 EUR and $300 USD'
result = re.findall(r'(?<=\$)\d+(?= USD)', text)
print(result)  # ['100', '300']

# 提取函数调用中的参数（简化版）
code = 'func(arg1) other_func(arg2) call(arg3)'
args = re.findall(r'(?<=\()[^)]+(?=\))', code)
print(args)  # ['arg1', 'arg2', 'arg3']

```

---

## 八、分组高级用法

### 8.1 命名分组 `(?P<name>pattern)`

```python
import re

# 基本命名分组
m = re.match(
    r'(?P<last>\w+),\s*(?P<first>\w+)',
    'Smith, John'
)
print(m.group('last'))    # 'Smith'
print(m.group('first'))   # 'John'
print(m.groupdict())      # {'last': 'Smith', 'first': 'John'}

# 命名分组反向引用（在模式内部）
# 匹配 HTML 开闭标签一致的结构
html_tag = re.compile(r'<(?P<tag>[a-z]+)>.*?</(?P=tag)>', re.S)
print(html_tag.findall('<div>content</div>'))   # ['div']，注意只返回分组内容
# 改用 search 获取完整匹配
m = html_tag.search('<div>hello world</div>')
print(m.group())     # '<div>hello world</div>'
print(m.group('tag')) # 'div'

# 在 re.sub 中引用命名分组
result = re.sub(
    r'(?P<first>\w+)\s+(?P<last>\w+)',
    r'\g<last>, \g<first>',
    'John Smith'
)
print(result)  # 'Smith, John'

```

### 8.2 非捕获分组 `(?:pattern)`

```python
import re

# 捕获分组 vs 非捕获分组
text = 'color: red, colour: blue'

# 使用捕获分组
result = re.findall(r'colou?r:\s*(\w+)', text)
print(result)  # ['red', 'blue']

# 如果需要对 colou?r 分组但不捕获
result = re.findall(r'(?:colou?r):\s*(\w+)', text)
print(result)  # ['red', 'blue']（只捕获颜色值）

# 非捕获分组常用于量词应用于多字符
text = 'abcabcabc'
print(re.findall(r'(?:abc)+', text))    # ['abcabcabc']
print(re.findall(r'(abc)+', text))      # ['abc']（返回最后一次捕获的分组值）

# 性能：非捕获分组略快于捕获分组

```

### 8.3 条件分组 `(?(id)yes|no)`

根据某个分组是否参与了匹配，选择不同的匹配模式。

```python
import re

# 基本语法：(?(id)yes_pattern|no_pattern)
# 如果第 id 个分组已匹配，则使用 yes_pattern，否则使用 no_pattern

# 匹配带括号或不带括号的数字
# 有左括号时，要求有右括号
pattern = re.compile(r'(\()?\d+(?(1)\)|)')

tests = ['(123)', '456', '(789']
for t in tests:
    m = pattern.fullmatch(t)
    print(f"{t}: {'匹配' if m else '不匹配'}")
# (123): 匹配
# 456: 匹配
# (789: 不匹配（有左括号但没有右括号）

# 匹配可选的 http/https 前缀
pattern = re.compile(r'(https?://)?www\.(?(1)[\w.]+|[\w.]+\.com)')
print(bool(pattern.fullmatch('http://www.example.org')))  # True
print(bool(pattern.fullmatch('www.example.com')))          # True

```

### 8.4 原子组 `(?>pattern)`（Python 3.11+）

原子组匹配后不允许回溯，可以提升性能并避免灾难性回溯。

```python
import re
import sys

# Python 3.11+
if sys.version_info >= (3, 11):
    # 不使用原子组（可能发生灾难性回溯）
    # pattern = re.compile(r'(a+)+b')

    # 使用原子组（禁止回溯）
    pattern = re.compile(r'(?>a+)+b')

    print(bool(pattern.search('aaab')))   # True
    print(bool(pattern.search('aaac')))   # False（快速失败，无回溯）

# Python 3.10 及以前的替代方案：使用非贪婪或重写模式
# 避免嵌套量词如 (a+)+ 的写法

```

### 8.5 数字反向引用

```python
import re

# 匹配重复的单词
pattern = re.compile(r'\b(\w+)\s+\1\b', re.I)
text = 'the the quick brown fox over the the lazy dog'
result = pattern.findall(text)
print(result)  # ['the', 'the']

# 匹配 HTML 配对标签
pattern = re.compile(r'<([a-z]+)>(.*?)</\1>', re.S)
html = '<div>hello</div><span>world</span>'
for m in pattern.finditer(html):
    print(f"标签: {m.group(1)}, 内容: {m.group(2)}")
# 标签: div, 内容: hello
# 标签: span, 内容: world

# 匹配引号（单引号或双引号配对）
pattern = re.compile(r'([\'"])(.*?)\1')
text = '"hello" and \'world\' and "mixed\''
for m in pattern.finditer(text):
    print(f"引号: {m.group(1)}, 内容: {m.group(2)}")
# 引号: ", 内容: hello
# 引号: ', 内容: world

```

---

## 九、实战案例

### 案例 1：邮箱提取

```python
import re

EMAIL_PATTERN = re.compile(
    r"""
    (?P<local>
        [\w.!#$%&'*+/=?^_`{|}~-]+   # 本地部分：允许的字符
    )
    @
    (?P<domain>
        (?:[a-zA-Z0-9]               # 域名：以字母或数字开头
        (?:[a-zA-Z0-9-]{0,61}        # 中间部分
        [a-zA-Z0-9])?                # 以字母或数字结尾
        \.)+                          # 点号分隔
        [a-zA-Z]{2,}                 # 顶级域名至少2字符
    )
    """,
    re.VERBOSE
)

def extract_emails(text):
    return EMAIL_PATTERN.findall(text)

# 注意：findall 返回分组内容，需要调整
EMAIL_SIMPLE = re.compile(r'[\w.!#$%&\'*+/=?^_`{|}~-]+@[\w.-]+\.[a-zA-Z]{2,}')

def extract_emails(text):
    return EMAIL_SIMPLE.findall(text)

test_text = """
联系邮箱：user@example.com 或 admin.test+tag@sub.domain.org
无效邮箱：@invalid.com，user@，plain-text
另一个：info@company.co.uk
"""

emails = extract_emails(test_text)
for email in emails:
    print(email)
# user@example.com
# admin.test+tag@sub.domain.org
# info@company.co.uk

```

### 案例 2：手机号匹配

```python
import re

# 中国大陆手机号：1 开头，第二位 3-9，共 11 位
PHONE_PATTERN = re.compile(r'(?<!\d)1[3-9]\d{9}(?!\d)')

def extract_phones(text):
    return PHONE_PATTERN.findall(text)

def is_valid_phone(phone):
    return bool(PHONE_PATTERN.fullmatch(phone))

test_text = "联系电话：13812345678，备用：19912345678，固话：010-12345678"
print(extract_phones(test_text))  # ['13812345678', '19912345678']

# 带格式的手机号（支持 +86、空格、横线分隔）
PHONE_FORMAT = re.compile(
    r'(?:\+86\s?)?'          # 可选国家代码
    r'(?:0\d{2,3}[-\s]?)?'  # 可选区号
    r'1[3-9]\d{9}'           # 手机号主体
)

test_phones = ['13812345678', '+8613812345678', '138 1234 5678', '138-1234-5678']
for phone in test_phones:
    cleaned = re.sub(r'[\s\-+]', '', phone)
    cleaned = re.sub(r'^86', '', cleaned)
    print(f"{phone} -> {cleaned}, valid: {is_valid_phone(cleaned)}")

```

### 案例 3：IP 地址验证

```python
import re

# IPv4 地址验证
# 每段 0-255
def _build_ipv4():
    # 250-255
    p1 = r'25[0-5]'
    # 200-249
    p2 = r'2[0-4]\d'
    # 100-199
    p3 = r'1\d{2}'
    # 10-99
    p4 = r'[1-9]\d'
    # 0-9
    p5 = r'\d'
    octet = f'(?:{p1}|{p2}|{p3}|{p4}|{p5})'
    return re.compile(rf'^{octet}\.{octet}\.{octet}\.{octet}$')

IPV4_PATTERN = _build_ipv4()

def is_valid_ipv4(ip):
    return bool(IPV4_PATTERN.match(ip))

test_ips = ['192.168.1.1', '255.255.255.255', '0.0.0.0',
            '256.1.1.1', '1.2.3', '192.168.01.1']
for ip in test_ips:
    print(f"{ip}: {is_valid_ipv4(ip)}")
# 192.168.1.1: True
# 255.255.255.255: True
# 0.0.0.0: True
# 256.1.1.1: False
# 1.2.3: False
# 192.168.01.1: True（注意：01 被接受，如需拒绝需额外处理）

# 从文本中提取 IP（不验证合法性）
IP_EXTRACT = re.compile(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b')
text = "服务器 192.168.1.100 和网关 10.0.0.1 以及无效的 999.999.999.999"
print(IP_EXTRACT.findall(text))  # ['192.168.1.100', '10.0.0.1', '999.999.999.999']

```

### 案例 4：URL 提取

```python
import re

URL_PATTERN = re.compile(
    r'https?://'                          # 协议
    r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' # 域名
    r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?))'             # TLD
    r'(?::\d+)?'                          # 可选端口
    r'(?:/?|[/?]\S+)',                    # 路径
    re.IGNORECASE
)

test_text = """
访问 https://www.example.com/path?q=1&p=2 或
http://api.test.org:8080/v1/users 获取更多信息。
无效的 ftp://not-http.com 不会被匹配。
"""

urls = URL_PATTERN.findall(test_text)
for url in urls:
    print(url)
# https://www.example.com/path?q=1&p=2
# http://api.test.org:8080/v1/users

# 解析 URL 组成部分
URL_PARTS = re.compile(
    r'(?P<scheme>https?)://'
    r'(?P<host>[^/:?#]+)'
    r'(?::(?P<port>\d+))?'
    r'(?P<path>/[^?#]*)?'
    r'(?:\?(?P<query>[^#]*))?'
    r'(?:#(?P<fragment>.*))?'
)

m = URL_PARTS.match('https://api.example.com:8080/v1/users?role=admin#section1')
if m:
    print(m.groupdict())
# {'scheme': 'https', 'host': 'api.example.com', 'port': '8080',
#  'path': '/v1/users', 'query': 'role=admin', 'fragment': 'section1'}

```

### 案例 5：HTML 标签提取

```python
import re

# 提取 HTML 标签及其属性（警告：复杂 HTML 请使用 BeautifulSoup）
HTML_TAG = re.compile(r'<([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>', re.S)

def parse_attributes(attr_string):
    """解析 HTML 属性字符串"""
    attr_pattern = re.compile(r'(\w[\w-]*)(?:\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|(\S+)))?')
    attrs = {}
    for m in attr_pattern.finditer(attr_string):
        name = m.group(1)
        value = m.group(2) or m.group(3) or m.group(4) or True
        attrs[name] = value
    return attrs

html = '<div class="container" id="main"><a href="http://example.com" target="_blank">link</a></div>'

for m in HTML_TAG.finditer(html):
    tag = m.group(1)
    attrs = parse_attributes(m.group(2))
    print(f"标签: {tag}, 属性: {attrs}")
# 标签: div, 属性: {'class': 'container', 'id': 'main'}
# 标签: a, 属性: {'href': 'http://example.com', 'target': '_blank'}

# 提取特定属性
hrefs = re.findall(r'<a\s[^>]*href=["\']([^"\']+)["\']', html, re.I)
print(hrefs)  # ['http://example.com']

```

### 案例 6：中文提取

```python
import re

# 中文 Unicode 范围：\u4e00-\u9fff（基本汉字）
# 扩展：\u3400-\u4dbf（扩展A）、\u20000-\u2a6df（扩展B）等
CHINESE_PATTERN = re.compile(r'[\u4e00-\u9fff\u3400-\u4dbf]+')

def extract_chinese(text):
    return CHINESE_PATTERN.findall(text)

text = "Hello 你好 World 中文提取 Test 测试123"
print(extract_chinese(text))  # ['你好', '中文提取', '测试']

# 包含中文标点符号
CHINESE_WITH_PUNCT = re.compile(
    r'[\u4e00-\u9fff\u3400-\u4dbf'  # 汉字
    r'\u3000-\u303f'                  # CJK 标点
    r'\uff00-\uffef]+'               # 全角字符
)

# 统计中文字符数
def count_chinese_chars(text):
    return sum(len(m) for m in CHINESE_PATTERN.findall(text))

text = "这是一段中文，contains English and 数字123"
print(count_chinese_chars(text))  # 10（'这是一段中文' + '数字'）

# 移除非中文字符（只保留中文）
def keep_chinese_only(text):
    return ''.join(CHINESE_PATTERN.findall(text))

print(keep_chinese_only("Hello世界！abc中文123"))  # '世界中文'

```

### 案例 7：日期格式匹配

```python
import re
from datetime import datetime

# 匹配多种日期格式
DATE_PATTERNS = {
    'ISO': re.compile(r'\b(\d{4})-(\d{2})-(\d{2})\b'),
    'CN': re.compile(r'\b(\d{4})年(\d{1,2})月(\d{1,2})日\b'),
    'US': re.compile(r'\b(\d{1,2})/(\d{1,2})/(\d{2,4})\b'),
    'EU': re.compile(r'\b(\d{1,2})\.(\d{1,2})\.(\d{2,4})\b'),
}

def extract_dates(text):
    results = []
    for fmt, pattern in DATE_PATTERNS.items():
        for m in pattern.finditer(text):
            results.append({'format': fmt, 'raw': m.group(), 'groups': m.groups()})
    return results

text = "日期：2026-03-05，或者2026年3月5日，也可以是03/05/2026"
for date in extract_dates(text):
    print(f"格式: {date['format']}, 原始: {date['raw']}")

# 验证并解析日期
def parse_date(text):
    pattern = re.compile(
        r'(?P<year>\d{4})[年/-](?P<month>\d{1,2})[月/-](?P<day>\d{1,2})日?'
    )
    m = pattern.search(text)
    if m:
        try:
            return datetime(int(m.group('year')), int(m.group('month')), int(m.group('day')))
        except ValueError:
            return None
    return None

print(parse_date("今天是2026-03-05"))    # datetime(2026, 3, 5, 0, 0)
print(parse_date("2026年13月5日"))       # None（月份无效）

```

### 案例 8：密码强度验证

```python
import re

def check_password_strength(password):
    """
    检查密码强度
    强密码条件：
    - 长度至少 8 位
    - 包含大写字母
    - 包含小写字母
    - 包含数字
    - 包含特殊字符
    """
    checks = {
        'length': len(password) >= 8,
        'uppercase': bool(re.search(r'[A-Z]', password)),
        'lowercase': bool(re.search(r'[a-z]', password)),
        'digit': bool(re.search(r'\d', password)),
        'special': bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password)),
    }

    score = sum(checks.values())
    strength_map = {5: '强', 4: '中等', 3: '弱', 2: '很弱', 1: '极弱', 0: '不合格'}
    strength = strength_map.get(score, '不合格')

    return {'score': score, 'strength': strength, 'checks': checks}

# 单一强密码正则（使用前行断言）
STRONG_PASSWORD = re.compile(
    r'^'
    r'(?=.*[a-z])'           # 至少一个小写字母
    r'(?=.*[A-Z])'           # 至少一个大写字母
    r'(?=.*\d)'              # 至少一个数字
    r'(?=.*[!@#$%^&*])'     # 至少一个特殊字符
    r'.{8,}$'                # 至少 8 位
)

passwords = ['MyPass@1', 'weakpass', 'NoSpecial1', 'MyPassword@123']
for pwd in passwords:
    result = check_password_strength(pwd)
    valid = bool(STRONG_PASSWORD.match(pwd))
    print(f"{pwd}: {result['strength']} (强密码: {valid})")

```

### 案例 9：JSON 字段提取

```python
import re

# 注意：复杂 JSON 请使用 json 模块。以下适用于日志中嵌入的 JSON 片段。

def extract_json_string_field(json_str, field_name):
    """提取 JSON 字符串字段值"""
    pattern = re.compile(
        rf'"{re.escape(field_name)}"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"'
    )
    m = pattern.search(json_str)
    return m.group(1) if m else None

def extract_json_number_field(json_str, field_name):
    """提取 JSON 数字字段值"""
    pattern = re.compile(
        rf'"{re.escape(field_name)}"\s*:\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)'
    )
    m = pattern.search(json_str)
    return m.group(1) if m else None

json_str = '{"name": "Alice", "age": 30, "email": "alice@example.com", "score": 98.5}'

print(extract_json_string_field(json_str, 'name'))   # 'Alice'
print(extract_json_string_field(json_str, 'email'))  # 'alice@example.com'
print(extract_json_number_field(json_str, 'age'))    # '30'
print(extract_json_number_field(json_str, 'score'))  # '98.5'

# 提取所有键值对（仅字符串值）
ALL_STRING_FIELDS = re.compile(r'"(\w+)"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"')
fields = dict(ALL_STRING_FIELDS.findall(json_str))
print(fields)  # {'name': 'Alice', 'email': 'alice@example.com'}

```

### 案例 10：去除 HTML 标签

```python
import re

def strip_html_tags(html):
    """移除所有 HTML 标签，保留文本内容"""
    # 移除 script 和 style 标签及其内容
    clean = re.sub(r'<(script|style)[^>]*>.*?</\1>', '', html, flags=re.S | re.I)
    # 移除所有 HTML 标签
    clean = re.sub(r'<[^>]+>', '', clean)
    # 替换 HTML 实体
    entities = {
        '&amp;': '&', '&lt;': '<', '&gt;': '>',
        '&quot;': '"', '&apos;': "'", '&nbsp;': ' ',
    }
    for entity, char in entities.items():
        clean = clean.replace(entity, char)
    # 去除多余空白
    clean = re.sub(r'\s+', ' ', clean).strip()
    return clean

html = """
<html>
<head><title>测试页面</title>
<style>body { color: red; }</style>
<script>alert('hello');</script>
</head>
<body>
  <h1>标题</h1>
  <p>这是<b>一段</b>文字，包含<a href="#">链接</a>。</p>
  <p>HTML实体：&amp; &lt; &gt; &nbsp;空格</p>
</body>
</html>
"""

print(strip_html_tags(html))
# '测试页面 标题 这是一段文字，包含链接。 HTML实体：& < >  空格'

```

### 案例 11：爬虫中的常见提取场景

```python
import re

# 提取页面标题
def extract_title(html):
    m = re.search(r'<title[^>]*>(.*?)</title>', html, re.S | re.I)
    return m.group(1).strip() if m else None

# 提取 meta 描述
def extract_meta_description(html):
    m = re.search(
        r'<meta\s[^>]*name=["\']description["\'][^>]*content=["\']([^"\']+)["\']',
        html, re.I
    )
    if not m:
        m = re.search(
            r'<meta\s[^>]*content=["\']([^"\']+)["\'][^>]*name=["\']description["\']',
            html, re.I
        )
    return m.group(1).strip() if m else None

# 提取所有图片 src
def extract_images(html):
    return re.findall(r'<img\s[^>]*src=["\']([^"\']+)["\']', html, re.I)

# 提取所有链接
def extract_links(html):
    return re.findall(r'<a\s[^>]*href=["\']([^"\']+)["\']', html, re.I)

# 提取 JSON-LD 结构化数据
def extract_jsonld(html):
    pattern = re.compile(
        r'<script\s[^>]*type=["\']application/ld\+json["\'][^>]*>(.*?)</script>',
        re.S | re.I
    )
    return pattern.findall(html)

# 示例
html = """
<html>
<head>
  <title>商品详情 - 示例网站</title>
  <meta name="description" content="这是商品描述信息">
</head>
<body>
  <img src="/images/product.jpg" alt="商品图片">
  <img src="/images/thumb.png">
  <a href="/category/1">分类一</a>
  <a href="https://external.com">外部链接</a>
</body>
</html>
"""

print(extract_title(html))           # '商品详情 - 示例网站'
print(extract_meta_description(html)) # '这是商品描述信息'
print(extract_images(html))          # ['/images/product.jpg', '/images/thumb.png']
print(extract_links(html))           # ['/category/1', 'https://external.com']

```

### 案例 12：日志解析

```python
import re
from datetime import datetime

# Nginx 访问日志格式：
# 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326

NGINX_LOG = re.compile(
    r'(?P<ip>\d{1,3}(?:\.\d{1,3}){3})\s+'         # IP 地址
    r'(?P<ident>\S+)\s+'                             # 标识（通常为 -）
    r'(?P<user>\S+)\s+'                              # 用户名
    r'\[(?P<time>[^\]]+)\]\s+'                       # 时间
    r'"(?P<method>\w+)\s+'                           # HTTP 方法
    r'(?P<path>[^\s"]+)\s+'                          # 请求路径
    r'(?P<protocol>[^"]+)"\s+'                       # 协议
    r'(?P<status>\d{3})\s+'                          # 状态码
    r'(?P<size>\d+|-)'                               # 响应大小
)

LOG_TIME_FORMAT = '%d/%b/%Y:%H:%M:%S %z'

def parse_nginx_log(line):
    m = NGINX_LOG.match(line)
    if not m:
        return None
    data = m.groupdict()
    try:
        data['time'] = datetime.strptime(data['time'], LOG_TIME_FORMAT)
    except ValueError:
        pass
    data['status'] = int(data['status'])
    data['size'] = int(data['size']) if data['size'] != '-' else 0
    return data

# Python 应用日志格式
APP_LOG = re.compile(
    r'(?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+'
    r'(?P<timestamp>\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+'
    r'(?P<module>[\w.]+):(?P<lineno>\d+)\s+-\s+'
    r'(?P<message>.*)'
)

log_line = 'ERROR 2026-03-05 14:30:00.123 myapp.views:45 - Database connection failed'
m = APP_LOG.match(log_line)
if m:
    print(m.groupdict())

```

### 案例 13：CSV 字段解析

```python
import re

# 注意：复杂 CSV 请使用 csv 模块。以下用于理解正则处理带引号字段。

CSV_FIELD = re.compile(
    r'(?:^|,)'                    # 行首或逗号
    r'(?:'
    r'"((?:[^"]*(?:""[^"]*)*)*)"'  # 双引号包裹的字段（允许 "" 转义）
    r'|'
    r'([^,]*)'                    # 普通字段
    r')'
)

def parse_csv_line(line):
    fields = []
    for m in CSV_FIELD.finditer(line):
        if m.group(1) is not None:
            # 双引号字段：还原 "" 为 "
            fields.append(m.group(1).replace('""', '"'))
        else:
            fields.append(m.group(2))
    return fields

test_lines = [
    'Alice,30,alice@example.com',
    '"Smith, John",25,"New York"',
    '"He said ""hello""",admin,active',
]

for line in test_lines:
    print(parse_csv_line(line))
# ['Alice', '30', 'alice@example.com']
# ['Smith, John', '25', 'New York']
# ['He said "hello"', 'admin', 'active']

```

### 案例 14：重复单词检测

```python
import re

def find_repeated_words(text):
    """检测文本中连续重复的单词"""
    pattern = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b', re.I)
    results = []
    for m in pattern.finditer(text):
        results.append({
            'word': m.group('word'),
            'position': m.span(),
            'context': text[max(0, m.start()-10):m.end()+10]
        })
    return results

text = "The the quick brown fox fox jumped over the lazy lazy dog."
repeated = find_repeated_words(text)
for item in repeated:
    print(f"重复词: '{item['word']}' 在位置 {item['position']}")
# 重复词: 'The' 在位置 (0, 7)
# 重复词: 'fox' 在位置 (20, 27)
# 重复词: 'lazy' 在位置 (40, 49)

def remove_repeated_words(text):
    """移除连续重复的单词"""
    return re.sub(r'\b(\w+)(\s+\1)+\b', r'\1', text, flags=re.I)

print(remove_repeated_words("The the quick fox fox"))
# 'The quick fox'

```

### 案例 15：代码注释提取

```python
import re

def extract_python_comments(code):
    """提取 Python 代码中的注释"""
    # 单行注释
    single_line = re.findall(r'#\s*(.*?)$', code, re.M)
    # 多行字符串（docstring）
    docstrings = re.findall(r'"""(.*?)"""|\'\'\'(.*?)\'\'\'', code, re.S)
    doc_list = [d[0] or d[1] for d in docstrings]
    return {'single_line': single_line, 'docstrings': doc_list}

def extract_js_comments(code):
    """提取 JavaScript 代码中的注释"""
    results = []

    # 多行注释 /* ... */
    for m in re.finditer(r'/\*(.*?)\*/', code, re.S):
        results.append({'type': 'block', 'content': m.group(1).strip()})

    # 单行注释 //
    for m in re.finditer(r'//\s*(.*?)$', code, re.M):
        results.append({'type': 'line', 'content': m.group(1).strip()})

    return results

python_code = '''
def hello(name):
    """
    打招呼函数
    参数：name - 姓名
    """
    # 打印问候语
    print(f"Hello, {name}")  # 这是行内注释
'''

comments = extract_python_comments(python_code)
print("单行注释:", comments['single_line'])
print("文档字符串:", comments['docstrings'])

```

---

## 十、性能优化

### 10.1 预编译正则

```python
import re
import timeit

# 不预编译：每次调用都重新编译（有内部缓存，但缓存满后仍需重编译）
def search_without_compile(texts):
    return [re.search(r'\d{4}-\d{2}-\d{2}', t) for t in texts]

# 预编译：直接使用编译好的对象
DATE_RE = re.compile(r'\d{4}-\d{2}-\d{2}')
def search_with_compile(texts):
    return [DATE_RE.search(t) for t in texts]

texts = ['date: 2026-03-05'] * 10000

t1 = timeit.timeit(lambda: search_without_compile(texts), number=10)
t2 = timeit.timeit(lambda: search_with_compile(texts), number=10)
print(f"不预编译: {t1:.3f}s")
print(f"预编译:   {t2:.3f}s")
# 预编译通常快 10-40%

```

### 10.2 避免灾难性回溯

灾难性回溯（Catastrophic Backtracking）是正则性能陷阱，可使匹配时间呈指数增长。

```python
import re
import time

# 危险模式示例：嵌套量词
# (a+)+ 对于不匹配的输入会指数级回溯
DANGEROUS = re.compile(r'(a+)+b')

# 对于这类输入，回溯次数是指数级的
test = 'a' * 20  # 不含 b，必然失败

start = time.time()
result = DANGEROUS.search(test)  # 可能非常慢！
elapsed = time.time() - start
print(f"耗时: {elapsed:.3f}s, 结果: {result}")

# 优化方案：
# 1. 重写模式，消除嵌套量词
SAFE = re.compile(r'a+b')  # 直接写，无嵌套

# 2. 使用原子组（Python 3.11+）
# SAFE = re.compile(r'(?>a+)+b')

# 3. 使用固定宽度模式代替变长模式
# 不好：r'(.+?)@(.+?)\.(.+)'
# 好：  r'([^@]+)@([^.]+)\.(\w+)'

# 更多危险模式
PATTERNS_TO_AVOID = [
    r'(a*)*',           # 嵌套量词
    r'(a|aa)+',         # 交替且有重叠的子模式
    r'(\w+\s?\w+)*',    # 嵌套量词且有公共字符
]

```

### 10.3 使用字符类代替交替

```python
import re

# 慢：使用交替
SLOW = re.compile(r'a|e|i|o|u')

# 快：使用字符类
FAST = re.compile(r'[aeiou]')

# 慢：对固定字符集使用 .
SLOW2 = re.compile(r'.+@')

# 快：用否定字符类明确范围
FAST2 = re.compile(r'[^@]+@')

```

### 10.4 锚点和边界的合理使用

```python
import re

# 使用锚点减少匹配范围
# 不好：扫描整个字符串
BAD = re.compile(r'\d{4}-\d{2}-\d{2}')

# 好：如果知道日期在行首，加锚点
GOOD = re.compile(r'^\d{4}-\d{2}-\d{2}', re.M)

# 单词边界可以避免不必要的匹配
# 不好：可能匹配 'python3' 中的 'python'
BAD2 = re.compile(r'python', re.I)

# 好：精确匹配单词
GOOD2 = re.compile(r'\bpython\b', re.I)

```

### 10.5 选择合适的函数

```python
import re

text = 'hello world'

# 只需判断是否匹配：使用 search/match 而非 findall
# 不好
if re.findall(r'hello', text):
    pass

# 好
if re.search(r'hello', text):
    pass

# 大文本查找所有匹配：使用 finditer（迭代器）而非 findall（列表）
# findall 把所有结果存入内存
# finditer 按需生成

large_text = 'data ' * 100000
# 好：内存效率高
for m in re.finditer(r'data', large_text):
    process = m.start()  # 逐个处理，不需要存储全部结果

```

### 10.6 使用 re.VERBOSE 提升可维护性

```python
import re

# 复杂模式使用 VERBOSE 模式提高可读性和可维护性
# 可读性差
EMAIL_BAD = re.compile(r'[\w.!#$%&\'*+/=?^_`{|}~-]+@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}')

# 可读性好
EMAIL_GOOD = re.compile(r"""
    [\w.!#$%&'*+/=?^_`{|}~-]+  # 本地部分
    @                            # @ 符号
    (?:                          # 域名部分（非捕获分组）
        [a-zA-Z0-9]              # 域名段开头
        (?:[a-zA-Z0-9-]{0,61}   # 域名段中间
        [a-zA-Z0-9])?            # 域名段结尾（可选）
        \.                       # 点号
    )+
    [a-zA-Z]{2,}                 # 顶级域名
""", re.VERBOSE)

```

---

## 十一、最佳实践

### 11.1 始终使用原始字符串

```python
import re

# 不好：\b 可能被 Python 字符串转义解释
# pattern = re.compile('\bword\b')   # \b 是退格符 chr(8)

# 好：使用原始字符串
pattern = re.compile(r'\bword\b')    # \b 是单词边界

# 不好：\d 在普通字符串中虽然有效（\d 不是标准转义序列），但不清晰
# pattern = re.compile('\d+')

# 好：明确使用原始字符串
pattern = re.compile(r'\d+')

```

### 11.2 编译一次，多次使用

```python
import re

# 模块级别预编译，避免在循环中重复编译
EMAIL_RE = re.compile(r'[\w.+-]+@[\w-]+\.[\w.]+')
PHONE_RE = re.compile(r'1[3-9]\d{9}')
DATE_RE = re.compile(r'\d{4}-\d{2}-\d{2}')

def process_records(records):
    for record in records:
        email = EMAIL_RE.search(record.get('contact', ''))
        phone = PHONE_RE.search(record.get('contact', ''))
        date = DATE_RE.search(record.get('date', ''))
        # ...处理结果

```

### 11.3 处理 None 返回值

```python
import re

# 不好：不检查 None，可能 AttributeError
def bad_extract(text):
    return re.search(r'\d+', text).group()  # 若无匹配则崩溃

# 好：检查匹配结果
def good_extract(text):
    m = re.search(r'\d+', text)
    return m.group() if m else None

# 或使用三元表达式
def extract_or_default(text, pattern, default=''):
    m = re.search(pattern, text)
    return m.group() if m else default

```

### 11.4 使用非捕获分组减少捕获

```python
import re

# 只需要分组用于量词应用，不需要捕获时，使用 (?:)
# 不好：findall 返回分组内容，不是完整匹配
result = re.findall(r'(https?://)[\w./]+', 'http://example.com https://test.org')
print(result)  # ['http://', 'https://']（只返回分组内容！）

# 好：使用非捕获分组
result = re.findall(r'(?:https?://)[\w./]+', 'http://example.com https://test.org')
print(result)  # ['http://example.com', 'https://test.org']

```

### 11.5 测试边界条件

```python
import re

EMAIL_RE = re.compile(r'[\w.+-]+@[\w-]+\.[\w.]+')

test_cases = [
    # (输入, 是否应该匹配)
    ('user@example.com', True),
    ('user.name+tag@example.co.uk', True),
    ('user@sub.domain.org', True),
    ('@example.com', False),          # 无本地部分
    ('user@', False),                  # 无域名
    ('user@.com', False),              # 域名以点开头
    ('user@example', False),           # 无顶级域名
    ('plainaddress', False),           # 无 @
    ('user name@example.com', False),  # 本地部分有空格
]

for email, expected in test_cases:
    m = EMAIL_RE.fullmatch(email)
    matched = m is not None
    status = 'PASS' if matched == expected else 'FAIL'
    print(f"{status}: '{email}' -> {'匹配' if matched else '不匹配'} (期望: {'匹配' if expected else '不匹配'})")

```

---

## 十二、常见陷阱与注意事项

### 12.1 re.match 与 re.search 的区别

```python
import re

text = 'abc123'

# match 只从起始位置匹配
print(re.match(r'\d+', text))    # None（'a' 不是数字）
print(re.search(r'\d+', text))   # 匹配 '123'

# 容易出错：以为 match 匹配整个字符串
# match 不是全串匹配！
print(re.match(r'\d+', '123abc'))  # 匹配 '123'，不是 None

# 全串匹配用 fullmatch
print(re.fullmatch(r'\d+', '123abc'))  # None
print(re.fullmatch(r'\d+', '123'))     # 匹配

```

### 12.2 findall 分组陷阱

```python
import re

text = 'id=123 name=Alice'

# 陷阱：有分组时 findall 只返回分组内容
result = re.findall(r'(\w+)=(\w+)', text)
print(result)  # [('id', '123'), ('name', 'Alice')]（元组列表）

# 想要完整匹配时，使用非捕获分组或 finditer
result = re.findall(r'(?:\w+)=(?:\w+)', text)
print(result)  # ['id=123', 'name=Alice']

# 或者使用 finditer
for m in re.finditer(r'(\w+)=(\w+)', text):
    print(m.group())     # 完整匹配
    print(m.group(1, 2)) # 分组内容

```

### 12.3 贪婪匹配导致超出预期

```python
import re

html = '<b>one</b> and <b>two</b>'

# 陷阱：贪婪匹配吃掉了中间内容
result = re.findall(r'<b>.*</b>', html)
print(result)  # ['<b>one</b> and <b>two</b>']（贪婪，匹配过多）

# 解决：使用非贪婪
result = re.findall(r'<b>.*?</b>', html)
print(result)  # ['<b>one</b>', '<b>two</b>']

# 或使用否定字符类（更高效）
result = re.findall(r'<b>[^<]*</b>', html)
print(result)  # ['<b>one</b>', '<b>two</b>']

```

### 12.4 re.MULTILINE 不影响 \\A 和 \\Z

```python
import re

text = "line1\nline2\nline3"

# ^ 在 MULTILINE 模式下匹配每行开头
print(re.findall(r'^\w+', text, re.M))  # ['line1', 'line2', 'line3']

# \A 始终只匹配字符串开头
print(re.findall(r'\A\w+', text, re.M))  # ['line1']

# $ 在 MULTILINE 模式下匹配每行结尾（注意：结尾换行符前）
print(re.findall(r'\w+$', text, re.M))  # ['line1', 'line2', 'line3']

# \Z 只匹配字符串结尾
print(re.findall(r'\w+\Z', text, re.M))  # ['line3']

```

### 12.5 特殊字符在字符类中的行为

```python
import re

# 字符类内部，大多数元字符失去特殊含义
# . 在 [] 中是字面量点号
print(re.findall(r'[.]', 'a.b'))    # ['.']（匹配字面量点）
print(re.findall(r'.', 'a.b'))      # ['a', '.', 'b']（. 匹配任意字符）

# ] 需要放在首位或转义
print(re.findall(r'[]a]', 'a]b'))   # ['a', ']']
print(re.findall(r'[a\]]', 'a]b'))  # ['a', ']']

# - 在字符类中需要放首位、末位或转义
print(re.findall(r'[a-z]', 'abc'))  # ['a', 'b', 'c']（范围）
print(re.findall(r'[-az]', 'a-z'))  # ['a', '-', 'z']（- 在首位是字面量）

# ^ 只在首位有特殊含义（取反）
print(re.findall(r'[a^]', 'a^b'))   # ['a', '^']（^ 不在首位是字面量）
print(re.findall(r'[^a]', 'a^b'))   # ['^', 'b']（取反）

```

### 12.6 后行断言的固定宽度限制

```python
import re

# Python 3.10 及以前，后行断言必须是固定宽度
# 这样会报错：
try:
    re.compile(r'(?<=a+)b')  # 可变宽度，报错
except re.error as e:
    print(f"错误: {e}")  # look-behind requires fixed width pattern

# 固定宽度的后行断言是合法的
print(re.findall(r'(?<=abc)d', 'abcd'))   # ['d']
print(re.findall(r'(?<=ab|cd)e', 'abe'))  # Python 3.6+ 支持交替（等宽）

# Python 3.11+ 支持可变宽度后行断言
import sys
if sys.version_info >= (3, 11):
    print(re.findall(r'(?<=a+)b', 'aaab'))  # ['b']

```

### 12.7 字节串与字符串的区别

```python
import re

# 字符串模式
text = "hello 123"
print(re.findall(r'\d+', text))    # ['123']

# 字节串模式（pattern 和 string 必须同类型）
data = b"hello 123"
print(re.findall(rb'\d+', data))   # [b'123']

# 混用会报错
try:
    re.findall(r'\d+', b'hello 123')
except TypeError as e:
    print(f"错误: {e}")  # cannot use a string pattern on a bytes-like object

```

### 12.8 re.sub 替换字符串中的反斜杠

```python
import re

# repl 字符串中，\ 有特殊含义（\1、\g<1> 等）
# 如果替换内容本身含有反斜杠，需要双重转义或使用 lambda

# 陷阱：直接用含反斜杠的字符串替换
text = 'path: /usr/local/bin'
# 想把 / 替换为 \
result = re.sub(r'/', r'\\', text)  # 注意：r'\\' 是两个反斜杠在原始字符串中
print(result)  # 'path: \usr\local\bin'（正确）

# 更安全：使用 lambda 避免歧义
result = re.sub(r'/', lambda m: '\\', text)
print(result)  # 'path: \usr\local\bin'

# 替换字符串中 \1 等的问题
text = 'John Smith'
# 想在两个词之间加逗号，同时保留原文
result = re.sub(r'(\w+)\s+(\w+)', r'\2, \1', text)
print(result)  # 'Smith, John'（正确使用分组引用）

```

### 12.9 Unicode 与 ASCII 模式差异

```python
import re

# 默认 Unicode 模式
text = "hello 你好 123 Ⅱ"

# \w 在 Unicode 模式下匹配所有 Unicode 字母数字
print(re.findall(r'\w+', text))         # ['hello', '你好', '123', 'Ⅱ']

# \w 在 ASCII 模式下只匹配 ASCII 字母数字下划线
print(re.findall(r'\w+', text, re.A))  # ['hello', '123']

# 处理多语言文本时要注意这个差异
def is_word(s, ascii_only=False):
    flags = re.A if ascii_only else 0
    return bool(re.fullmatch(r'\w+', s, flags))

print(is_word('hello'))        # True
print(is_word('你好'))          # True（Unicode 模式）
print(is_word('你好', True))    # False（ASCII 模式）

```

---

## 附录：速查表

### 常用正则模式速查

| 用途             | 正则表达式                                                        |
| -------------- | ------------------------------------------------------------ |
| 整数             | \[+-\]?\\d+                                                  |
| 浮点数            | \[+-\]?\\d+(?:\\.\\d+)?(?:\[eE\]\[+-\]?\\d+)?                |
| 中国手机号          | 1\[3-9\]\\d{9}                                               |
| 电子邮箱（简化）       | \[\\w.+-\]+@\[\\w-\]+\\.\[\\w.\]+                            |
| IPv4 地址        | \\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b                             |
| HTTP/HTTPS URL | https?://\[^\\s<>"\]+                                        |
| HTML 标签        | <\[^>\]+>                                                    |
| 中文字符           | \[\\u4e00-\\u9fff\]+                                         |
| 日期（ISO）        | \\d{4}-(?:0\[1-9\]\|1\[0-2\])-(?:0\[1-9\]|\[12\]\\d|3\[01\]) |
| 时间（24H）        | (?:\[01\]\\d\|2\[0-3\]):\[0-5\]\\d(?::\[0-5\]\\d)?           |
| 十六进制颜色         | #(?:\[0-9a-fA-F\]{3}\|\[0-9a-fA-F\]{6})\\b                   |
| 空行             | ^\\s\*$                                                      |
| 行首/尾空白         | ^\\s+\|\\s+$                                                 |
| 重复单词           | \\b(\\w+)\\s+\\1\\b                                          |
| 身份证（中国）        | \\d{17}\[\\dXx\]                                             |
| 邮政编码（中国）       | \[1-9\]\\d{5}                                                |

### re 模块函数速查

| 函数                    | 返回值         | 说明      |
| --------------------- | ----------- | ------- |
| re.compile(pat)       | Pattern     | 编译正则    |
| re.match(pat, s)      | Match\|None | 从起始匹配   |
| re.fullmatch(pat, s)  | Match\|None | 全串匹配    |
| re.search(pat, s)     | Match\|None | 搜索第一个   |
| re.findall(pat, s)    | list        | 所有匹配列表  |
| re.finditer(pat, s)   | iterator    | 所有匹配迭代器 |
| re.sub(pat, repl, s)  | str         | 替换      |
| re.subn(pat, repl, s) | (str, int)  | 替换并计数   |
| re.split(pat, s)      | list        | 分割      |
| re.escape(s)          | str         | 转义特殊字符  |
| re.purge()            | None        | 清除缓存    |

---

## 最佳实践

**编译复用：频繁使用的正则表达式应提前 compile**：`re.compile()` 将正则预编译为 Pattern 对象，避免每次调用都重新解析。内部有 512 个 pattern 的 LRU 缓存，但显式 compile 语义更清晰且不受缓存淘汰影响。

```python
# 正确：提前 compile，用于循环或频繁调用
EMAIL_RE = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')
for email in email_list:
    if EMAIL_RE.match(email):
        ...

# 低效：每次都解析正则
for email in email_list:
    if re.match(r'^[a-zA-Z0-9_.+-]+@...', email):
        ...

```

**用具名捕获组（`?P<name>`）提高可读性**：位置编号 `\1`、`\2` 在正则修改后极易出错，具名捕获组让代码自描述。

```python
import re
pattern = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})')
m = pattern.search("发布日期：2026-05-07")
print(m.group('year'), m.group('month'), m.group('day'))
# 2026 05 07

```

**非贪婪匹配处理嵌套内容**：默认贪婪匹配 `.*` 会尽可能多匹配，提取 HTML 标签内容时匹配到最后一个闭合标签。用 `.*?` 改为非贪婪。

```python
html = "<b>hello</b> and <b>world</b>"
# 贪婪：匹配从第一个 <b> 到最后一个 </b> 的所有内容
re.findall(r'<b>.*</b>', html)   # ['<b>hello</b> and <b>world</b>']

# 非贪婪：匹配每个 <b>...</b>
re.findall(r'<b>.*?</b>', html)  # ['<b>hello</b>', '<b>world</b>']

```

**用 `re.VERBOSE` 写可读的多行正则**：复杂正则应分行书写并加注释，`re.VERBOSE`（或 `re.X`）忽略空白和 `#` 后的注释。

```python
EMAIL_RE = re.compile(r"""
    ^                   # 字符串起始
    [a-zA-Z0-9_.+-]+    # 用户名
    @                   # @符号
    [a-zA-Z0-9-]+       # 域名
    \.                  # 点
    [a-zA-Z0-9-.]+      # 顶级域名
    $                   # 字符串结尾
""", re.VERBOSE)

```

---

## 常见陷阱

### 陷阱：re.match 只匹配字符串开头

**现象：** `re.match(r'\d+', 'abc123')` 返回 `None`，以为正则写错了。

**原因：** `re.match()` 只从字符串**开头**开始匹配，等价于在正则前加了 `^`。`re.search()` 才是在整个字符串中搜索。

**解决：** 搜索任意位置用 `re.search()`；全串匹配用 `re.fullmatch()`；只需开头匹配用 `re.match()`。

```python
re.match(r'\d+', 'abc123')    # None（不从开头）
re.search(r'\d+', 'abc123')   # Match('123')
re.fullmatch(r'\d+', '123')   # Match('123')
re.fullmatch(r'\d+', 'abc123') # None（不是全串数字）

```

---

### 陷阱：贪婪匹配导致提取范围过大

**现象：** 提取 HTML 属性值时，`".*"` 从第一个引号匹配到了最后一个引号。

**原因：** 默认贪婪，`".*"` 会尽可能多匹配。

**解决：** 改为非贪婪 `".*?"`，或用字符类排除引号 `"[^"]+"` （更高效）。

```python
text = 'class="header" id="main"'
re.findall(r'".*"', text)    # ['"header" id="main"']  贪婪
re.findall(r'".*?"', text)   # ['"header"', '"main"']  非贪婪
re.findall(r'"[^"]+"', text) # ['"header"', '"main"']  排除法（最快）

```

---

### 陷阱：忘记转义特殊字符

**现象：** 正则 `re.search(r'1.2', '1X2')` 意外匹配成功。

**原因：** `.` 在正则中是通配符，匹配任意一个字符（除换行）。字面点号需要转义为 `\.`。

**解决：** 匹配用户输入的字面字符串时，用 `re.escape()` 转义。

```python
# 错误：匹配了任意字符
re.search(r'1.2', '1X2')      # Match

# 正确：匹配字面点号
re.search(r'1\.2', '1X2')     # None
re.search(r'1\.2', '1.2')     # Match

# 动态字符串转义
user_input = "1.2"
re.search(re.escape(user_input), text)

```

---

## 参见

- [内置函数完全参考](https://blog.vercanti.com/python-nei-zhi-han-shu-wan-quan-can-kao/)
- [数据类型](https://blog.vercanti.com/python-nei-zhi-shu-ju-lei-xing-wan-quan-can-kao/)
- [装饰器与函数高级](https://blog.vercanti.com/python-zhuang-shi-qi-yu-han-shu-gao-ji-yong-fa/)