Python正则表达式匹配查找替换与日志解析脚本实战
Python 标准库 re 用来处理字符串匹配、查找、替换和拆分。日志、聊天记录、网页字段混杂时,正则适合先写出目标文本特征,再交给程序查找。下面从 search 开始,串起分组、替换、拆分、编译和日志解析导出 CSV。一、re.search 完成第一次匹配
如果订单号始终由一个大写字母和八位数字组成,可以写成 \d{8}。其中 代表一位大写字母,\d{8} 代表连续八位数字。正则表达式本身不负责读文件,它处理的是字符串,日志、网页内容或 CSV 字段读成字符串后都能交给它匹配。
import re
text = '订单号:A20260919'
result = re.search(r'\d+', text)
print(result)
\d 表示数字,后面的 + 表示前一个规则可以出现一次或多次。运行后会得到 Match 对象,里面保存匹配内容和位置。常用方法包括:
print(result.group())
print(result.start())
print(result.end())
print(result.span())
输出分别是匹配文本、起始下标、结束位置和区间元组。没有找到内容时 search() 返回 None,直接调用 group() 会报错,实际使用要先判断:
result = re.search(r'\d+', '订单暂未生成')
if result:
print(result.group())
else:
print('没有找到订单号')
二、为什么正则前面经常带 r
正则中经常出现反斜杠,Python 字符串也使用反斜杠表示转义字符,例如换行和制表符。两套规则写在一起,很容易把反斜杠写乱。在字符串前加 r 可以创建原始字符串:
pattern = r'\d+'
这里的 r 只影响 Python 解析字符串的方式,最终交给正则引擎的规则还是 \d+。写正则时建议默认使用原始字符串。
三、search、match 和 fullmatch 的区别
三个方法都会返回 Match 对象,区别在于从哪里开始找,以及需要匹配多少内容。search() 在整个字符串中查找第一个符合规则的位置;match() 只检查字符串开头;fullmatch() 要求整段字符串都符合规则,很适合校验格式。
import re
text = '用户ID: 1024'
result = re.search(r'\d+', text)
print(result.group())
print(re.match(r'用户', text))
print(re.match(r'\d+', text))
order_id = 'A20260919'
if re.fullmatch(r'\d{8}', order_id):
print('订单号格式正确')
else:
print('订单号格式错误')
查找一段内容时用 search(),检查开头时用 match(),校验完整格式时用 fullmatch()。
四、字符规则、集合、数量词和锚点
常见字符规则如下:\d 匹配数字,\D 匹配非数字,\w 匹配 Unicode 字母、数字和下划线,\W 匹配不属于 \w 的字符,\s 匹配空白字符,\S 匹配非空白字符,. 匹配除换行以外的任意字符。
下面从一段文字中找出价格:
text = '键盘价格 299 元,鼠标价格 129 元'
prices = re.findall(r'\d+', text)
print(prices)
输出为 ['299', '129']。注意 \w 在 Python 3 中默认能匹配中文。如果只想匹配英文字母、数字和下划线,可以明确写成 。
方括号可以列出允许出现的字符:
text = '订单 A102、B205、c308'
orders = re.findall(r'\d+', text)
print(orders)
只匹配大写英文字母,小写 c308 不符合规则。常见写法还有 、、 和 [^0-9]。方括号开头的 ^ 表示排除;放在方括号外时,含义会变成字符串开头。
数量词负责说明出现几次:* 表示零次或多次,+ 表示一次或多次,? 表示零次或一次,{m} 表示恰好 m 次,{m,n} 表示最少 m 次最多 n 次,{m,} 表示最少 m 次。用数量词检查手机号基本结构:
phone = '13812345678'
result = re.fullmatch(r'1\d{9}', phone)
print(bool(result))
1 是固定开头, 匹配第二位,\d{9} 要求后面还有九位数字。这里检查的是常见手机号格式,不能证明号码真实存在。
^ 表示字符串开头,$ 表示字符串结尾。锚点匹配的是位置,本身不占用字符。校验整段内容时,也可以直接使用 fullmatch():
username = 'ivan_2026'
if re.fullmatch(r'{3,15}', username):
print('用户名可用')
这个规则要求用户名以英文字母开头,总长度为 4 到 16 位,后面可以使用字母、数字和下划线。
五、分组、命名分组和非捕获分组
圆括号可以把一段规则放进一个分组。下面从日期中分别取出年、月、日:
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() 一次返回所有分组。分组多起来后,记编号不方便,可以给分组命名:
pattern = (
r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
)
result = re.fullmatch(pattern, date_text)
if result:
print(result.group('year'))
print(result.groupdict())
groupdict() 会把命名分组整理成字典。解析日志、订单和接口返回文本时,命名分组比数字编号更容易维护。
有时圆括号只是为了把几条规则放在一起,并不需要保存成分组,可以使用 (?:...):
text = '图片:cover.jpg,附件:report.pdf'
files = re.findall(r'\w+\.(?:jpg|png|pdf)', text)
print(files)
如果写成 (jpg|png|pdf),findall() 会优先返回捕获分组里的扩展名。这里使用非捕获分组,得到的才是完整文件名。竖线 | 表示多个规则任选一个,例如 INFO|WARNING|ERROR。
六、findall 和 finditer 批量查找
search() 只返回第一个匹配。想拿到所有结果,可以使用 findall():
text = '今天处理 A102,明天处理 A205,A308 已完成'
orders = re.findall(r'\d+', text)
print(orders)
还需要每个结果的位置时,使用 finditer():
for match in re.finditer(r'\d+', text):
print(
match.group(),
match.start(),
match.end(),
)
finditer() 返回迭代器,不会提前把所有 Match 对象放进列表。数据较多,或需要逐个查看分组和位置时,它更合适。
七、贪婪匹配和非贪婪匹配
数量词默认会尽量多匹配。下面这段 HTML 中有两个标签:
html = '<p>Python</p><p>正则表达式</p>'
print(re.findall(r'<p>.*</p>', html))
print(re.findall(r'<p>.*?</p>', html))
.* 会从第一个 <p> 一直匹配到最后一个 </p>,得到整段内容。在数量词后面加 ? 可以改成非贪婪匹配,这次得到两个独立结果。这个例子适合说明贪婪规则,真实 HTML 的结构远比它复杂。需要可靠解析网页时,应使用 HTML 解析库,不要只靠一条正则处理整份页面。
八、使用 re.sub 替换内容
re.sub() 会把符合规则的内容替换掉。处理手机号脱敏时,可以保留前三位和后四位:
import re
phone = '13812345678'
masked = re.sub(
r'(\d{3})\d{4}(\d{4})',
r'\1****\2',
phone,
)
print(masked)
替换字符串中的 \1 和 \2 引用了第一个、第二个分组。分组较多时,可以改用命名分组:
pattern = r'(?P<start>\d{3})\d{4}(?P<end>\d{4})'
masked = re.sub(
pattern,
r'\g<start>****\g<end>',
phone,
)
替换逻辑需要计算时,可以把函数传给 sub():
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():
text = 'Python,Java;Go Rust'
languages = re.split(r'[,;\s]+', text)
print(languages)
[,;\s]+ 表示一个或多个逗号、分号或空白字符。实际数据结尾可能还带分隔符,可以顺手去掉空字符串:
languages = [
item
for item in re.split(r'[,;\s]+', text)
if item
]
十、重复使用规则时先编译,常用匹配标志
同一条规则需要使用多次时,可以用 re.compile() 创建正则对象:
import re
order_pattern = re.compile(r'\d{3}')
texts = [
'新订单 A102 已创建',
'B205 等待付款',
'没有订单号',
]
for text in texts:
result = order_pattern.search(text)
if result:
print(result.group())
编译后的对象也有 search()、findall()、sub() 等方法。这样写能把规则集中放在一个位置,名称也能说明它是用来匹配什么的。
匹配标志可以改变正则的工作方式。忽略英文字母大小写时,使用 re.IGNORECASE:
text = 'Error: file not found'
result = re.search(
r'error',
text,
flags=re.IGNORECASE,
)
print(result.group())
处理多行文本时,re.MULTILINE 会让 ^ 和 $ 匹配每一行的开头和结尾:
logs = '''INFO: 启动成功
ERROR: 数据库连接失败
INFO: 开始重试'''
errors = re.findall(
r'^ERROR:.*$',
logs,
flags=re.MULTILINE,
)
print(errors)
. 默认不匹配换行。需要让它跨行匹配时,可以使用 re.DOTALL。多个标志能用 | 组合,例如 flags = re.IGNORECASE | re.MULTILINE。
十一、写正则时容易踩的坑
最常见的问题是规则写得太宽。比如 .* 什么都能接,短文本里看不出问题,换一批数据就可能吞掉多余内容。能写清字符范围时,尽量使用 、\d{4} 这类具体规则。
第二个问题是忘记处理 None。search() 和 fullmatch() 都有匹配失败的时候,调用 group() 以前先判断结果。
还要留意捕获分组对 findall() 返回值的影响:
text = 'A102 B205'
print(re.findall(r'\d+', text))
print(re.findall(r'()\d+', text))
第二条规则包含捕获分组,结果会变成 ['A', 'B']。如果括号只用来控制范围,改成 (?:)。
正则也不适合承担所有格式校验。邮箱地址、URL 和 HTML 都有许多边界情况。教程里常见的邮箱规则可以拿来提取简单地址,不能当作完整标准的验证器。
十二、综合示例:解析日志并导出 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 中写入:
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
十三、练习与总结
练习一:准备字符串“键盘 299 元,鼠标 129 元,显示器 1599 元”,使用正则提取所有价格,并把结果转换成整数列表。
练习二:订单号由两个大写字母和六位数字组成,使用 fullmatch() 分别检查 AB123456、A123456 和 ab123456。
练习三:从一段文本中提取形如 name@example.com 的简单邮箱地址,先处理常见英文字母、数字、点、下划线和连字符,不需要覆盖所有邮箱标准。
练习四:把十八位身份证号的中间八位替换成星号,只保留前六位和后四位,使用捕获分组保留两端内容。
练习五:从 report_2026-09-19.csv 中取出文件类型和日期,分别保存到命名分组 date 和 extension 中。
练习六:读取一份日志文件,使用 re.MULTILINE 找出所有以 ERROR 开头的行,再从每一行中提取时间和错误内容,并写入 CSV 文件。
学正则时没必要先背完所有符号。先把目标文本找出来,标清哪些内容固定、哪些内容会变,再把它们换成字符规则和数量词。规则写完后多准备几条正常数据和异常数据测试,比盯着一条样例反复修改更容易发现问题。
Re: Python正则表达式匹配查找替换与日志解析脚本实战
写得很系统,search、match、fullmatch 这组对比特别实用,尤其 fullmatch 做订单号、手机号、用户名这种整段格式校验很直观。r 原始字符串那段也很关键,正则里反斜杠多,不写 r 很容易踩转义的坑。命名分组和 groupdict 用来解析日志确实比记数字编号舒服,字段一多也不容易乱。你举的订单号、日期、价格、用户名例子都很贴近实际,能直接借鉴到脚本里。期待后面替换、拆分、编译和日志解析导出 CSV 的部分,尤其想看看复杂日志怎么用命名分组抽字段后整理成表格。Re: Python正则表达式匹配查找替换与日志解析脚本实战
这段整理得很系统,search、match、fullmatch 的区别讲得很直观,尤其是 fullmatch 做手机号和用户名格式校验,很适合新手建立“先写特征再匹配”的思路。原始字符串 r 的建议也很实用,方括号、数量词和锚点这些基础串得挺顺。命名分组那段如果是在论坛里显示成带转义符号的样子,复制到编辑器里记得还原成真正的尖括号,不然会报错。期待后面 compile、sub、split 和日志解析导出 CSV 的内容,尤其是命名分组配合 groupdict 直接整理成字段,应该会很像实战日志处理。感谢分享。Re: Python正则表达式匹配查找替换与日志解析脚本实战
这篇整理得挺系统,从 search 到分组命名一路串下来,对刚开始写日志解析脚本的人很友好。几个点特别有共鸣:没有匹配时先判断再 group,能少踩很多空对象的坑;正则默认用 r 字符串确实省事;search、match、fullmatch 按“查找、检查开头、校验整段”来区分,比死记规则清楚多了。命名分组加 groupdict 做字段提取也很舒服,后面导出 CSV 时把字典键当表头、值当行,可读性会比按编号取值好很多。 帖子最后好像断在命名分组比数字编号那里,有点意犹未尽,期待把日志解析导出 CSV 的完整例子补上。个人也比较想看一行日志里同时混了时间、级别、模块和消息时,分组会怎么设计得更稳、更不容易被空格或中文干扰。
页:
[1]