正则表达式
当固定字符串方法无法覆盖可变格式时,如何用一条规则完成查找、提取、校验、替换和切分?正则表达式正是用规则描述字符串格式的工具。
使用前先判断普通字符串方法能否解决问题:
- 查找固定文字,可以使用
in或str.find()。 - 判断固定开头、结尾,可以使用
startswith()、endswith()。 - 规则包含可变字符、数量或多种格式时,再考虑正则表达式。
Python 使用 re 模块执行正则规则:
import re原始字符串
正则中的反斜杠 \ 用来表示特殊规则,Python 字符串也使用反斜杠转义。为了减少两层转义,正则通常写成原始字符串:
import re
text = "课程编号:2026"
print(re.findall(r"\d+", text))这里的 r"\d+" 表示匹配一个或多个数字。没有 r 时,需要额外转义反斜杠:
re.findall("\\d+", text)两种写法结果相同,但原始字符串更容易阅读。
原始字符串只解决 Python 字符串的转义问题,不会取消正则语法。如果需要匹配普通的点,仍然要写成 r"\."。
匹配规则基础
字符与字符集
| 规则 | 含义 |
|---|---|
abc | 按字面匹配字符串 abc |
. | 匹配除换行符外的任意一个字符 |
[abc] | 匹配 a、b、c 中的一个 |
[a-z] | 匹配一个小写英文字母 |
[^0-9] | 匹配一个非 ASCII 数字字符 |
\d | 匹配一个 Unicode 十进制数字 |
[0-9] | 匹配一个 ASCII 数字 |
\s | 匹配一个空白字符 |
\w | 匹配一个 Unicode 字母、数字或下划线字符 |
Python 3 默认按 Unicode 规则解释 \d 和 \w:
import re
print(re.findall(r"\d+", "编号123"))
print(re.findall(r"\w+", "中文_ABC"))运行结果:
['123']
['中文_ABC']如果规则只允许英文和 ASCII 数字,直接写清楚范围通常更直观:
account_pattern = r"[A-Za-z0-9_]+"数量词
数量词控制前一项出现多少次:
| 规则 | 含义 |
|---|---|
* | 0 次或多次 |
+ | 1 次或多次 |
? | 0 次或 1 次 |
{m} | 正好 m 次 |
{m,n} | m 到 n 次 |
例如,下面的规则匹配连续的 ASCII 数字:
import re
text = "订单 A-2026-001,共 3 件商品"
print(re.findall(r"[0-9]", text))
print(re.findall(r"[0-9]+", text))第一条规则逐个返回数字,第二条规则把连续数字作为一个结果返回。
位置边界
| 规则 | 含义 |
|---|---|
^ | 字符串或一行的开头 |
$ | 字符串或一行的结尾 |
^ 和 $ 适合定位行。校验整个字符串时,后面介绍的 fullmatch() 通常更加明确。
匹配查找
首个匹配:search()
re.search() 从左到右查找第一个匹配结果,成功时返回 Match 对象,失败时返回 None:
import re
text = "订单号:A-2026-001"
result = re.search(r"[A-Z]-[0-9]{4}-[0-9]{3}", text)
if result is None:
print("没有找到订单号")
else:
print(result.group())不要在没有判断的情况下直接调用 search(...).group(),因为没有匹配结果时会产生 AttributeError。
全部匹配:findall()
re.findall() 返回所有匹配结果组成的列表:
import re
text = "订单 A-2026-001 和 B-2026-015 已完成"
order_ids = re.findall(r"[A-Z]-[0-9]{4}-[0-9]{3}", text)
print(order_ids)下面是一条适用于课程数据的简化邮箱规则:
text = "联系 alice@example.com 或 teacher@school.cn"
emails = re.findall(
r"[A-Za-z0-9._+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}",
text,
)
print(emails)这条规则用于练习提取,不代表覆盖所有合法邮箱地址。真实系统的邮箱校验还要结合业务要求。
匹配迭代:finditer()
re.finditer() 返回 Match 对象,可以读取每次匹配的内容和位置:
import re
text = "A-2026-001,B-2026-015"
for match in re.finditer(r"[A-Z]-[0-9]{4}-[0-9]{3}", text):
print(match.group(), match.span())数据量较大,或者还需要位置、分组信息时,可以使用 finditer()。
完整字符串校验
re.fullmatch() 要求整个字符串都符合规则,适合校验编号、账号等固定格式:
import re
pattern = r"PY-[0-9]{4}-[0-9]{3}"
course_ids = ["PY-2026-001", "PY-26-001", "编号PY-2026-001"]
for course_id in course_ids:
if re.fullmatch(pattern, course_id):
print(f"{course_id}:格式正确")
else:
print(f"{course_id}:格式错误")其中只有 PY-2026-001 能够通过完整校验。
re.match() 只要求从字符串开头匹配,不要求消耗完整字符串。因此,“从开头查找”使用 match(),“校验完整格式”优先使用 fullmatch()。
文本替换与切分
替换:sub()
re.sub() 把所有匹配内容替换成指定字符串:
import re
text = "订单号:A-2026-001"
masked_text = re.sub(r"[0-9]", "*", text)
print(masked_text)替换结果是 订单号:A-****-***。需要限制替换次数时,可以使用 count 参数。
切分:split()
普通 str.split() 只能方便地处理一种固定分隔符。re.split() 可以同时处理多种分隔符:
import re
text = "Python,Java;Go,Rust;C"
languages = re.split(r"[,,;;]", text)
print(languages)运行结果:
['Python', 'Java', 'Go', 'Rust', 'C']分组与字段提取
圆括号 () 可以把规则的一部分组成分组:
import re
result = re.fullmatch(r"([0-9]{4})-([0-9]{2})-([0-9]{2})", "2026-09-12")
if result is not None:
print(result.group(0))
print(result.group(1))
print(result.groups())其中:
group(0)是完整匹配内容。group(1)是第一个捕获分组。groups()返回所有捕获分组。
命名分组
分组较多时,可以通过 (?P<名称>规则) 设置名称:
import re
pattern = r"(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})"
result = re.fullmatch(pattern, "2026-09-12")
if result is not None:
print(result.group("year"))
print(result.groupdict())命名分组比记忆第几个括号更容易维护。
捕获分组与非捕获分组
findall() 遇到捕获分组时,会优先返回分组内容:
import re
text = "alice@example.com,teacher@school.cn"
print(re.findall(r"[A-Za-z]+@(example|school)\.(com|cn)", text))运行结果是域名分组组成的元组,而不是完整邮箱:
[('example', 'com'), ('school', 'cn')]如果括号只用于组合规则,不需要提取,可以使用非捕获分组 (?:...):
emails = re.findall(
r"[A-Za-z]+@(?:example|school)\.(?:com|cn)",
text,
)
print(emails)贪婪与非贪婪模式
数量词默认采用贪婪模式,会尽量多匹配:
import re
text = 'title="Python" category="编程"'
print(re.findall(r'".*"', text))
print(re.findall(r'".*?"', text))运行结果:
['"Python" category="编程"']
['"Python"', '"编程"']在数量词后增加 ? 会变成非贪婪模式,例如 *?、+? 和 {m,n}?。
非贪婪不是“更加正确”,而是选择尽可能短的匹配。能够写出明确字符范围时,优先减少对 .* 的依赖。
正则适合处理规则明确的文本,不适合代替专业的 HTML、JSON 或 CSV 解析器。
规则编译与匹配标志
同一条规则需要重复使用时,可以通过 re.compile() 创建 Pattern 对象:
import re
order_pattern = re.compile(r"[A-Z]-[0-9]{4}-[0-9]{3}")
print(order_pattern.findall("A-2026-001,B-2026-015"))
print(order_pattern.fullmatch("C-2026-008"))预编译能够集中管理规则,也能直接调用 search()、findall()、fullmatch() 等方法。简单规则只使用一次时,直接调用 re 模块函数即可。
常用匹配标志如下:
| 标志 | 作用 |
|---|---|
re.IGNORECASE | 忽略英文字母大小写 |
re.MULTILINE | 让 ^ 和 $ 匹配每一行的开头和结尾 |
re.DOTALL | 让 . 也能匹配换行符 |
word_pattern = re.compile(r"python", re.IGNORECASE)
print(word_pattern.findall("Python PYTHON python"))只在规则确实需要时添加标志,避免无意扩大匹配范围。
综合案例:解析日志文本
需求: 解析上一章生成的日志,提取时间、级别、模块名和消息,并统计各级别日志数量。
功能分析: 使用 compile() 统一管理规则,使用 fullmatch() 校验每一行,使用命名分组提取字段;遇到格式不符合规则的行时保留提示并继续处理。
上一章生成的日志具有固定格式:
2026-09-12 10:30:00 | INFO | student_service | 已添加学生:张三准备下面两个文件:
01-log-parser/
├── app.log
└── main.py在 main.py 同级创建 app.log,放入以下日志作为固定输入:
2026-09-12 10:30:00 | INFO | __main__ | 学生程序启动
2026-09-12 10:30:00 | INFO | student_service | 已添加学生:张三
2026-09-12 10:30:00 | DEBUG | student_service | 当前学生数量:1
2026-09-12 10:30:00 | WARNING | student_service | 添加学生失败:姓名不能为空
2026-09-12 10:30:00 | INFO | __main__ | 学生程序结束使用命名分组拆出时间、级别、模块名和消息:
import os
import re
LOG_PATTERN = re.compile(
r"(?P<time>[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})"
r" \| (?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL)"
r" \| (?P<module>[A-Za-z_][A-Za-z0-9_.]*)"
r" \| (?P<message>.+)"
)
script_dir = os.path.dirname(os.path.abspath(__file__))
log_path = os.path.join(script_dir, "app.log")
level_counts = {}
with open(log_path, encoding="utf-8") as log_file:
for line in log_file:
result = LOG_PATTERN.fullmatch(line.rstrip("\n"))
if result is None:
print(f"无法解析:{line.rstrip()}")
continue
data = result.groupdict()
level = data["level"]
level_counts[level] = level_counts.get(level, 0) + 1
print(f"{level} | {data['module']} | {data['message']}")
print("级别统计:")
for level, count in sorted(level_counts.items()):
print(f"{level}: {count}")运行结果:
INFO | __main__ | 学生程序启动
INFO | student_service | 已添加学生:张三
DEBUG | student_service | 当前学生数量:1
WARNING | student_service | 添加学生失败:姓名不能为空
INFO | __main__ | 学生程序结束
级别统计:
DEBUG: 1
INFO: 3
WARNING: 1案例使用 compile() 统一管理规则,使用 fullmatch() 校验每一行,并通过命名分组提取字段。
常见问题
- 优先使用原始字符串,例如
r"\d+"。 search()可能返回None,调用group()前先判断。- 提取部分内容使用
search()或findall(),校验完整字符串使用fullmatch()。 *和?允许出现 0 次,规则可能匹配到空字符串。\d、\w默认遵循 Unicode 规则,只允许 ASCII 时要明确写出范围。- 不要用一条过于复杂的正则代替清楚的字符串处理或专业解析器。
- 邮箱、手机号等真实业务规则会变化,课程中的简化规则不能直接作为生产校验规则。
小结
- 正则适合处理具有明确重复规则的文本,简单需求优先使用字符串方法。
- 原始字符串可以减少 Python 字符串和正则表达式的双重转义。
- 字符集、数量词、位置和分组共同组成匹配规则。
search()查找第一个结果,findall()提取全部结果,fullmatch()校验完整字符串。sub()用于替换,split()用于按多种规则切分。- 命名分组可以提高复杂规则的可读性。
compile()适合集中管理需要重复使用的规则。
练习
- 从文本中提取所有格式为
C-2026-001的课程编号。 - 使用
fullmatch()校验账号只能包含英文字母、数字和下划线,长度为 6~16 位。 - 使用
sub()隐藏订单编号中的所有数字。 - 使用
split()按中英文逗号、分号切分一组标签。 - 为日期规则添加命名分组,分别输出年、月、日。
- 修改综合案例,只输出
WARNING和ERROR日志。