Python 标准库 re 用来处理字符串匹配、查找、替换和拆分。日志、聊天记录、网页字段混杂时,正则适合先写出目标文本特征,再交给程序查找。下面从 search 开始,串起分组、替换、拆分、编译和日志解析导出 CSV。
一、re.search 完成第一次匹配
如果订单号始终由一个大写字母和八位数字组成,可以写成 [A-Z]\d{8}。其中 [A-Z] 代表一位大写字母,\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 可以创建原始字符串:
这里的 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'[A-Z]\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 中默认能匹配中文。如果只想匹配英文字母、数字和下划线,可以明确写成 [A-Za-z0-9_]。
方括号可以列出允许出现的字符:
- text = '订单 A102、B205、c308'
- orders = re.findall(r'[A-Z]\d+', text)
- print(orders)
复制代码
[A-Z] 只匹配大写英文字母,小写 c308 不符合规则。常见写法还有 [abc]、[0-9]、[a-zA-Z] 和 [^0-9]。方括号开头的 ^ 表示排除;放在方括号外时,含义会变成字符串开头。
数量词负责说明出现几次:* 表示零次或多次,+ 表示一次或多次,? 表示零次或一次,{m} 表示恰好 m 次,{m,n} 表示最少 m 次最多 n 次,{m,} 表示最少 m 次。用数量词检查手机号基本结构:
- phone = '13812345678'
- result = re.fullmatch(r'1[3-9]\d{9}', phone)
- print(bool(result))
复制代码
1 是固定开头,[3-9] 匹配第二位,\d{9} 要求后面还有九位数字。这里检查的是常见手机号格式,不能证明号码真实存在。
^ 表示字符串开头,$ 表示字符串结尾。锚点匹配的是位置,本身不占用字符。校验整段内容时,也可以直接使用 fullmatch():
- username = 'ivan_2026'
- if re.fullmatch(r'[A-Za-z][A-Za-z0-9_]{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'[A-Z]\d+', text)
- print(orders)
复制代码
还需要每个结果的位置时,使用 finditer():
- for match in re.finditer(r'[A-Z]\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'[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:
- 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。
十一、写正则时容易踩的坑
最常见的问题是规则写得太宽。比如 .* 什么都能接,短文本里看不出问题,换一批数据就可能吞掉多余内容。能写清字符范围时,尽量使用 [A-Z]、\d{4} 这类具体规则。
第二个问题是忘记处理 None。search() 和 fullmatch() 都有匹配失败的时候,调用 group() 以前先判断结果。
还要留意捕获分组对 findall() 返回值的影响:
- 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
准备一份 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 文件。
学正则时没必要先背完所有符号。先把目标文本找出来,标清哪些内容固定、哪些内容会变,再把它们换成字符规则和数量词。规则写完后多准备几条正常数据和异常数据测试,比盯着一条样例反复修改更容易发现问题。 |