第5章 5.3 正则表达式

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

上一章我们学了 try/catch,知道了怎么捕捉程序里「炸」掉的错误。但有时候,错误不是「炸」出来的,而是数据本身就不对——比如用户填了个假手机号,程序却傻乎乎地存进去了。

你有没有遇到过这些情况?

  • 用户注册时填了「abc123」当手机号,你不知道怎么判断对不对
  • 爬虫抓了一堆网页,里面混杂着邮箱、电话、网址,你想只提取出电话号码
  • 领导给你一个 Excel,里面「2023/03/15」「2023年3月15日」「03-15-2023」混着写,你得统一格式

这些问题,正则表达式能帮你自动化搞定。说白了,正则就是一张「字符串的筛子」——你告诉它长什么样,它帮你从一堆乱七八糟的文本里,挑出你想要的那部分。

学完这章,你能:

  • 自己写正则验证手机号、邮箱、身份证
  • 从一段文字里批量提取想要的内容
  • 替换字符串里特定模式的文字
  • 避开新人常踩的 5 个坑

🧱 基础 25 分钟:核心概念

什么是正则表达式?

\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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 个核心点:

  1. 正则表达式是字符串的筛子——用 re.searchre.findall 按模式查找
  2. 分组和替换——() 圈住要提取的部分,\1\2 在替换时引用
  3. 贪婪 vs 懒惰——*?+? 是解决「一次匹配太多」的法宝

延伸学习资源:

互动钩子:
你在工作或生活中有没有遇到过「这段文字乱七八糟不知道怎么提取」的场景?评论区聊聊你是怎么解决的,老粉优先回复!


📌 下章剧透: 学会了正则,下一章我们要聊点不一样的——浏览器里的「记忆宫殿」。你知道网页是怎么记住你的登录状态的吗?localStorage 和 sessionStorage 有什么区别?下章揭晓!

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