7 minutes
正则表达式从入门到精通
什么是正则表达式
正则表达式(Regular Expression,简称 regex)是一种用于匹配字符串中字符组合的模式。它提供了一种强大、灵活且高效的方式来处理文本——搜索、匹配、替换和提取数据。
Python 的 re 模块提供了完整的正则表达式支持。在本章中,我们将从基础语法开始,逐步深入到实战应用。
第一个正则表达式
import re
# 检查字符串中是否包含 "python"
text = "我喜欢 Python 编程语言"
pattern = r"Python"
result = re.search(pattern, text)
if result:
print(f"找到了: {result.group()}") # 输出: 找到了: Python
这里的 r"Python" 中的 r 表示原始字符串(raw string),它会告诉 Python 不要处理字符串中的反斜杠转义,这对于正则表达式至关重要。
re 模块的核心函数
re.match() — 从开头匹配
re.match() 从字符串的起始位置开始匹配,如果开头不匹配则返回 None:
import re
text = "Python 是一门优秀的编程语言"
# 从开头匹配
match = re.match(r"Python", text)
if match:
print(match.group()) # Python
# 不从开头匹配则失败
match = re.match(r"优秀", text)
print(match) # None
re.search() — 搜索整个字符串
re.search() 扫描整个字符串,找到第一个匹配的位置:
text = "我喜欢 Python,也喜欢 Java"
# 搜索整个字符串
match = re.search(r"Java", text)
if match:
print(match.group()) # Java
print(match.start()) # 匹配起始位置
print(match.end()) # 匹配结束位置
print(match.span()) # (起始, 结束) 元组
re.findall() — 查找所有匹配
re.findall() 返回所有匹配的字符串列表:
text = "我的邮箱是 alice@example.com,Bob 的邮箱是 bob@test.com"
emails = re.findall(r"\w+@\w+\.\w+", text)
print(emails)
# 输出: ['alice@example.com', 'bob@test.com']
如果有分组,findall() 返回元组列表:
text = "2026-05-21, 2026-06-15"
dates = re.findall(r"(\d{4})-(\d{2})-(\d{2})", text)
print(dates)
# 输出: [('2026', '05', '21'), ('2026', '06', '15')]
re.finditer() — 迭代匹配对象
re.finditer() 返回一个迭代器,生成 Match 对象,适合处理大量匹配:
text = "Python 1, Java 2, Go 3"
for match in re.finditer(r"\d+", text):
print(f"找到数字: {match.group()} 在位置 {match.span()}")
# 输出:
# 找到数字: 1 在位置 (7, 8)
# 找到数字: 2 在位置 (14, 15)
# 找到数字: 3 在位置 (19, 20)
re.sub() — 替换
re.sub() 替换所有匹配的文本:
text = "我的电话是 138-0000-1111,请拨打"
# 替换手机号为 ***
masked = re.sub(r"\d{3}-\d{4}-\d{4}", "***-****-****", text)
print(masked) # 我的电话是 ***-****-****,请拨打
# 使用函数进行动态替换
def replace_func(match):
phone = match.group()
return phone[:3] + "-****-" + phone[-4:]
masked = re.sub(r"\d{3}-\d{4}-\d{4}", replace_func, text)
print(masked) # 我的电话是 138-****-1111,请拨打
re.split() — 分割
re.split() 按模式分割字符串:
text = "苹果, 香蕉; 橙子 | 葡萄"
# 按逗号、分号或竖线分割
fruits = re.split(r"[,;|]\s*", text)
print(fruits) # ['苹果', '香蕉', '橙子', '葡萄']
元字符与字符类
常用元字符
| 元字符 | 说明 | 示例 |
|---|---|---|
. |
匹配任意单个字符(除换行符) | a.b 匹配 acb、a_b |
^ |
匹配字符串开头 | ^Hello 匹配以 Hello 开头的字符串 |
$ |
匹配字符串结尾 | end$ 匹配以 end 结尾的字符串 |
* |
匹配前一个字符 0 次或多次 | ab*c 匹配 ac、abc、abbc |
+ |
匹配前一个字符 1 次或多次 | ab+c 匹配 abc、abbc,不匹配 ac |
? |
匹配前一个字符 0 次或 1 次 | ab?c 匹配 ac、abc |
{n} |
精确匹配 n 次 | a{3} 匹配 aaa |
{n,} |
至少匹配 n 次 | a{2,} 匹配 aa、aaa |
{n,m} |
匹配 n 到 m 次 | a{2,4} 匹配 aa、aaa、aaaa |
字符类
字符类用 [] 定义,匹配括号内的任意一个字符:
import re
# [abc] 匹配 a、b 或 c
print(re.findall(r"[aeiou]", "hello world")) # ['e', 'o', 'o']
# [a-z] 匹配所有小写字母
print(re.findall(r"[a-z]", "Hello 123")) # ['e', 'l', 'l', 'o']
# [^abc] 匹配除 a、b、c 外的任意字符
print(re.findall(r"[^0-9]", "abc123def")) # ['a', 'b', 'c', 'd', 'e', 'f']
预定义字符类
| 字符类 | 等价于 | 说明 |
|---|---|---|
\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] |
匹配非空白字符 |
text = "用户: 小明, 年龄: 25, 邮箱: xiao@test.com"
# 提取所有数字
print(re.findall(r"\d+", text)) # ['25']
# 提取所有单词
print(re.findall(r"\w+", text)) # ['用户', '小明', '年龄', '25', '邮箱', 'xiao', 'test', 'com']
分组与捕获
普通分组 ()
圆括号用于创建分组,可以提取匹配中的特定部分:
import re
text = "2026-05-21"
# 使用分组分别提取年、月、日
match = re.search(r"(\d{4})-(\d{2})-(\d{2})", text)
if match:
print(match.group(0)) # 完整匹配: 2026-05-21
print(match.group(1)) # 年: 2026
print(match.group(2)) # 月: 05
print(match.group(3)) # 日: 21
print(match.groups()) # ('2026', '05', '21')
命名分组 (?P<name>)
命名分组让代码更可读:
text = "2026-05-21"
match = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", text)
if match:
print(match.group("year")) # 2026
print(match.group("month")) # 05
print(match.group("day")) # 21
# 在 re.sub 中使用命名分组
def format_date(match):
return f"{match.group('month')}/{match.group('day')}/{match.group('year')}"
formatted = re.sub(
r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})",
format_date,
"今天是 2026-05-21"
)
print(formatted) # 今天是 05/21/2026
非捕获分组 (?:...)
有时只需要分组来应用量词,而不需要捕获内容:
text = "hello world hello python"
# 使用捕获分组(不必要的捕获)
matches = re.findall(r"(hello)\s+\w+", text)
print(matches) # ['hello', 'hello'] — 只捕获了 hello
# 使用非捕获分组
matches = re.findall(r"(?:hello)\s+(\w+)", text)
print(matches) # ['world', 'python'] — 只捕获了目标内容
反向引用
可以在同一个正则表达式中引用之前捕获的分组:
# 匹配重叠的单词(如 "hello hello")
text = "hello hello world world python"
duplicates = re.findall(r"(\w+)\s+\1", text)
print(duplicates) # ['hello', 'world']
# 在 re.sub 中使用反向引用
text = "2026-05-21"
print(re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\2/\3/\1", text))
# 输出: 05/21/2026
前瞻断言与后顾断言
前瞻断言 (Lookahead)
前瞻断言检查后面是否跟着某个模式,但不消耗字符:
text = "5px, 10px, 20em, 30px"
# 正向前瞻: 匹配后面跟 "px" 的数字
print(re.findall(r"\d+(?=px)", text))
# 输出: ['5', '10', '30']
# 负向前瞻: 匹配后面不跟 "px" 的数字
print(re.findall(r"\d+(?!px)", text))
# 输出: ['5', '1', '20', '30']
后顾断言 (Lookbehind)
后顾断言检查前面是否有某个模式:
text = "$100, $200, €50, $300"
# 正向后顾: 匹配前面有 "$" 的数字
print(re.findall(r"(?<=\$)\d+", text))
# 输出: ['100', '200', '300']
# 负向后顾: 匹配前面没有 "$" 的数字
print(re.findall(r"(?<!\$)\d+", text))
# 输出: ['50']
综合使用
text = "apple: 5元, banana: 3元, cherry: 8元"
# 提取价格数字(前面有 ": ",后面有 "元")
prices = re.findall(r"(?<=: )\d+(?=元)", text)
print(prices) # ['5', '3', '8']
贪婪与非贪婪
正则表达式默认是贪婪的,会尽可能多地匹配:
import re
text = "<div><p>你好</p></div>"
# 贪婪匹配: 尽可能长
greedy = re.search(r"<.+>", text)
print(greedy.group()) # <div><p>你好</p></div>
# 非贪婪匹配: 尽可能短(加 ?)
non_greedy = re.search(r"<.+?>", text)
print(non_greedy.group()) # <div>
关键区别:
text = "a123b456c"
# 贪婪
print(re.findall(r"a(.+)b", text)) # ['123b456']
# 非贪婪
print(re.findall(r"a(.+?)b", text)) # ['123']
| 贪婪版本 | 非贪婪版本 |
|---|---|
* |
*? |
+ |
+? |
? |
?? |
{n,m} |
{n,m}? |
编译正则表达式
当同一个正则表达式被多次使用时,编译它可以提高性能:
import re
# 编译正则表达式
email_pattern = re.compile(r"\b\w+@\w+\.\w+\b")
# 使用编译后的对象(方法相同)
text = "联系我们: support@example.com 或 info@test.org"
emails = email_pattern.findall(text)
print(emails) # ['support@example.com', 'info@test.org']
for match in email_pattern.finditer(text):
print(f"邮箱: {match.group()} (位置: {match.span()})")
编译的好处:
- 性能提升——正则表达式只解析一次
- 代码复用——在多个地方使用同一个模式
- 可读性——给模式一个有意义的变量名
标志 (Flags)
标志可以改变正则表达式的行为:
| 标志 | 缩写 | 说明 |
|---|---|---|
re.IGNORECASE |
re.I |
忽略大小写 |
re.MULTILINE |
re.M |
多行模式,^ 和 $ 匹配每行 |
re.DOTALL |
re.S |
. 匹配换行符 |
re.VERBOSE |
re.X |
允许注释和空白,提高可读性 |
re.ASCII |
re.A |
让 \w、\d 等只匹配 ASCII 字符 |
re.IGNORECASE
text = "Python python PYTHON"
print(re.findall(r"python", text, re.IGNORECASE))
# 输出: ['Python', 'python', 'PYTHON']
re.MULTILINE
text = """第一行
第二行
第三行"""
# 默认: ^ 只匹配整个字符串开头
print(re.findall(r"^\d+", text, re.MULTILINE)) # []
# 多行模式: ^ 匹配每行开头
text = """1. 第一项
2. 第二项
3. 第三项"""
print(re.findall(r"^\d+", text, re.MULTILINE))
# 输出: ['1', '2', '3']
re.DOTALL
text = "你好\n世界"
# 默认: . 不匹配换行符
print(re.findall(r"你.界", text)) # []
# DOTALL: . 匹配换行符
print(re.findall(r"你.界", text, re.DOTALL)) # ['你好\n世界']
re.VERBOSE
# 复杂正则表达式变得可读
pattern = re.compile(r"""
\b # 单词边界
\d{3,4} # 区号: 3-4位数字
[- ]? # 可选的分隔符
\d{7,8} # 号码: 7-8位数字
\b # 单词边界
""", re.VERBOSE)
text = "联系电话: 010-12345678 或 0755 87654321"
print(pattern.findall(text))
# 输出: ['010-12345678', '0755 87654321']
实战案例
1. 邮箱验证
import re
def is_valid_email(email: str) -> bool:
"""验证邮箱地址是否合法"""
pattern = re.compile(r"""
^ # 开头
[a-zA-Z0-9._%+-]+ # 用户名: 字母数字._%+-
@ # @ 符号
[a-zA-Z0-9.-]+ # 域名
\. # 点
[a-zA-Z]{2,} # 顶级域名: 至少2个字母
$ # 结尾
""", re.VERBOSE | re.IGNORECASE)
return bool(pattern.match(email))
# 测试
emails = ["user@example.com", "invalid-email", "user@.com", "user.name@domain.co.uk"]
for email in emails:
print(f"{email}: {'有效' if is_valid_email(email) else '无效'}")
2. 手机号提取与脱敏
import re
def extract_phones(text: str) -> list[str]:
"""提取中国手机号(11位数字,以1开头)"""
return re.findall(r"\b1[3-9]\d{9}\b", text)
def mask_phones(text: str) -> str:
"""手机号脱敏:138****1111"""
return re.sub(r"\b(1[3-9]\d)(\d{4})(\d{4})\b", r"\1****\3", text)
text = "请联系张先生 13812345678 或李女士 15987654321"
print(extract_phones(text)) # ['13812345678', '15987654321']
print(mask_phones(text))
# 请联系张先生 138****5678 或李女士 159****4321
3. URL 解析
import re
def parse_url(url: str) -> dict:
"""解析 URL 的各个组成部分"""
pattern = re.compile(r"""
(?P<protocol>https?):// # 协议
(?P<domain>[^/:]+) # 域名
(?::(?P<port>\d+))? # 可选端口
(?P<path>/[^?#]*) # 路径
(?:\?(?P<query>[^#]*))? # 可选查询参数
(?:\#(?P<fragment>.*))? # 可选锚点
""", re.VERBOSE | re.IGNORECASE)
match = pattern.match(url)
if match:
return match.groupdict()
return {}
url = "https://example.com:8080/path/to/page?name=python&version=3.13#section1"
print(parse_url(url))
# {
# 'protocol': 'https',
# 'domain': 'example.com',
# 'port': '8080',
# 'path': '/path/to/page',
# 'query': 'name=python&version=3.13',
# 'fragment': 'section1'
# }
4. 日志文件解析
import re
log_pattern = re.compile(r"""
(?P<ip>\d+\.\d+\.\d+\.\d+) # IP 地址
\s-\s-\s
\[(?P<time>[^\]]+)\] # 时间戳
\s"(?P<method>\w+) # HTTP 方法
\s(?P<path>[^\s]+) # 请求路径
\s[^"]*"
\s(?P<status>\d{3}) # 状态码
""", re.VERBOSE)
log_line = '192.168.1.1 - - [21/May/2026:10:30:25 +0800] "GET /api/users HTTP/1.1" 200 1234'
match = log_pattern.match(log_line)
if match:
data = match.groupdict()
print(f"IP: {data['ip']}")
print(f"时间: {data['time']}")
print(f"方法: {data['method']}")
print(f"路径: {data['path']}")
print(f"状态: {data['status']}")
5. 数据清洗
import re
def clean_text(text: str) -> str:
"""清理文本中的多余空白和特殊字符"""
# 移除 HTML 标签
text = re.sub(r"<[^>]+>", "", text)
# 将多个空白合并为一个
text = re.sub(r"\s+", " ", text)
# 移除开头和结尾的空白
text = text.strip()
return text
def extract_keywords(text: str) -> list[str]:
"""提取中文和英文关键词"""
# 匹配中文字符或英文单词
return re.findall(r"[\u4e00-\u9fff]+|[a-zA-Z]\w*", text)
html = "<p>欢迎学习 Python 编程!</p><br>这是 第二 段。</p>"
clean = clean_text(html)
print(clean) # 欢迎学习 Python 编程!这是 第二 段。
keywords = extract_keywords("Python编程 数据分析 Machine Learning")
print(keywords) # ['Python', '编程', '数据分析', 'Machine', 'Learning']
常用正则表达式速查表
| 用途 | 正则表达式 |
|---|---|
| 邮箱 | ^[\w._%+-]+@[\w.-]+\.[a-zA-Z]{2,}$ |
| 中国手机号 | ^1[3-9]\d{9}$ |
| 固定电话 | ^0\d{2,3}-?\d{7,8}$ |
| URL | ^https?://[\w./?=&%-]+$ |
| IP 地址 (IPv4) | ^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$ |
| 日期 (YYYY-MM-DD) | ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ |
| 时间 (HH:MM:SS) | ^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$ |
| 正整数 | ^[1-9]\d*$ |
| 中文字符 | [\u4e00-\u9fff] |
| HTTP 状态码 2xx | ^2\d{2}$ |
| 强密码(8位以上,含大小写和数字) | ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$ |
| 连续重复单词 | (\w+)\s+\1 |
| 16 进制颜色 | ^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ |
性能与最佳实践
1. 避免灾难性回溯
某些正则表达式在特定输入下会导致指数级回溯:
import re
import time
# BAD: 灾难性回溯
bad_pattern = re.compile(r"(a+)+b")
# 对 "aaaaaaaaac" 这样的输入会运行极长时间
# GOOD: 避免嵌套量词
good_pattern = re.compile(r"a+b")
# 测试
text = "a" * 30 + "c"
start = time.perf_counter()
bad_pattern.match(text)
print(f"BAD: {time.perf_counter() - start:.4f}秒")
# 指数级时间
2. 尽量使用原始字符串
# BAD: 普通字符串,需要双重转义
pattern = "\\d+\\.\\d+"
# GOOD: 原始字符串
pattern = r"\d+\.\d+"
3. 预编译频繁使用的模式
# BAD: 每次调用都解析
def has_email(text):
return bool(re.search(r"\w+@\w+\.\w+", text))
# GOOD: 预编译
EMAIL_PATTERN = re.compile(r"\w+@\w+\.\w+")
def has_email(text):
return bool(EMAIL_PATTERN.search(text))
4. 精确匹配时使用锚点
# BAD: 部分匹配
re.search(r"\d{11}", "abc13812345678def") # 匹配成功
# GOOD: 精确匹配
re.fullmatch(r"1[3-9]\d{9}", "13812345678") # 完全匹配
5. 使用非捕获分组提高性能
# BAD: 不必要的捕获
re.findall(r"(https?)://(\w+\.\w+)", url)
# GOOD: 不需要捕获时用非捕获分组
re.findall(r"(?:https?)://(\w+\.\w+)", url)
小结
正则表达式是文本处理的瑞士军刀。本章我们系统学习了 re 模块的核心函数(match、search、findall、finditer、sub、split),元字符与字符类的基础用法,分组与捕获(包括命名分组和非捕获分组),前瞻/后顾断言,贪婪与非贪婪匹配,以及编译优化和标志使用。通过邮箱验证、手机号脱敏、URL 解析、日志解析和数据清洗等实战案例,你应该已经掌握了正则表达式在真实项目中的应用方式。最后,记得始终使用原始字符串(r"...")、预编译频繁使用的模式、避免灾难性回溯——这些最佳实践将帮助你写出高效且可靠的正则表达式。
Summary: re模块函数、元字符、分组断言、实战案例与速查表。