Python 正则表达式(十四):文本匹配、查找与替换

相信大家平时都见过这样的内容,内容里明明有我们需要的信息,但它们都混杂在一大段文字里。比如日志中的时间和 IP,聊天记录里的手机号和邮箱,没法像 JSON 或 CSV 那样直接按字段读取。

思维导图

这种时候,正则表达式就很适合用来处理文本匹配。今天我们从第一次匹配开始,逐步讲清楚查找、分组、替换和拆分,最后再写一个日志解析脚本。

一、正则表达式是什么

正则表达式可以理解成一套文本规则。我们先写出目标内容的特征,再让程序按这套规则去查找。

例如,下面这段文字中包含一个订单号:

python 复制代码
text = "订单号:A20260919,状态:已发货"

如果订单号始终由一个大写字母和八位数字组成,它的规则可以写成:

复制代码
[A-Z]\d{8}

其中 [A-Z] 代表一位大写字母,\d{8} 代表连续八位数字。正则表达式本身并不负责读取文件,它处理的是字符串。日志文件、网页内容或者 CSV 字段,读成字符串以后都能交给它匹配。

二、使用 re.search() 完成第一次匹配

Python 的正则功能放在标准库 re 中,不需要另外安装。先用 search() 找出订单号中的数字:

python 复制代码
import re

text = "订单号:A20260919"
result = re.search(r"\d+", text)

print(result)

\d 表示数字,后面的 + 表示前一个规则可以出现一次或多次。运行以后会得到一个 Match 对象:

复制代码
<re.Match object; span=(5, 13), match='20260919'>

Match 对象里保存了匹配内容和位置。一般会通过这些方法读取:

python 复制代码
print(result.group())
print(result.start())
print(result.end())
print(result.span())

输出结果:

复制代码
20260919
5
13
(5, 13)

start() 是起始下标,end() 是结束位置,span() 会把两者放进一个元组。

没有找到内容时,search() 返回 None。直接调用 group() 会报错,实际使用时要先判断:

python 复制代码
result = re.search(r"\d+", "订单暂未生成")

if result:
    print(result.group())
else:
    print("没有找到订单号")

三、为什么正则前面经常带 r

正则表达式中经常出现反斜杠。Python 字符串也使用反斜杠表示转义字符,例如 \n 是换行,\t 是制表符。两套规则写在一起,很容易把反斜杠写乱。

在字符串前加 r,可以创建原始字符串:

python 复制代码
pattern = r"\d+"

这里的 r 只影响 Python 解析字符串的方式,最终交给正则引擎的规则还是 \d+。写正则时建议默认使用原始字符串。

需要匹配反斜杠本身时,正则中仍然要写两个反斜杠:

python 复制代码
text = r"C:\Users\ivan\notes.txt"
result = re.search(r"\\Users\\", text)

print(result.group())

四、search、match 和 fullmatch 有什么区别

这三个方法都会返回 Match 对象,区别在于从哪里开始找,以及需要匹配多少内容。

search() 会在整个字符串中查找第一个符合规则的位置:

python 复制代码
import re

text = "用户ID: 1024"

result = re.search(r"\d+", text)
print(result.group())

match() 只检查字符串开头:

python 复制代码
print(re.match(r"用户", text))
print(re.match(r"\d+", text))

第二次返回 None,因为数字不在开头。

fullmatch() 要求整段字符串都符合规则,很适合校验格式:

python 复制代码
order_id = "A20260919"

if re.fullmatch(r"[A-Z]\d{8}", order_id):
    print("订单号格式正确")
else:
    print("订单号格式错误")

查找一段内容时用 search(),检查开头时用 match(),校验完整格式时用 fullmatch()。

五、常用字符规则

正则中最常见的几个规则如下:

规则 含义 示例
\d 数字 0 到 9
\D 非数字 字母、中文、符号
\w Unicode 字母、数字和下划线 user_01、中文
\W 不属于 \w 的字符 空格和多数标点
\s 空白字符 空格、制表符、换行
\S 非空白字符 普通可见文字
. 除换行以外的任意字符 字母、数字、标点

下面从一段文字中找出价格:

python 复制代码
text = "键盘价格 299 元,鼠标价格 129 元"
prices = re.findall(r"\d+", text)

print(prices)

输出结果:

python 复制代码
['299', '129']

\w 在 Python 3 中默认能匹配中文。如果只想匹配英文字母、数字和下划线,可以明确写成 [A-Za-z0-9_],不要把两者当成完全相同的规则。

六、使用字符集合限制范围

方括号可以列出允许出现的字符:

python 复制代码
text = "订单 A102、B205、c308"
orders = re.findall(r"[A-Z]\d+", text)

print(orders)

输出:

python 复制代码
['A102', 'B205']

[A-Z] 只匹配大写英文字母,小写的 c308 不符合规则。常见写法还有:

复制代码
[abc]        匹配 a、b 或 c
[0-9]        匹配一位数字
[a-zA-Z]     匹配一位英文字母
[^0-9]       匹配非数字字符

方括号开头的 ^ 表示排除。它放在方括号外时,含义会变成字符串开头,后面会单独讲。

七、数量词控制出现次数

字符规则只说明"匹配什么",数量词负责说明"出现几次"。

数量词 含义
* 零次或多次
+ 一次或多次
? 零次或一次
{m} 恰好 m 次
{m,n} 最少 m 次,最多 n 次
{m,} 最少 m 次

使用数量词检查手机号的基本结构:

python 复制代码
phone = "13812345678"
result = re.fullmatch(r"1[3-9]\d{9}", phone)

print(bool(result))

1 是固定开头,[3-9] 匹配第二位,\d{9} 要求后面还有九位数字。这里检查的是常见手机号格式,不能证明号码真实存在。

八、使用锚点限制开头和结尾

^ 表示字符串开头,$ 表示字符串结尾:

python 复制代码
text = "ERROR: 登录失败"

print(bool(re.search(r"^ERROR", text)))
print(bool(re.search(r"失败$", text)))

两次结果都是 True。锚点匹配的是位置,本身不会占用字符。

校验一整段内容时,也可以直接使用 fullmatch():

python 复制代码
username = "ivan_2026"

if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{3,15}", username):
    print("用户名可用")

这个规则要求用户名以英文字母开头,总长度为 4 到 16 位,后面可以使用字母、数字和下划线。

九、分组:一次取出多个字段

圆括号可以把一段规则放进一个分组。下面从日期中分别取出年、月、日:

python 复制代码
import re

date_text = "2026-09-19"
result = re.fullmatch(
    r"(\d{4})-(\d{2})-(\d{2})",
    date_text,
)

if result:
    print(result.group(0))
    print(result.group(1))
    print(result.group(2))
    print(result.group(3))
    print(result.groups())

group(0) 是完整匹配,后面的编号按左括号出现的顺序排列。groups() 会一次返回所有分组:

复制代码
2026-09-19
2026
09
19
('2026', '09', '19')

分组多起来以后,记编号很不方便。这时可以给分组命名:

python 复制代码
pattern = (
    r"(?P<year>\d{4})-"
    r"(?P<month>\d{2})-"
    r"(?P<day>\d{2})"
)

result = re.fullmatch(pattern, date_text)

if result:
    print(result.group("year"))
    print(result.groupdict())

groupdict() 会把命名分组整理成字典:

python 复制代码
{'year': '2026', 'month': '09', 'day': '19'}

解析日志、订单和接口返回文本时,命名分组比数字编号更容易维护。

十、只分组,不保存内容

有时圆括号只是为了把几条规则放在一起,并不需要把内容保存成分组。可以使用 (?:...):

python 复制代码
text = "图片:cover.jpg,附件:report.pdf"
files = re.findall(r"\w+\.(?:jpg|png|pdf)", text)

print(files)

输出:

python 复制代码
['cover.jpg', 'report.pdf']

如果写成 (jpg|png|pdf),findall() 会优先返回捕获分组里的扩展名。这里使用非捕获分组,得到的才是完整文件名。

竖线 | 表示多个规则任选一个:

复制代码
INFO|WARNING|ERROR

它能匹配三种日志级别中的任意一种。

十一、findall 和 finditer 批量查找

search() 只返回第一个匹配。想拿到所有结果,可以使用 findall():

python 复制代码
text = "今天处理 A102,明天处理 A205,A308 已完成"
orders = re.findall(r"[A-Z]\d+", text)

print(orders)

输出:

python 复制代码
['A102', 'A205', 'A308']

还需要每个结果的位置时,使用 finditer():

python 复制代码
for match in re.finditer(r"[A-Z]\d+", text):
    print(
        match.group(),
        match.start(),
        match.end(),
    )

finditer() 返回一个迭代器,不会提前把所有 Match 对象放进列表。数据比较多,或者需要逐个查看分组和位置时,它更合适。

十二、贪婪匹配和非贪婪匹配

数量词默认会尽量多匹配。下面这段 HTML 中有两个标签:

python 复制代码
html = "<p>Python</p><p>正则表达式</p>"
print(re.findall(r"<p>.*</p>", html))

.* 会从第一个 <p> 一直匹配到最后一个 </p>:

python 复制代码
['<p>Python</p><p>正则表达式</p>']

在数量词后面加 ?,可以改成非贪婪匹配:

python 复制代码
print(re.findall(r"<p>.*?</p>", html))

这次得到两个结果:

python 复制代码
['<p>Python</p>', '<p>正则表达式</p>']

这个例子适合说明贪婪规则,真实 HTML 的结构远比它复杂。需要可靠地解析网页时,应该使用 HTML 解析库,不要只靠一条正则处理整份页面。

十三、使用 re.sub() 替换内容

re.sub() 会把符合规则的内容替换掉。处理手机号脱敏时,可以保留前三位和后四位:

python 复制代码
import re

phone = "13812345678"
masked = re.sub(
    r"(\d{3})\d{4}(\d{4})",
    r"\1****\2",
    phone,
)

print(masked)

输出:

复制代码
138****5678

替换字符串中的 \1 和 \2 引用了第一个、第二个分组。分组较多时,可以改用命名分组:

python 复制代码
pattern = r"(?P<start>\d{3})\d{4}(?P<end>\d{4})"
masked = re.sub(
    pattern,
    r"\g<start>****\g<end>",
    phone,
)

替换逻辑需要计算时,可以把函数传给 sub():

python 复制代码
text = "商品价格 299 元,优惠价 199 元"

def add_currency(match):
    price = match.group()
    return f"¥{price}"

result = re.sub(r"\d+", add_currency, text)
print(result)

十四、使用 re.split() 拆分内容

普通字符串的 split() 只能方便地处理一种固定分隔符。文本中混用了逗号、分号和空格时,可以使用 re.split():

python 复制代码
text = "Python,Java;Go Rust"
languages = re.split(r"[,;\s]+", text)

print(languages)

输出:

python 复制代码
['Python', 'Java', 'Go', 'Rust']

[,;\s]+ 表示一个或多个逗号、分号或空白字符。实际数据结尾可能还带分隔符,可以顺手去掉空字符串:

python 复制代码
languages = [
    item
    for item in re.split(r"[,;\s]+", text)
    if item
]

十五、重复使用规则时先编译

同一条规则需要使用多次时,可以用 re.compile() 创建正则对象:

python 复制代码
import re

order_pattern = re.compile(r"[A-Z]\d{3}")

texts = [
    "新订单 A102 已创建",
    "B205 等待付款",
    "没有订单号",
]

for text in texts:
    result = order_pattern.search(text)

    if result:
        print(result.group())

编译后的对象也有 search()、findall()、sub() 等方法。这样写能把规则集中放在一个位置,名称也能说明它是用来匹配什么的。

十六、常用匹配标志

匹配标志可以改变正则的工作方式。忽略英文字母大小写时,使用 re.IGNORECASE:

python 复制代码
text = "Error: file not found"
result = re.search(
    r"error",
    text,
    flags=re.IGNORECASE,
)

print(result.group())

处理多行文本时,re.MULTILINE 会让 ^ 和 $ 匹配每一行的开头和结尾:

python 复制代码
logs = """INFO: 启动成功
ERROR: 数据库连接失败
INFO: 开始重试"""

errors = re.findall(
    r"^ERROR:.*$",
    logs,
    flags=re.MULTILINE,
)

print(errors)

. 默认不匹配换行。需要让它跨行匹配时,可以使用 re.DOTALL。多个标志能用 | 组合:

python 复制代码
flags = re.IGNORECASE | re.MULTILINE

十七、写正则时容易踩的坑

最常见的问题是规则写得太宽。比如 .* 什么都能接,短文本里看不出问题,换一批数据就可能吞掉多余内容。能写清字符范围时,尽量使用 [A-Z]、\d{4} 这类具体规则。

第二个问题是忘记处理 None。search() 和 fullmatch() 都有匹配失败的时候,调用 group() 以前先判断结果。

还要留意捕获分组对 findall() 返回值的影响:

python 复制代码
text = "A102 B205"

print(re.findall(r"[A-Z]\d+", text))
print(re.findall(r"([A-Z])\d+", text))

第二条规则包含捕获分组,结果会变成 ['A', 'B']。如果括号只用来控制范围,改成 (?:[A-Z])。

正则也不适合承担所有格式校验。邮箱地址、URL 和 HTML 都有许多边界情况。教程里常见的邮箱规则可以拿来提取简单地址,不能当作完整标准的验证器。

十八、综合示例:解析日志并导出 CSV

现在把正则和上一章的 CSV 连起来。我们准备一份 app.log:

复制代码
2026-09-19 10:15:32 INFO user=ivan action=login ip=192.168.1.10
2026-09-19 10:16:03 WARNING user=alice action=download ip=10.0.0.5
这是一行无法识别的内容
2026-09-19 10:18:45 ERROR user=bob action=upload ip=172.16.3.8

项目结构如下:

复制代码
regex_demo/
├─ parse_log.py
├─ data/
│  └─ app.log
└─ output/

在 parse_log.py 中写入:

python 复制代码
from pathlib import Path
import csv
import re

base_dir = Path(__file__).resolve().parent
log_file = base_dir / "data" / "app.log"
output_dir = base_dir / "output"
csv_file = output_dir / "log_report.csv"

output_dir.mkdir(parents=True, exist_ok=True)

log_pattern = re.compile(
    r"^(?P<time>\d{4}-\d{2}-\d{2} "
    r"\d{2}:\d{2}:\d{2})\s+"
    r"(?P<level>INFO|WARNING|ERROR)\s+"
    r"user=(?P<user>\w+)\s+"
    r"action=(?P<action>\w+)\s+"
    r"ip=(?P<ip>(?:\d{1,3}\.){3}\d{1,3})$"
)

records = []
failed_lines = []

with log_file.open("r", encoding="utf-8") as file:
    for line_number, line in enumerate(file, start=1):
        line = line.strip()

        if not line:
            continue

        result = log_pattern.fullmatch(line)

        if result:
            records.append(result.groupdict())
        else:
            failed_lines.append((line_number, line))

fieldnames = [
    "time",
    "level",
    "user",
    "action",
    "ip",
]

with csv_file.open(
    "w",
    encoding="utf-8-sig",
    newline="",
) as file:
    writer = csv.DictWriter(
        file,
        fieldnames=fieldnames,
    )
    writer.writeheader()
    writer.writerows(records)

print(f"成功解析:{len(records)} 行")
print(f"解析失败:{len(failed_lines)} 行")
print(f"CSV 文件:{csv_file}")

for line_number, content in failed_lines:
    print(f"第 {line_number} 行无法识别:{content}")

这里使用命名分组直接生成字典,再交给 csv.DictWriter() 写入文件。无法匹配的行没有被悄悄丢掉,程序会保存行号和原内容,方便回头检查规则或原始数据。

IP 的规则 (?:\d{1,3}\.){3}\d{1,3} 只检查"由四段一到三位数字组成"的外形,没有继续判断每一段是否处于 0 到 255。需要严格验证 IP 时,可以把匹配结果交给标准库 ipaddress 再检查。

生成的 log_report.csv 如下:

复制代码
time,level,user,action,ip
2026-09-19 10:15:32,INFO,ivan,login,192.168.1.10
2026-09-19 10:16:03,WARNING,alice,download,10.0.0.5
2026-09-19 10:18:45,ERROR,bob,upload,172.16.3.8

!10-log-pipeline.png

十九、练习

练习一:提取所有价格

准备字符串 "键盘 299 元,鼠标 129 元,显示器 1599 元",使用正则提取所有价格,并把结果转换成整数列表。

练习二:检查订单号

订单号由两个大写字母和六位数字组成。使用 fullmatch() 分别检查 AB123456、A123456 和 ab123456。

练习三:提取邮箱

从一段文本中提取形如 name@example.com 的简单邮箱地址。先处理常见英文字母、数字、点、下划线和连字符,不需要覆盖所有邮箱标准。

练习四:隐藏身份证号

把十八位身份证号的中间八位替换成星号,只保留前六位和后四位。使用捕获分组保留两端内容。

练习五:解析文件名

从 report_2026-09-19.csv 中取出文件类型和日期,分别保存到命名分组 date 和 extension 中。

练习六:统计错误日志

读取一份日志文件,使用 re.MULTILINE 找出所有以 ERROR 开头的行。再从每一行中提取时间和错误内容,并写入 CSV 文件。

学正则时没必要先背完所有符号。先把目标文本找出来,标清哪些内容固定、哪些内容会变,再把它们换成字符规则和数量词。规则写完后多准备几条正常数据和异常数据测试,比盯着一条样例反复修改更容易发现问题。

相关推荐
夜雪一千1 小时前
Python URL编码踩坑:单层编码、双重编码实战解析
开发语言·python
茶栀(*´I`*)1 小时前
【Python数据可视化】Matplotlib从零到精通:图表核心解剖、多子图布局与三大经典图表实战
python·信息可视化·matplotlib
孙启超1 小时前
【AI开发之Rust】第 21 课:双端集成与出包 —— Android(.so→AAR)与 iOS(xcframework)
开发语言·后端·rust
SelectDB技术团队1 小时前
Agent Trace 数据底座建设:宽表建模、全文检索与成本聚合的配置与验证步骤
大数据·python·clickhouse·elk·elasticsearch·全文检索·复杂查询
Brilliantwxx1 小时前
【STM32】 __weak 弱引用、中断标志位、异常处理与 HAL_Delay 优先级
开发语言·stm32·单片机·嵌入式硬件
驭渊的小故事1 小时前
SpringBoot 配置文件详解:properties 与 yml 从入门到实战
java·开发语言·笔记
计算机源码社1 小时前
【27届大数据毕设】基于Spark的TikTok短视频属性分享与互动比率可视化分析系统 基于机器学习的TikTok短视频认证属性与互动效能关联分析
大数据·hadoop·python·机器学习·spark·毕业设计·课程设计
北冥有鱼被烹2 小时前
Linux timeout 命令完全指南:指定固定时间并发送固定信号
python
郝学胜-神的一滴2 小时前
游戏引擎原理与实践 02:揭开3A游戏背后的技术面纱
开发语言·计算机网络·程序人生·游戏·游戏引擎