Skip to content

正则表达式 ​

当固定字符串方法无法覆盖可变格式时,如何用一条规则完成查找、提取、校验、替换和切分?正则表达式正是用规则描述字符串格式的工具。

使用前先判断普通字符串方法能否解决问题:

  • 查找固定文字,可以使用 in 或 str.find()。
  • 判断固定开头、结尾,可以使用 startswith()、endswith()。
  • 规则包含可变字符、数量或多种格式时,再考虑正则表达式。

正则表达式处理流程

Python 使用 re 模块执行正则规则:

python
import re

原始字符串 ​

正则中的反斜杠 \ 用来表示特殊规则,Python 字符串也使用反斜杠转义。为了减少两层转义,正则通常写成原始字符串:

python
import re

text = "课程编号:2026"
print(re.findall(r"\d+", text))

这里的 r"\d+" 表示匹配一个或多个数字。没有 r 时,需要额外转义反斜杠:

python
re.findall("\\d+", text)

两种写法结果相同,但原始字符串更容易阅读。

原始字符串只解决 Python 字符串的转义问题,不会取消正则语法。如果需要匹配普通的点,仍然要写成 r"\."。

Python 字符串与正则表达式双重转义对比

匹配规则基础 ​

字符与字符集 ​

规则含义
abc按字面匹配字符串 abc
.匹配除换行符外的任意一个字符
[abc]匹配 a、b、c 中的一个
[a-z]匹配一个小写英文字母
[^0-9]匹配一个非 ASCII 数字字符
\d匹配一个 Unicode 十进制数字
[0-9]匹配一个 ASCII 数字
\s匹配一个空白字符
\w匹配一个 Unicode 字母、数字或下划线字符

Python 3 默认按 Unicode 规则解释 \d 和 \w:

python
import re

print(re.findall(r"\d+", "编号123"))
print(re.findall(r"\w+", "中文_ABC"))

运行结果:

text
['123']
['中文_ABC']

如果规则只允许英文和 ASCII 数字,直接写清楚范围通常更直观:

python
account_pattern = r"[A-Za-z0-9_]+"

数量词 ​

数量词控制前一项出现多少次:

规则含义
*0 次或多次
+1 次或多次
?0 次或 1 次
{m}正好 m 次
{m,n}m 到 n 次

例如,下面的规则匹配连续的 ASCII 数字:

python
import re

text = "订单 A-2026-001,共 3 件商品"

print(re.findall(r"[0-9]", text))
print(re.findall(r"[0-9]+", text))

第一条规则逐个返回数字,第二条规则把连续数字作为一个结果返回。

位置边界 ​

规则含义
^字符串或一行的开头
$字符串或一行的结尾

^ 和 $ 适合定位行。校验整个字符串时,后面介绍的 fullmatch() 通常更加明确。

匹配查找 ​

re.search() 从左到右查找第一个匹配结果,成功时返回 Match 对象,失败时返回 None:

python
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() 返回所有匹配结果组成的列表:

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

下面是一条适用于课程数据的简化邮箱规则:

python
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 对象,可以读取每次匹配的内容和位置:

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

正则匹配 API 对比

完整字符串校验 ​

re.fullmatch() 要求整个字符串都符合规则,适合校验编号、账号等固定格式:

python
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() 把所有匹配内容替换成指定字符串:

python
import re

text = "订单号:A-2026-001"
masked_text = re.sub(r"[0-9]", "*", text)

print(masked_text)

替换结果是 订单号:A-****-***。需要限制替换次数时,可以使用 count 参数。

切分:split() ​

普通 str.split() 只能方便地处理一种固定分隔符。re.split() 可以同时处理多种分隔符:

python
import re

text = "Python,Java;Go,Rust;C"
languages = re.split(r"[,,;;]", text)

print(languages)

运行结果:

text
['Python', 'Java', 'Go', 'Rust', 'C']

分组与字段提取 ​

圆括号 () 可以把规则的一部分组成分组:

python
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<名称>规则) 设置名称:

python
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() 遇到捕获分组时,会优先返回分组内容:

python
import re

text = "alice@example.com,teacher@school.cn"

print(re.findall(r"[A-Za-z]+@(example|school)\.(com|cn)", text))

运行结果是域名分组组成的元组,而不是完整邮箱:

text
[('example', 'com'), ('school', 'cn')]

如果括号只用于组合规则,不需要提取,可以使用非捕获分组 (?:...):

python
emails = re.findall(
    r"[A-Za-z]+@(?:example|school)\.(?:com|cn)",
    text,
)
print(emails)

findall 分组捕获行为与非捕获分组

贪婪与非贪婪模式 ​

数量词默认采用贪婪模式,会尽量多匹配:

python
import re

text = 'title="Python" category="编程"'

print(re.findall(r'".*"', text))
print(re.findall(r'".*?"', text))

运行结果:

text
['"Python" category="编程"']
['"Python"', '"编程"']

贪婪与非贪婪匹配

在数量词后增加 ? 会变成非贪婪模式,例如 *?、+? 和 {m,n}?。

非贪婪不是“更加正确”,而是选择尽可能短的匹配。能够写出明确字符范围时,优先减少对 .* 的依赖。

正则适合处理规则明确的文本,不适合代替专业的 HTML、JSON 或 CSV 解析器。

规则编译与匹配标志 ​

同一条规则需要重复使用时,可以通过 re.compile() 创建 Pattern 对象:

python
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让 . 也能匹配换行符
python
word_pattern = re.compile(r"python", re.IGNORECASE)
print(word_pattern.findall("Python PYTHON python"))

只在规则确实需要时添加标志,避免无意扩大匹配范围。

综合案例:解析日志文本 ​

需求: 解析上一章生成的日志,提取时间、级别、模块名和消息,并统计各级别日志数量。

功能分析: 使用 compile() 统一管理规则,使用 fullmatch() 校验每一行,使用命名分组提取字段;遇到格式不符合规则的行时保留提示并继续处理。

上一章生成的日志具有固定格式:

text
2026-09-12 10:30:00 | INFO | student_service | 已添加学生:张三

准备下面两个文件:

text
01-log-parser/
├── app.log
└── main.py

在 main.py 同级创建 app.log,放入以下日志作为固定输入:

text
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__ | 学生程序结束

使用命名分组拆出时间、级别、模块名和消息:

python
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}")

运行结果:

text
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() 适合集中管理需要重复使用的规则。

练习 ​

  1. 从文本中提取所有格式为 C-2026-001 的课程编号。
  2. 使用 fullmatch() 校验账号只能包含英文字母、数字和下划线,长度为 6~16 位。
  3. 使用 sub() 隐藏订单编号中的所有数字。
  4. 使用 split() 按中英文逗号、分号切分一组标签。
  5. 为日期规则添加命名分组,分别输出年、月、日。
  6. 修改综合案例,只输出 WARNING 和 ERROR 日志。