第5章 5.3 正则表达式
🎯 开场 3 分钟:为什么要学这个?
上一章我们学了 try/catch,知道了怎么捕捉程序里「炸」掉的错误。但有时候,错误不是「炸」出来的,而是数据本身就不对——比如用户填了个假手机号,程序却傻乎乎地存进去了。
你有没有遇到过这些情况?
- 用户注册时填了「abc123」当手机号,你不知道怎么判断对不对
- 爬虫抓了一堆网页,里面混杂着邮箱、电话、网址,你想只提取出电话号码
- 领导给你一个 Excel,里面「2023/03/15」「2023年3月15日」「03-15-2023」混着写,你得统一格式
这些问题,正则表达式能帮你自动化搞定。说白了,正则就是一张「字符串的筛子」——你告诉它长什么样,它帮你从一堆乱七八糟的文本里,挑出你想要的那部分。
学完这章,你能:
- 自己写正则验证手机号、邮箱、身份证
- 从一段文字里批量提取想要的内容
- 替换字符串里特定模式的文字
- 避开新人常踩的 5 个坑
🧱 基础 25 分钟:核心概念
什么是正则表达式?
\n\n
\n\n
\n\n
类比时间: 正则表达式就像是你给字符串写的「招聘要求」。
比如你招人,要求是「男性、25-30岁、会Python」,只有完全符合条件的人才会被录用。正则也一样,你写一个模式,符合这个模式的字符串就匹配,不符合就不匹配。
5.3.1 最简单的匹配:test()
先来最简单的——检查一个字符串里有没有我要找的东西。
import re
# 用 re.search() 检查字符串里有没有 "python"
text = "我正在学 JavaScript"
result = re.search("python", text)
print(result) # None(没找到,因为大小写不匹配)
print(result is None) # True
# 大小写不敏感?加 re.IGNORECASE
text2 = "我正在学 PYTHON"
result2 = re.search("python", text2, re.IGNORECASE)
print(result2) # <re.Match object; span=(5, 11), match='PYTHON'>
re.search() 返回一个「匹配对象」或者 None。如果返回 None,说明没找到;如果返回了一个对象,说明找到了。
5.3.2 提取内容:match() 和 findall()
有时候我们不只想知道「有没有」,还想把找到的内容拿出来。
# match() 只从字符串开头匹配
text = "123456789"
result = re.match(r"\d+", text) # \d+ 表示一个或多个数字
print(result.group()) # "123456789"(全部都是数字,从头开始匹配)
# findall() 找出所有匹配的部分,返回列表
text = "我的手机是13812345678,他的手机是13998765432"
phones = re.findall(r"1\d{10}", text) # 1开头,后面10位数字
print(phones) # ['13812345678', '13998765432']
零宽断言是什么?你可以理解为「找对象时的户籍要求」——它不占用字符,只是声明「我前面/后面得是什么」。比如 (?=.*[A-Z]) 表示「前面必须有大写字母」,但匹配位置不变。
5.3.3 替换内容:sub()
找到之后想换掉?用 sub():
text = "我的邮箱是 tom@qq.com,请联系他"
# 把邮箱替换成 [已隐藏]
hidden = re.sub(r"\w+@\w+\.\w+", "[已隐藏]", text)
print(hidden) # 我的邮箱是 [已隐藏],请联系他
# 替换时保留原内容:用到分组和 \1
text2 = "tom@qq.com 和 jerry@gmail.com"
result = re.sub(r"(\w+)@(\w+)\.(\w+)", r"\1@[已替换]", text2)
print(result) # tom@[已替换] 和 jerry@[已替换]
这里 \1 是第一个括号里的内容。分组用 (), \1 引用第一个分组,\2 引用第二个分组。
5.3.4 贪婪匹配 vs 懒惰匹配
这是新人最容易踩的坑!先看问题:
# 贪婪匹配:.* 会尽可能多地匹配
text = "你好 <Python> 和 <JavaScript>"
result = re.findall(r"<.*>", text)
print(result) # ['<Python> 和 <JavaScript>'] (一次性匹配完了!)
# 懒惰匹配:.*? 尽可能少地匹配
result2 = re.findall(r"<.*?>", text)
print(result2) # ['<Python>', '<JavaScript>'] (分别匹配了)
记住: 在 .* 后面加个 ?,就变成懒惰模式 .*?。
5.3.5 常用正则符号速查表
| 符号 | 意思 | 例子 |
|---|---|---|
\d |
任意数字 | \d{3} 匹配 3 个数字 |
\w |
字母、数字、下划线 | \w+ 匹配一个单词 |
\s |
空白字符 | 空格、tab、换行 |
. |
任意字符(除换行) | a.c 匹配 "abc" |
* |
0 个或多个 | ab* 匹配 "a", "ab", "abbb" |
+ |
1 个或多个 | ab+ 匹配 "ab", "abbb" |
? |
0 个或 1 个 | ab? 匹配 "a", "ab" |
^ |
字符串开头 | ^hello 匹配 "hello world" |
$ |
字符串结尾 | world$ 匹配 "hello world" |
\| |
或者 | cat\|dog 匹配 "cat" 或 "dog" |
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):手机号验证器
场景: 你要验证用户输入的手机号是不是合法的中国手机号。
import re
def validate_phone(phone):
"""验证手机号:1开头,第二位是3-9,后面9位数字"""
pattern = r"^1[3-9]\d{9}$" # ^开头 $结尾,中间是1[3-9]加9个数字
if re.match(pattern, phone):
return True
return False
# 测试
test_phones = ["13812345678", "12345678901", "1381234567", "abc13812345678"]
for phone in test_phones:
result = validate_phone(phone)
print(f"{phone}: {'✅ 合法' if result else '❌ 非法'}")
输出:
13812345678: ✅ 合法
12345678901: ❌ 非法
1381234567: ❌ 非法
abc13812345678: ❌ 非法
解释: ^1[3-9]\d{9}$ 这个正则说「必须是1开头,第二位是3-9,后面9个数字」。^ 和 $ 保证了整个字符串从头到尾都得符合,不允许前面有多余字符。
项目 2(15 分钟):从文本中提取订单信息
场景: 你有一段邮件文本,里面混杂着订单号、金额、日期,你要把它们提取出来整理成结构化数据。
import re
import json
email_content = """
您好,您的订单已确认:
订单号:DD20230615001
下单时间:2023年06月15日 14:30
商品名称:Python入门到精通
实付金额:¥99.00
收货人:张三
联系电话:13812345678
感谢您购买!
"""
def extract_order_info(text):
"""从邮件文本中提取订单信息"""
info = {}
# 提取订单号:DD开头,后面是日期+序号
order_match = re.search(r"订单号:(\w+)", text)
info["订单号"] = order_match.group(1) if order_match else None
# 提取日期:支持多种格式
date_match = re.search(r"(\d{4})年(\d{1,2})月(\d{1,2})日", text)
if date_match:
info["日期"] = f"{date_match.group(1)}-{date_match.group(2).zfill(2)}-{date_match.group(3).zfill(2)}"
# 提取金额:¥开头,后面是数字和小数点
money_match = re.search(r"¥(\d+\.\d{2})", text)
info["金额"] = float(money_match.group(1)) if money_match else None
# 提取手机号
phone_match = re.search(r"联系电话:(\d{11})", text)
info["电话"] = phone_match.group(1) if phone_match else None
return info
result = extract_order_info(email_content)
print("提取结果:")
print(json.dumps(result, ensure_ascii=False, indent=2))
输出:
{
"订单号": "DD20230615001",
"日期": "2023-06-15",
"金额": 99.0,
"电话": "13812345678"
}
解释: 用 () 把要的部分包起来,.group(1) 就能取到第一个括号里的内容。zfill(2) 是把月份和日期补成两位数,比如 "6" 变成 "06"。
项目 3(15 分钟):数据清洗工具
场景: 你从网上爬了一批数据,里面格式混乱:邮箱有的带 mailto:,有的没有;网址有的以 http 开头,有的以 www 开头。你要把它统一格式。
import re
raw_data = [
"联系邮箱: tom@example.com",
"邮箱地址是 mailto:jerry@company.cn 记得联系",
"官网:www.test.com/index",
"HTTP://WWW.EXAMPLE.ORG 也要访问",
"手机:13812345678 或 139-9876-5432",
"订单号 DD20230615-001 确认了",
]
def clean_data(text):
"""清洗混乱的数据,统一格式"""
result = text
# 统一邮箱格式:去掉 mailto: 前缀
result = re.sub(r"mailto:", "", result)
# 统一网址格式:转小写
result = re.sub(r"https?://www\.\w+", lambda m: m.group().lower(), result, flags=re.IGNORECASE)
# 统一手机号格式:去掉短横线
result = re.sub(r"(\d{3})-(\d{4})-(\d{4})", r"\1\2\3", result)
# 统一订单号格式:把 DD20230615-001 变成 DD20230615001
result = re.sub(r"DD(\d{8})-(\d{3})", r"DD\1\2", result)
return result
print("清洗后的数据:")
for i, item in enumerate(raw_data, 1):
cleaned = clean_data(item)
print(f"{i}. {cleaned}")
输出:
1. 联系邮箱: tom@example.com
2. 邮箱地址是 jerry@company.cn 记得联系
3. 官网:www.test.com/index
4. http://www.example.org 也要访问
5. 手机:13812345678 或 13998765432
6. 订单号 DD20230615001 确认了
解释: re.sub 的替换内容可以是函数!lambda m: m.group().lower() 拿到匹配内容后转成小写返回。分组替换 \1\2\3 把手机号的三段连起来。
💪 进阶 20 分钟:常见坑 + 性能小贴士
❌ 坑 1:忘记转义特殊字符
# ❌ 错误:. 在正则里是「任意字符」,不会匹配真的点号
text = "文件:report.2023.pdf"
result = re.findall(r"report.2023", text)
print(result) # ['report.2023'] —— 意外匹配了 reportx2023
# ✅ 正确:用 \. 转义
result = re.findall(r"report\.2023", text)
print(result) # []
注意! . * ? + ^ $ [ ] { } ( ) | \ 这些符号在正则里有特殊含义,如果要找原文,得加 \ 转义。
❌ 坑 2:贪婪匹配吃掉了不该吃的东西
# ❌ 错误
html = "<div>标题</div><div>内容</div>"
result = re.findall(r"<div>.*</div>", html)
print(result) # ['<div>标题</div><div>内容</div>'] —— 一次匹配完了!
# ✅ 正确:用惰性匹配
result = re.findall(r"<div>.*?</div>", html)
print(result) # ['<div>标题</div>', '<div>内容</div>']
❌ 坑 3:re.match 和 re.search 傻傻分不清
text = "abc123"
# re.match 只从字符串开头匹配
print(re.match(r"\d", text)) # None(开头是字母,不是数字)
# re.search 从任意位置匹配
print(re.search(r"\d", text)) # <re.Match ...> (找到了数字)
❌ 坑 4:分组编号数错
text = "Hello World"
# ❌ 错误:以为 \1 是整个匹配
result = re.sub(r"(\w+) (\w+)", r"\2 \1", text)
print(result) # World Hello —— 实际是正确的,但容易搞混
# ✅ 正确理解:\1 是第一个括号,\2 是第二个括号
❌ 坑 5:不在字符类里转义 -
# ❌ 错误:- 在中间表示范围
pattern = r"[a-z]" # 匹配 a 到 z
# ✅ 正确:- 放首尾,或转义
pattern = r"[-a-z]" # 匹配 -、a 到 z
pattern = r"[a\-z]" # 同上
💡 性能小贴士:预编译正则
如果你要重复用一个正则上千次,先编译再使用会快很多:
import re
# ❌ 每次调用都重新编译(慢)
for _ in range(10000):
re.search(r"\d{11}", text)
# ✅ 预编译(快)
pattern = re.compile(r"\d{11}")
for _ in range(10000):
pattern.search(text)
🔧 调试技巧:用 re.DEBUG 看正则结构
import re
# 打印正则的调试信息
re.compile(r"^1[3-9]\d{9}$", re.DEBUG)
✏️ 练习题
练习 1(2 分钟):验证邮箱
- 输入:
"user@example.com" - 预期输出:
True - 提示: 邮箱格式是「字母/数字/@/字母/数字/.字母」
练习 2(2 分钟):在项目 1 基础上加个判断
- 输入:
"13812345678"和"12345" - 预期输出:
"13812345678: 合法手机号"和"12345: 非法手机号" - 提示: 把验证函数改一下,加个 print
练习 3(3 分钟):提取 URL
- 输入:
"访问 https://www.example.com 和 http://test.org" - 预期输出:
['https://www.example.com', 'http://test.org'] - 提示: 用
findall,URL 模式大概是https?://[\w.]+
练习 4(3 分钟):把手机号中间四位变成 *
- 输入:
"张三 13812345678" - 预期输出:
"张三 138****5678" - 提示: 用
sub和分组替换
练习 5(5 分钟):找出报错原因
import re
text = "价格:99.99元"
result = re.sub(r"(\d+).(\d+)", r"\1.\2", text)
print(result)
- 问题: 这个代码本意是想把
99.99保留不变,但输出少了「元」字,为什么?怎么修? - 提示: 想想
(\d+).(\d+)里的.匹配了什么
📝 作业:做一个「文本提取工具」
需求描述:
做一个命令行工具,能从用户输入的一段文本里,提取出手机号、邮箱、URL、日期。
功能点:
1. 输入一段文字,输出所有匹配到的信息
2. 每种信息分类显示(手机号、邮箱、URL、日期)
3. 支持从文件读取内容(python extractor.py input.txt)
加分项:
1. 能把结果导出成 JSON 文件
2. 支持多种日期格式的识别和统一转换
验收标准:
- 运行 python extractor.py "联系13812345678,邮箱test@mail.com" 能输出正确结果
- 代码有注释,说明每一步在干嘛
📚 总结 + 资源
这一章学了 3 个核心点:
- 正则表达式是字符串的筛子——用
re.search、re.findall按模式查找 - 分组和替换——
()圈住要提取的部分,\1\2在替换时引用 - 贪婪 vs 懒惰——
*?和+?是解决「一次匹配太多」的法宝
延伸学习资源:
- Python re 官方文档 —— 最权威的参考
- 正则表达式30分钟入门 —— 经典中文教程
- Regex101 —— 在线正则测试工具,实时看匹配结果
互动钩子:
你在工作或生活中有没有遇到过「这段文字乱七八糟不知道怎么提取」的场景?评论区聊聊你是怎么解决的,老粉优先回复!
📌 下章剧透: 学会了正则,下一章我们要聊点不一样的——浏览器里的「记忆宫殿」。你知道网页是怎么记住你的登录状态的吗?localStorage 和 sessionStorage 有什么区别?下章揭晓!

评论(0)