# Python 正则表达式

## 学习目标
- 掌握正则表达式的基本语法
- 学会使用 `re` 模块进行匹配、搜索和替换
- 理解贪婪与非贪婪匹配
- 掌握常用正则模式和实际应用

---

## 1. re 模块基础

### 1.1 匹配与搜索

```python
import re

# re.match - 从字符串开头匹配
result = re.match(r'hello', 'hello world')
print(result)          # <re.Match object; span=(0, 5), match='hello'>
print(result.group())  # hello

# re.search - 搜索整个字符串
result = re.search(r'world', 'hello world')
print(result.group())  # world

# re.findall - 查找所有匹配
result = re.findall(r'\d+', 'abc 123 def 456')
print(result)  # ['123', '456']

# re.finditer - 返回迭代器
for match in re.finditer(r'\d+', 'abc 123 def 456'):
    print(f"找到: {match.group()} 在位置 {match.span()}")
```

### 1.2 替换与分割

```python
import re

# re.sub - 替换
result = re.sub(r'\d+', '#', 'abc 123 def 456')
print(result)  # abc # def #

# 替换并限制次数
result = re.sub(r'\d+', '#', '1 2 3 4', count=2)
print(result)  # # # 3 4

# re.subn - 替换并返回次数
result = re.subn(r'\d+', '#', 'abc 123 def 456')
print(result)  # ('abc # def #', 2)

# re.split - 分割
result = re.split(r'\s+', 'hello   world  python')
print(result)  # ['hello', 'world', 'python']
```

---

## 2. 正则表达式语法

### 2.1 元字符

```python
import re

# . 匹配任意字符（除换行）
print(re.findall(r'a.c', 'abc aac acc axc'))  # ['abc', 'aac', 'acc', 'axc']

# ^ 匹配开头  $ 匹配结尾
print(bool(re.match(r'^hello', 'hello world')))   # True
print(bool(re.search(r'world$', 'hello world')))  # True

# * 零次或多次  + 一次或多次  ? 零次或一次
print(re.findall(r'ab*c', 'ac abc abbc abbbc'))  # ['ac', 'abc', 'abbc', 'abbbc']
print(re.findall(r'ab+c', 'ac abc abbc'))         # ['abc', 'abbc']
print(re.findall(r'ab?c', 'ac abc abbc'))         # ['ac', 'abc']

# {n} 恰好n次  {n,} 至少n次  {n,m} n到m次
print(re.findall(r'a{3}', 'aa aaa aaaa'))         # ['aaa', 'aaa']
print(re.findall(r'a{2,}', 'a aa aaa'))           # ['aa', 'aaa']
print(re.findall(r'a{2,3}', 'a aa aaa aaaa'))     # ['aa', 'aaa', 'aaa']
```

### 2.2 字符类

```python
import re

# [...] 字符集
print(re.findall(r'[aeiou]', 'hello world'))  # ['e', 'o', 'o']

# [^...] 否定字符集
print(re.findall(r'[^aeiou]', 'hello'))  # ['h', 'l', 'l']

# \d 数字  \D 非数字
print(re.findall(r'\d+', 'age: 25, score: 90'))  # ['25', '90']

# \w 单词字符 [a-zA-Z0-9_]  \W 非单词字符
print(re.findall(r'\w+', 'hello world!'))  # ['hello', 'world']

# \s 空白字符  \S 非空白字符
print(re.findall(r'\S+', 'hello   world'))  # ['hello', 'world']

# 预定义字符集
# \d = [0-9]
# \w = [a-zA-Z0-9_]
# \s = [ \t\n\r\f\v]
```

### 2.3 分组与引用

```python
import re

# (...) 捕获分组
result = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-01-15')
print(result.group(0))  # 2024-01-15（完整匹配）
print(result.group(1))  # 2024（第一组）
print(result.group(2))  # 01（第二组）
print(result.group(3))  # 15（第三组）
print(result.groups())  # ('2024', '01', '15')

# 命名分组 (?P<name>...)
result = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', '2024-01')
print(result.group('year'))   # 2024
print(result.group('month'))  # 01

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

# \1 \2 反向引用
print(re.findall(r'(\w+) \1', 'hello hello world world'))  # ['hello', 'world']
```

---

## 3. 贪婪与非贪婪

```python
import re

text = '<div>content1</div><div>content2</div>'

# 贪婪匹配（默认）- 匹配尽可能长的字符串
print(re.findall(r'<div>.*</div>', text))
# ['<div>content1</div><div>content2</div>']

# 非贪婪匹配 - 匹配尽可能短的字符串
print(re.findall(r'<div>.*?</div>', text))
# ['<div>content1</div>', '<div>content2</div>']

# 其他非贪婪量词
# *?  +?  ??  {n,m}?
text = '"hello" "world"'
print(re.findall(r'".*?"', text))  # ['"hello"', '"world"']
```

---

## 4. 编译与标志

### 4.1 编译正则

```python
import re

# 编译正则（提高效率，适合重复使用）
pattern = re.compile(r'\d{4}-\d{2}-\d{2}')

# 使用编译后的对象
print(pattern.match('2024-01-15'))
print(pattern.search('date: 2024-01-15'))
print(pattern.findall('2024-01-15 2024-02-20'))
```

### 4.2 标志位

```python
import re

# re.IGNORECASE / re.I - 忽略大小写
print(re.findall(r'hello', 'HELLO hello Hello', re.I))
# ['HELLO', 'hello', 'Hello']

# re.DOTALL / re.S - . 匹配换行
print(re.findall(r'a.b', 'a\nb', re.S))  # ['a\nb']

# re.MULTILINE / re.M - ^$ 匹配每行开头结尾
text = 'line1\nline2\nline3'
print(re.findall(r'^line\d', text, re.M))  # ['line1', 'line2', 'line3']

# 多个标志组合
print(re.findall(r'hello.world', 'HELLO\nWORLD', re.I | re.S))
```

---

## 5. 实际应用

### 5.1 邮箱验证

```python
import re

def validate_email(email):
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

# 测试
print(validate_email('test@example.com'))      # True
print(validate_email('invalid.email'))          # False
print(validate_email('user.name+tag@domain.co')) # True
```

### 5.2 手机号提取

```python
import re

def extract_phones(text):
    # 中国手机号
    pattern = r'1[3-9]\d{9}'
    return re.findall(pattern, text)

text = "联系我：13800138000 或 15912345678"
print(extract_phones(text))  # ['13800138000', '15912345678']
```

### 5.3 URL 解析

```python
import re

def parse_url(url):
    pattern = r'(?P<protocol>https?)://(?P<host>[^:/\s]+)(?::(?P<port>\d+))?(?P<path>/[^\s]*)?'
    match = re.match(pattern, url)
    if match:
        return match.groupdict()
    return None

url = "https://example.com:8080/path/to/page?query=1"
print(parse_url(url))
# {'protocol': 'https', 'host': 'example.com', 'port': '8080', 'path': '/path/to/page?query=1'}
```

### 5.4 日志解析

```python
import re
from datetime import datetime

def parse_log_line(line):
    """解析 Nginx 日志"""
    pattern = r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<size>\d+)'
    match = re.match(pattern, line)
    return match.groupdict() if match else None

log = '192.168.1.1 - - [15/Jan/2024:14:30:00 +0800] "GET /index.html HTTP/1.1" 200 1234'
result = parse_log_line(log)
print(result)
```

---

## 6. 正则最佳实践

```python
import re

# 1. 使用原始字符串
# 推荐
pattern = r'\d+\w+'
# 不推荐
pattern = '\\d+\\w+'

# 2. 复杂正则应编译
EMAIL_PATTERN = re.compile(r'^[\w.-]+@[\w.-]+\.\w{2,}$')

# 3. 使用注释模式（VERBOSE）
PHONE_PATTERN = re.compile(r'''
    ^
    (\+?86)?           # 可选的国家代码
    [-\s]?             # 可选的分隔符
    1[3-9]\d{9}        # 手机号
    $
''', re.VERBOSE)

# 4. 处理匹配失败
match = re.search(r'\d+', 'no numbers')
if match:
    print(match.group())
else:
    print("未找到匹配")
```

---

## 本节小结

- **匹配函数**：`match`（开头）、`search`（全局）、`findall`、`finditer`
- **修改函数**：`sub`（替换）、`split`（分割）
- **元字符**：`. ^ $ * + ? {} [] \ | ()`
- **分组**：捕获分组 `()`、命名分组 `(?P<name>...)`、非捕获 `(?:...)`
- **贪婪**：`*` `+` `?` 默认贪婪，加 `?` 变非贪婪
- **编译**：频繁使用的正则应编译，支持标志位

---

## 练习

1. 编写正则验证中国大陆身份证号（18位）
2. 使用正则从 HTML 中提取所有链接的 href 和文本
3. 编写函数，将驼峰命名转换为下划线命名（camelCase -> snake_case）
4. 使用正则解析 Markdown 中的标题、链接和图片

