第5章 5.2 正则表达式入门

🎯 开场 3 分钟:为什么要学这个?

上一章我们学了字符串方法大全,你会用 split() 切字符串、用 replace() 换内容了。但你有没有遇到过这种情况:

  • 手机号138-1234-5678,想知道里面有没有"1234"这个数字组合?
  • 爬虫抓了一堆网页,想把所有邮箱地址挖出来?
  • 日志文件有几千行,想找出所有带"ERROR"的行?

用字符串方法行不行?行,但很麻烦——你得写一堆 if "1234" in 字符串 嵌套循环,代码又臭又长。

正则表达式就是来解决这个问题的:它是一种「描述文本规律」的语言,让你用一行代码,从一堆乱七八糟的文本里,精确定位你想找的内容。

学完这章,你就能:写一个「邮箱猎人」从网页源码里自动提取所有邮箱,或者写一个「敏感词检测器」扫描一篇文章里有没有违禁词。


🧱 基础 25 分钟:核心概念

什么是正则表达式?

说白了:正则表达式就是一张「找文字的藏宝图」。

比如你想在一本书里找所有"以数字开头的电话号码",你不用挨个看,你可以画一张图:"开头是数字,后面是...",这张图就是正则表达式。

先学会 Import

import re

这行代码干吗的?把正则表达式这个工具箱加载进来。Python 不会自带这个工具箱,必须先 import。

4 个最常用的「侦探」函数

1. re.search() - 找第一个匹配的

类比:像警察抓人,找到第一个符合条件的就收工。

import re

text = "我的手机号是138-1234-5678,你的呢?"
result = re.search(r"\d{3}-\d{4}-\d{4}", text)

if result:
print("找到了:", result.group())
else:
print("没找到")

输出:

找到了: 138-1234-5678

r"\d{3}-\d{4}-\d{4}" 是什么?这就是正则表达式本身,意思是"3个数字-4个数字-4个数字"。

2. re.findall() - 找所有匹配的

类比:像雷达扫描,把所有符合条件的都找出来。

import re

text = """
张三的手机是138-1234-5678,
李四是139-9876-5432,
王五是137-5555-8888。
"""

phones = re.findall(r"\d{3}-\d{4}-\d{4}", text)
print("找到", len(phones), "个手机号:")
for phone in phones:
print(phone)

输出:

找到 3 个手机号:
138-1234-5678
139-9876-5432
137-5555-8888

3. re.match() - 从头匹配

类比:像考试,第一名必须从第一名开始才算数。

import re

text1 = "12345是世界人口"
text2 = "人类历史有12345年"

print("text1:", re.match(r"\d+", text1).group())  # 从头就是数字,匹配成功
print("text2:", re.match(r"\d+", text2))          # 从头不是数字,匹配失败

输出:

text1: 12345
None

重要区别match() 只看开头,search() 看全文。

4. re.sub() - 替换

类比:像文字整容手术,把符合条件的地方整成别的样子。

import re

text = "我的邮箱是tom@qq.com,别发垃圾邮件到tom@qq.com!"
new_text = re.sub(r"\w+@\w+\.\w+", "[已隐藏邮箱]", text)

print(new_text)

输出:

我的邮箱是[已隐藏邮箱],别发垃圾邮件到[已隐藏邮箱]!

配图说明:正则表达式sub替换示意图,把邮箱换成[已隐藏邮箱]

元字符:正则表达式的 vocabulary

元字符就是正则表达式的"单词",每个都有特殊含义。

元字符 含义 类比
\d 任意数字 "0-9任何一个"
\w 任意字母或数字或下划线 "英语字母表+数字+_"
\s 空白字符(空格、Tab、换行) "空格键按一下"
. 任意字符(换行除外) "万能卡"
* 0个或多个 "0到无穷多个"
+ 1个或多个 "至少1个"
? 0个或1个 "有没有都行"
^ 开头 "一行的最左边"
$ 结尾 "一行的最右边"

看个例子:

import re

# 找所有数字
print(re.findall(r"\d+", "abc123def456"))

# 找所有字母数字组合
print(re.findall(r"\w+", "hello! world? 123"))

# 找所有空格
print(re.findall(r"\s", "hello world"))

输出:

['123', '456']
['hello', 'world', '123']
[' ']

字符类 [] - 自定义范围

用方括号告诉正则:"这些字符里任何一个都行"。

import re

# 找所有元音字母
text = "Python is a great language"
vowels = re.findall(r"[aeiou]", text, re.IGNORECASE)
print("元音字母:", vowels)

# 找所有数字和大写字母
text2 = "A1B2C3"
alphanumeric = re.findall(r"[A-Z0-9]", text2)
print("大写字母和数字:", alphanumeric)

输出:

元音字母: ['o', 'i', 'a', 'e', 'a', 'a', 'u', 'a', 'e']
大写字母和数字: ['A', '1', 'B', '2', 'C', '3']

数量词 - 告诉正则要几个

数量词 含义
{3} 正好3个
{3,5} 3到5个
* 0个或多个
+ 1个或多个
? 0个或1个
import re

# 手机号:138开头,后面8位
phones = re.findall(r"138\d{8}", "我的13812345678和13898765432")
print("138开头的手机号:", phones)

# 找出6位数字(可能是邮编)
text = "北京100000,上海200000,广州510000"
postcodes = re.findall(r"\d{6}", text)
print("邮编:", postcodes)

输出:

138开头的手机号: ['13812345678', '13898765432']
邮编: ['100000', '200000', '510000']

分组 () - 提取特定部分

圆括号可以分组,让你只提取你关心的部分。

import re

# 提取日期中的年月日
text = "2024-01-15是重要日子"
match = re.search(r"(\d{4})-(\d{2})-(\d{2})", text)

if match:
print("完整匹配:", match.group())      # 2024-01-15
print("年:", match.group(1))           # 2024
print("月:", match.group(2))           # 01
print("日:", match.group(3))           # 15

输出:

完整匹配: 2024-01-15
年: 2024
月: 01
日: 15

分组有什么用?你想从一段文字里精确挖出某个部分。比如从日志里提取时间、从邮件地址里提取用户名。

配图2 - 配图2

贪婪 vs 非贪婪

这是正则表达式最让人困惑的地方,但其实很简单:

  • 贪婪匹配(默认):.* 会尽可能多地吃字符
  • 非贪婪匹配.*? 只吃最少的字符
import re

text = "我喜欢<p>苹果</p>和<p>香蕉</p>"

# 贪婪:.* 会一直吃到最后
greedy = re.search(r"<p>.*</p>", text)
print("贪婪匹配:", greedy.group())

# 非贪婪:.*? 遇到第一个就停
non_greedy = re.search(r"<p>.*?</p>", text)
print("非贪婪匹配:", non_greedy.group())

输出:

贪婪匹配: <p>苹果</p>和<p>香蕉</p>
非贪婪匹配: <p>苹果</p>

什么时候用非贪婪? 当你要匹配HTML标签、成对出现的符号时,用非贪婪否则会吃撑。


🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):邮箱猎人

场景:你有一段文本,想把所有邮箱挖出来。

import re

text = """
李老师 li@example.com 负责招生
王同学 wang_student@163.com 咨询课程
客服邮箱: service@python-course.com.cn
"""

# 简单邮箱匹配
emails = re.findall(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+", text)

print("找到以下邮箱:")
for email in emails:
print("-", email)

预期输出:

找到以下邮箱:
- li@example.com
- wang_student@163.com
- service@python-course.com.cn

这行正则说白了就是:字母数字下划线@字母数字点字母数字

项目 2(15 分钟):日志敏感词扫描器

场景:你有一个日志文件,找出所有包含 ERROR 或 WARNING 的行,并提取时间戳。

import re

# 模拟日志内容
log_content = """
2024-01-15 10:23:45 [INFO] 系统启动成功
2024-01-15 10:23:46 [ERROR] 数据库连接失败
2024-01-15 10:23:47 [WARNING] 内存使用率 85%
2024-01-15 10:23:48 [ERROR] 用户认证失败: user=admin
2024-01-15 10:23:49 [INFO] 重试连接数据库
2024-01-15 10:23:50 [ERROR] 连接超时: timeout=30s
"""

# 找出所有 ERROR 和 WARNING 行
error_lines = re.findall(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(ERROR|WARNING)\] (.+)", log_content)

print("=" * 50)
print("扫描结果:发现", len(error_lines), "条问题日志")
print("=" * 50)

for timestamp, level, message in error_lines:
print(f"[{level}] {timestamp}")
print(f"   详情: {message}")
print()

预期输出:

==================================================
扫描结果:发现 3 条问题日志
==================================================
[ERROR] 2024-01-15 10:23:46
情: 数据库连接失败

[WARNING] 2024-01-15 10:23:47
情: 内存使用率 85%

[ERROR] 2024-01-15 10:23:48
情: 用户认证失败: user=admin

[ERROR] 2024-01-15 10:23:50
情: 连接超时: timeout=30s

这个例子展示了分组的强大:用分组分别提取了时间、级别、详情。

项目 3(15 分钟):手机号隐私脱敏工具

场景:给一批手机号脱敏,把中间4位变成 ****。

import re

def mask_phone(phone):
"""把手机号中间4位脱敏"""
# 匹配3位-4位-4位格式
match = re.search(r"(\d{3})-(\d{4})-(\d{4})", phone)
if match:
    return f"{match.group(1)}-****-{match.group(3)}"
return phone

def mask_phone_v2(phone):
"""用sub替换的方式脱敏(更简洁)"""
return re.sub(r"(\d{3})-(\d{4})-(\d{4})", r"\1-****-\3", phone)

# 测试数据
phones = [
"138-1234-5678",
"139-9876-5432",
"137-5555-8888",
"手机号:136-0000-9999 记得保存"
]

print("脱敏前后对比:")
print("-" * 40)
for phone in phones:
masked = mask_phone(phone)
print(f"原: {phone}")
print(f"新: {masked}")
print()

预期输出:

脱敏前后对比:
----------------------------------------
原: 138-1234-5678
新: 138-****-5678

原: 139-9876-5432
新: 139-****-5432

原: 137-5555-8888
新: 137-****-8888

原: 手机号:136-0000-9999 记得保存
新: 手机号:136-****-9999 记得保存

技巧r"\1-****-\3" 里的 \1 \3 是反向引用,引用第一个和第三个分组的内容。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:转义字符让人头疼

❌ 错误示例:

# 想匹配 "C:\Users\test",但 \U 被当成了特殊字符
text = "路径是 C:\Users\test"
result = re.findall(r"\Users", text)  # 匹配不到!

✅ 正确做法:

# 方法1:加 r 前缀(raw字符串)
result = re.findall(r"C:\\Users\\test", text)

# 方法2:双反斜杠转义
result = re.findall("C:\\\\Users\\\\test", text)

记住:正则里的 \ 本身就是特殊字符,如果要匹配字面意义上的 \,得写 \\

坑 2:match 和 search 傻傻分不清

❌ 错误示例:

text = "工号是 A12345"
result = re.match(r"A\d+", text)  # match从开头匹配,开头是"工",匹配失败
print(result)  # None

✅ 正确做法:

# 想在字符串中间找,用 search
result = re.search(r"A\d+", text)
print(result.group())  # A12345

记住:找东西用 search(),除非你确定要验证"是不是以X开头"才用 match()

坑 3:findall 返回的是列表,不是 Match 对象

❌ 错误示例:

text = "2024-01-15"
result = re.findall(r"(\d{4})-(\d{2})-(\d{2})", text)
print(result.group(1))  # 报错!列表没有 group 方法

✅ 正确做法:

# findall 返回的是字符串列表
result = re.findall(r"(\d{4})-(\d{2})-(\d{2})", text)
print(result[0])  # ('2024', '01', '15') - 元组形式

# 如果需要用 group(),用 search 或 finditer
match = re.search(r"(\d{4})-(\d{2})-(\d{2})", text)
print(match.group(1))  # 2024

坑 4:贪婪匹配吃多了

❌ 错误示例:

text = '他说"你好"然后说"再见"'
result = re.search(r".*", text)  # 贪婪,匹配整行
print(result.group())  # 他说"你好"然后说"再见"

✅ 正确做法:

result = re.search(r"".*?"", text)  # 非贪婪,匹配到第一个"
print(result.group())  # "你好"

坑 5:忽略 re.IGNORECASE 大小写问题

❌ 错误示例:

text = "Email: TOM@EXAMPLE.COM"
emails = re.findall(r"[a-z]+@[a-z]+\.[a-z]+", text)  # 只匹配小写,漏掉了TOM
print(emails)  # []

✅ 正确做法:

emails = re.findall(r"[a-z]+@[a-z]+\.[a-z]+", text, re.IGNORECASE)
print(emails)  # ['TOM@EXAMPLE.COM']

性能小贴士:预编译正则

如果一个正则要匹配很多次,提前「编译」它会更快:

import re

# 不用预编译:每次匹配都要解析正则
text = "a1 b2 c3 d4 e5 f6"
for _ in range(10000):
re.findall(r"\d", text)

# 预编译:只解析一次
pattern = re.compile(r"\d")
for _ in range(10000):
pattern.findall(text)

什么时候用?循环内匹配同一个正则超过100次时,预编译能提升明显性能。

调试技巧:print 大法

import re

text = "错误码 E001 发生在 10:30"

# 先看看正则能不能匹配
match = re.search(r"(E\d{3})", text)
if match:
print("DEBUG: 匹配到了", match.group())
print("DEBUG: 分组1是", match.group(1))

✏️ 练习题

练习 1(2 分钟):找数字

  • 输入:"订单号 A2024001 于 2024-01-15 发货"
  • 预期输出:['A2024001', '2024', '01', '15']
  • 提示:\w+ 可以匹配字母数字组合

练习 2(2 分钟):加个判断

在练习1基础上,写一个 if 判断,检测订单号是否以 "A" 开头。

  • 预期输出:如果以A开头打印 "是A开头的订单",否则打印 "不是A开头"

练习 3(3 分钟):处理新数据

re.findall() 提取字符串 "金额:¥999元,折扣:¥199元" 中的所有金额数字。

  • 预期输出:['999', '199']
  • 提示:金额格式是 ¥后面跟数字

练习 4(3 分钟):组合练习

re.sub()"原价300元,现价250元" 里的"300"替换成"XXX",保留其他部分。

  • 预期输出:"原价XXX元,现价250元"
  • 提示:sub() 的第二个参数可以直接写要替换成的字符串

练习 5(挑战题,5 分钟):报错分析

以下代码为什么输出 None?如何修复?

import re
text = "我的学号是 S001"
result = re.match(r"\d+", text)
print(result.group())
  • 提示:学号 S001 开头是字母,不是数字

作业:做一个「文本清洗工具」

需求描述
做一个命令行工具,输入一段文字,自动:
1. 把所有手机号中间4位脱敏
2. 把所有邮箱替换成 [EMAIL]
3. 把所有数字替换成 #

功能点
1. 用 re.sub() + 分组实现手机号脱敏
2. 用 re.sub() 实现邮箱替换
3. 用 re.sub() 实现数字替换
4. 支持从命令行输入文本

加分项
1. 支持读取文件并输出清洗后的文件
2. 用 re.compile() 预编译所有正则提升性能

验收标准
- 运行 python cleaner.py "手机138-1234-5678,邮箱test@163.com,编号10086"
- 输出:手机138-****-5678,邮箱[EMAIL],编号##
- 代码有注释


📚 总结

这一章我们学了:

  • 正则表达式是描述文本规律的语言,用它可以从杂乱文本里精确定位目标
  • 4个核心函数:match(从头匹配)、search(搜一个)、findall(搜全部)、sub(替换)
  • 分组和反向引用:用 () 提取特定部分,用 \1 \2 引用它们

延伸资源
- Python 官方文档正则表达式 HOWTO:https://docs.python.org/zh-cn/3/howto/regex.html
- 菜鸟教程正则表达式:https://www.runoob.com/python3/python3-reg-expressions.html
- RegExr 正则表达式在线练习:https://regexr.com/

互动钩子
你在工作或生活中有没有遇到过「这段文字乱七八糟,想找出特定信息」的场景?评论区说说,下一章我们要学文件读写,就可以把这些技巧用在自己的文件上啦!


下章剧透
学会了正则,你能从文本里挖宝藏了。但这些宝藏还存在文件里,下一章教你用 with open() 读写文件,把正则用在真实文件上,做一个真正的「日志分析器」!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。