查看: 1301|回复: 3

Python正则表达式匹配查找替换与日志解析脚本实战

[复制链接]
发表于 昨天 10:00 | 显示全部楼层 |阅读模式
Python 标准库 re 用来处理字符串匹配、查找、替换和拆分。日志、聊天记录、网页字段混杂时,正则适合先写出目标文本特征,再交给程序查找。下面从 search 开始,串起分组、替换、拆分、编译和日志解析导出 CSV。

一、re.search 完成第一次匹配

如果订单号始终由一个大写字母和八位数字组成,可以写成 [A-Z]\d{8}。其中 [A-Z] 代表一位大写字母,\d{8} 代表连续八位数字。正则表达式本身不负责读文件,它处理的是字符串,日志、网页内容或 CSV 字段读成字符串后都能交给它匹配。
  1. import re
  2. text = '订单号:A20260919'
  3. result = re.search(r'\d+', text)
  4. print(result)
复制代码

\d 表示数字,后面的 + 表示前一个规则可以出现一次或多次。运行后会得到 Match 对象,里面保存匹配内容和位置。常用方法包括:
  1. print(result.group())
  2. print(result.start())
  3. print(result.end())
  4. print(result.span())
复制代码

输出分别是匹配文本、起始下标、结束位置和区间元组。没有找到内容时 search() 返回 None,直接调用 group() 会报错,实际使用要先判断:
  1. result = re.search(r'\d+', '订单暂未生成')
  2. if result:
  3.     print(result.group())
  4. else:
  5.     print('没有找到订单号')
复制代码

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

正则中经常出现反斜杠,Python 字符串也使用反斜杠表示转义字符,例如换行和制表符。两套规则写在一起,很容易把反斜杠写乱。在字符串前加 r 可以创建原始字符串:
  1. pattern = r'\d+'
复制代码

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

三、search、match 和 fullmatch 的区别

三个方法都会返回 Match 对象,区别在于从哪里开始找,以及需要匹配多少内容。search() 在整个字符串中查找第一个符合规则的位置;match() 只检查字符串开头;fullmatch() 要求整段字符串都符合规则,很适合校验格式。
  1. import re
  2. text = '用户ID: 1024'
  3. result = re.search(r'\d+', text)
  4. print(result.group())
  5. print(re.match(r'用户', text))
  6. print(re.match(r'\d+', text))
  7. order_id = 'A20260919'
  8. if re.fullmatch(r'[A-Z]\d{8}', order_id):
  9.     print('订单号格式正确')
  10. else:
  11.     print('订单号格式错误')
复制代码

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

四、字符规则、集合、数量词和锚点

常见字符规则如下:\d 匹配数字,\D 匹配非数字,\w 匹配 Unicode 字母、数字和下划线,\W 匹配不属于 \w 的字符,\s 匹配空白字符,\S 匹配非空白字符,. 匹配除换行以外的任意字符。

下面从一段文字中找出价格:
  1. text = '键盘价格 299 元,鼠标价格 129 元'
  2. prices = re.findall(r'\d+', text)
  3. print(prices)
复制代码

输出为 ['299', '129']。注意 \w 在 Python 3 中默认能匹配中文。如果只想匹配英文字母、数字和下划线,可以明确写成 [A-Za-z0-9_]。

方括号可以列出允许出现的字符:
  1. text = '订单 A102、B205、c308'
  2. orders = re.findall(r'[A-Z]\d+', text)
  3. print(orders)
复制代码

[A-Z] 只匹配大写英文字母,小写 c308 不符合规则。常见写法还有 [abc]、[0-9]、[a-zA-Z] 和 [^0-9]。方括号开头的 ^ 表示排除;放在方括号外时,含义会变成字符串开头。

数量词负责说明出现几次:* 表示零次或多次,+ 表示一次或多次,? 表示零次或一次,{m} 表示恰好 m 次,{m,n} 表示最少 m 次最多 n 次,{m,} 表示最少 m 次。用数量词检查手机号基本结构:
  1. phone = '13812345678'
  2. result = re.fullmatch(r'1[3-9]\d{9}', phone)
  3. print(bool(result))
复制代码

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

^ 表示字符串开头,$ 表示字符串结尾。锚点匹配的是位置,本身不占用字符。校验整段内容时,也可以直接使用 fullmatch():
  1. username = 'ivan_2026'
  2. if re.fullmatch(r'[A-Za-z][A-Za-z0-9_]{3,15}', username):
  3.     print('用户名可用')
复制代码

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

五、分组、命名分组和非捕获分组

圆括号可以把一段规则放进一个分组。下面从日期中分别取出年、月、日:
  1. import re
  2. date_text = '2026-09-19'
  3. result = re.fullmatch(
  4.     r'(\d{4})-(\d{2})-(\d{2})',
  5.     date_text,
  6. )
  7. if result:
  8.     print(result.group(0))
  9.     print(result.group(1))
  10.     print(result.group(2))
  11.     print(result.group(3))
  12.     print(result.groups())
复制代码

group(0) 是完整匹配,后面的编号按左括号出现顺序排列。groups() 一次返回所有分组。分组多起来后,记编号不方便,可以给分组命名:
  1. pattern = (
  2.     r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
  3. )
  4. result = re.fullmatch(pattern, date_text)
  5. if result:
  6.     print(result.group('year'))
  7.     print(result.groupdict())
复制代码

groupdict() 会把命名分组整理成字典。解析日志、订单和接口返回文本时,命名分组比数字编号更容易维护。

有时圆括号只是为了把几条规则放在一起,并不需要保存成分组,可以使用 (?:...):
  1. text = '图片:cover.jpg,附件:report.pdf'
  2. files = re.findall(r'\w+\.(?:jpg|png|pdf)', text)
  3. print(files)
复制代码

如果写成 (jpg|png|pdf),findall() 会优先返回捕获分组里的扩展名。这里使用非捕获分组,得到的才是完整文件名。竖线 | 表示多个规则任选一个,例如 INFO|WARNING|ERROR。

六、findall 和 finditer 批量查找

search() 只返回第一个匹配。想拿到所有结果,可以使用 findall():
  1. text = '今天处理 A102,明天处理 A205,A308 已完成'
  2. orders = re.findall(r'[A-Z]\d+', text)
  3. print(orders)
复制代码

还需要每个结果的位置时,使用 finditer():
  1. for match in re.finditer(r'[A-Z]\d+', text):
  2.     print(
  3.         match.group(),
  4.         match.start(),
  5.         match.end(),
  6.     )
复制代码

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

七、贪婪匹配和非贪婪匹配

数量词默认会尽量多匹配。下面这段 HTML 中有两个标签:
  1. html = '<p>Python</p><p>正则表达式</p>'
  2. print(re.findall(r'<p>.*</p>', html))
  3. print(re.findall(r'<p>.*?</p>', html))
复制代码

.* 会从第一个 <p> 一直匹配到最后一个 </p>,得到整段内容。在数量词后面加 ? 可以改成非贪婪匹配,这次得到两个独立结果。这个例子适合说明贪婪规则,真实 HTML 的结构远比它复杂。需要可靠解析网页时,应使用 HTML 解析库,不要只靠一条正则处理整份页面。

八、使用 re.sub 替换内容

re.sub() 会把符合规则的内容替换掉。处理手机号脱敏时,可以保留前三位和后四位:
  1. import re
  2. phone = '13812345678'
  3. masked = re.sub(
  4.     r'(\d{3})\d{4}(\d{4})',
  5.     r'\1****\2',
  6.     phone,
  7. )
  8. print(masked)
复制代码

替换字符串中的 \1 和 \2 引用了第一个、第二个分组。分组较多时,可以改用命名分组:
  1. pattern = r'(?P<start>\d{3})\d{4}(?P<end>\d{4})'
  2. masked = re.sub(
  3.     pattern,
  4.     r'\g<start>****\g<end>',
  5.     phone,
  6. )
复制代码

替换逻辑需要计算时,可以把函数传给 sub():
  1. text = '商品价格 299 元,优惠价 199 元'
  2. def add_currency(match):
  3.     price = match.group()
  4.     return f'¥{price}'
  5. result = re.sub(r'\d+', add_currency, text)
  6. print(result)
复制代码

九、使用 re.split 拆分内容

普通字符串的 split() 只能方便地处理一种固定分隔符。文本中混用逗号、分号和空格时,可以使用 re.split():
  1. text = 'Python,Java;Go Rust'
  2. languages = re.split(r'[,;\s]+', text)
  3. print(languages)
复制代码

[,;\s]+ 表示一个或多个逗号、分号或空白字符。实际数据结尾可能还带分隔符,可以顺手去掉空字符串:
  1. languages = [
  2.     item
  3.     for item in re.split(r'[,;\s]+', text)
  4.     if item
  5. ]
复制代码

十、重复使用规则时先编译,常用匹配标志

同一条规则需要使用多次时,可以用 re.compile() 创建正则对象:
  1. import re
  2. order_pattern = re.compile(r'[A-Z]\d{3}')
  3. texts = [
  4.     '新订单 A102 已创建',
  5.     'B205 等待付款',
  6.     '没有订单号',
  7. ]
  8. for text in texts:
  9.     result = order_pattern.search(text)
  10.     if result:
  11.         print(result.group())
复制代码

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

匹配标志可以改变正则的工作方式。忽略英文字母大小写时,使用 re.IGNORECASE:
  1. text = 'Error: file not found'
  2. result = re.search(
  3.     r'error',
  4.     text,
  5.     flags=re.IGNORECASE,
  6. )
  7. print(result.group())
复制代码

处理多行文本时,re.MULTILINE 会让 ^ 和 $ 匹配每一行的开头和结尾:
  1. logs = '''INFO: 启动成功
  2. ERROR: 数据库连接失败
  3. INFO: 开始重试'''
  4. errors = re.findall(
  5.     r'^ERROR:.*$',
  6.     logs,
  7.     flags=re.MULTILINE,
  8. )
  9. print(errors)
复制代码

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

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

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

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

还要留意捕获分组对 findall() 返回值的影响:
  1. text = 'A102 B205'
  2. print(re.findall(r'[A-Z]\d+', text))
  3. print(re.findall(r'([A-Z])\d+', text))
复制代码

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

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

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

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

项目结构可以写成:
  1. regex_demo/
  2. ├─ parse_log.py
  3. ├─ data/
  4. │  └─ app.log
  5. └─ output/
复制代码

在 parse_log.py 中写入:
  1. from pathlib import Path
  2. import csv
  3. import re
  4. base_dir = Path(__file__).resolve().parent
  5. log_file = base_dir / 'data' / 'app.log'
  6. output_dir = base_dir / 'output'
  7. csv_file = output_dir / 'log_report.csv'
  8. output_dir.mkdir(parents=True, exist_ok=True)
  9. log_pattern = re.compile(
  10.     r'^(?P<time>\d{4}-\d{2}-\d{2} '
  11.     r'\d{2}:\d{2}:\d{2})\s+'
  12.     r'(?P<level>INFO|WARNING|ERROR)\s+'
  13.     r'user=(?P<user>\w+)\s+'
  14.     r'action=(?P<action>\w+)\s+'
  15.     r'ip=(?P<ip>(?:\d{1,3}\.){3}\d{1,3})$'
  16. )
  17. records = []
  18. failed_lines = []
  19. with log_file.open('r', encoding='utf-8') as file:
  20.     for line_number, line in enumerate(file, start=1):
  21.         line = line.strip()
  22.         if not line:
  23.             continue
  24.         result = log_pattern.fullmatch(line)
  25.         if result:
  26.             records.append(result.groupdict())
  27.         else:
  28.             failed_lines.append((line_number, line))
  29. fieldnames = [
  30.     'time',
  31.     'level',
  32.     'user',
  33.     'action',
  34.     'ip',
  35. ]
  36. with csv_file.open(
  37.     'w',
  38.     encoding='utf-8-sig',
  39.     newline='',
  40. ) as file:
  41.     writer = csv.DictWriter(
  42.         file,
  43.         fieldnames=fieldnames,
  44.     )
  45.     writer.writeheader()
  46.     writer.writerows(records)
  47. print(f'成功解析:{len(records)} 行')
  48. print(f'解析失败:{len(failed_lines)} 行')
  49. print(f'CSV 文件:{csv_file}')
  50. for line_number, content in failed_lines:
  51.     print(f'第 {line_number} 行无法识别:{content}')
复制代码

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

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

生成的 log_report.csv 如下:
  1. time,level,user,action,ip
  2. 2026-09-19 10:15:32,INFO,ivan,login,192.168.1.10
  3. 2026-09-19 10:16:03,WARNING,alice,download,10.0.0.5
  4. 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 文件。

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

使用道具 举报

发表于 昨天 19:10 | 显示全部楼层

Re: Python正则表达式匹配查找替换与日志解析脚本实战

写得很系统,search、match、fullmatch 这组对比特别实用,尤其 fullmatch 做订单号、手机号、用户名这种整段格式校验很直观。r 原始字符串那段也很关键,正则里反斜杠多,不写 r 很容易踩转义的坑。命名分组和 groupdict 用来解析日志确实比记数字编号舒服,字段一多也不容易乱。你举的订单号、日期、价格、用户名例子都很贴近实际,能直接借鉴到脚本里。期待后面替换、拆分、编译和日志解析导出 CSV 的部分,尤其想看看复杂日志怎么用命名分组抽字段后整理成表格。
回复 支持 反对

使用道具 举报

发表于 昨天 19:20 | 显示全部楼层

Re: Python正则表达式匹配查找替换与日志解析脚本实战

这段整理得很系统,search、match、fullmatch 的区别讲得很直观,尤其是 fullmatch 做手机号和用户名格式校验,很适合新手建立“先写特征再匹配”的思路。原始字符串 r 的建议也很实用,方括号、数量词和锚点这些基础串得挺顺。命名分组那段如果是在论坛里显示成带转义符号的样子,复制到编辑器里记得还原成真正的尖括号,不然会报错。期待后面 compile、sub、split 和日志解析导出 CSV 的内容,尤其是命名分组配合 groupdict 直接整理成字段,应该会很像实战日志处理。感谢分享。
回复 支持 反对

使用道具 举报

发表于 昨天 19:30 | 显示全部楼层

Re: Python正则表达式匹配查找替换与日志解析脚本实战

这篇整理得挺系统,从 search 到分组命名一路串下来,对刚开始写日志解析脚本的人很友好。几个点特别有共鸣:没有匹配时先判断再 group,能少踩很多空对象的坑;正则默认用 r 字符串确实省事;search、match、fullmatch 按“查找、检查开头、校验整段”来区分,比死记规则清楚多了。命名分组加 groupdict 做字段提取也很舒服,后面导出 CSV 时把字典键当表头、值当行,可读性会比按编号取值好很多。 帖子最后好像断在命名分组比数字编号那里,有点意犹未尽,期待把日志解析导出 CSV 的完整例子补上。个人也比较想看一行日志里同时混了时间、级别、模块和消息时,分组会怎么设计得更稳、更不容易被空格或中文干扰。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-9 06:01 , Processed in 0.046292 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部