
文章目录
📖 开篇导读
在之前的课程中,我们学习了字符串的各种操作:查找、替换、分割、拼接等。但是,当面对复杂的文本匹配需求时,例如“匹配一个邮箱地址”、“提取日志中的所有IP地址”、“验证手机号格式”,普通的字符串方法就显得力不从心了。这时,就需要用到正则表达式(Regular Expression)。
正则表达式是一种用于描述字符串模式的“微型语言”。它通过特定的符号组合,定义一套匹配规则,可以在海量文本中快速查找、替换、提取符合特定模式的子串。几乎所有主流编程语言都支持正则表达式,Python提供了re模块来实现。
💡 工作场景:
- 日志分析:从Nginx/Apache日志中提取IP、URL、状态码。
- 数据清洗:去除HTML标签、提取数字、替换敏感词。
- 表单验证:校验邮箱、手机号、身份证号等格式。
- 爬虫:提取网页中的链接、标题、价格信息。
- 自然语言处理:分词、匹配特定语法结构。
本课将系统讲解正则表达式的基础语法、re模块的核心函数、高级技巧(贪婪/非贪婪、分组、反向引用、编译标志),并通过丰富的实战案例让你彻底掌握正则表达式。
学完本课,你将能够用几行正则表达式,解决原本可能需要数十行字符串操作的复杂问题。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 理解正则表达式的基本元字符及作用(.、*、+、?、[]、()等) | 编写基础匹配模式 |
| 2️⃣ | 掌握re模块的核心函数(match、search、findall、finditer、sub、split) | 实际应用中的工具选择 |
| 3️⃣ | 理解贪婪模式与非贪婪模式,能根据需要切换 | 避免匹配过多字符 |
| 4️⃣ | 掌握分组与捕获,以及反向引用\1 | 提取子串 |
| 5️⃣ | 掌握编译标志(re.I、re.S、re.M等) | 增强模式灵活性 |
| 6️⃣ | 通过实战案例(邮箱验证、HTML标签提取、日志解析)提升应用能力 | 解决实际问题 |
🔥 面试考点:“写出匹配邮箱的正则表达式”“
re.match和re.search的区别”“贪婪与非贪婪的区别”“如何提取括号内的内容?”
📚 知识点理论精讲
一、正则表达式语法基础
正则表达式由普通字符(如字母、数字)和特殊字符(元字符)组成。元字符是具有特殊含义的符号。
1.1 常用元字符
| 元字符 | 含义 | 示例 |
|---|---|---|
. | 匹配除换行符以外的任意单个字符 | a.c 匹配 abc、a c |
^ | 匹配字符串开头 | ^Hello 匹配以Hello开头的字符串 |
$ | 匹配字符串结尾 | end$ 匹配以end结尾的字符串 |
* | 前一个字符出现0次或多次 | ab* 匹配 a、ab、abb… |
+ | 前一个字符出现1次或多次 | ab+ 匹配 ab、abb,不匹配 a |
? | 前一个字符出现0次或1次 | colou?r 匹配 color 和 colour |
{n} | 前一个字符恰好出现n次 | \d{3} 匹配三个数字 |
{n,} | 至少n次 | \d{3,} 匹配三个及以上数字 |
{n,m} | 出现n到m次 | \d{2,4} 匹配2-4个数字 |
[] | 匹配中括号内任意一个字符 | [aeiou] 匹配任意一个元音字母 |
| | 或,匹配左边或右边 | cat|dog 匹配 cat 或 dog |
() | 分组,捕获子模式 | (\d{3})-(\d{4}) 捕获两组数字 |
\ | 转义字符 | \. 匹配点号本身 |
1.2 预定义字符集
| 简写 | 含义 | 等价于 |
|---|---|---|
\d | 数字 | [0-9] |
\D | 非数字 | [^0-9] |
\w | 单词字符(字母、数字、下划线) | [a-zA-Z0-9_] |
\W | 非单词字符 | [^a-zA-Z0-9_] |
\s | 空白字符(空格、制表符、换行等) | [ \t\n\r\f\v] |
\S | 非空白字符 | [^ \t\n\r\f\v] |
\b | 单词边界 | \bword\b 匹配完整单词 |
\B | 非单词边界 |
1.3 字符集合取反
在[]内使用^表示取反:[^0-9] 匹配非数字字符。
1.4 贪婪与非贪婪
默认情况下,量词(*、+、{n,m})是贪婪的,会匹配尽可能多的字符。在量词后加?变为非贪婪(最小匹配)。
import re
text = "<html><head></head></html>"
print(re.findall(r'<.*>', text)) # 贪婪: ['<html><head></head></html>']
print(re.findall(r'<.*?>', text)) # 非贪婪: ['<html>', '<head>', '</head>', '</html>']
二、Python re 模块核心函数
2.1 re.compile()
将正则表达式编译成模式对象,提高效率(尤其在多次使用时)。
pattern = re.compile(r'\d+')
2.2 re.match(pattern, string, flags)
从字符串开头匹配,成功返回匹配对象,否则返回None。
m = re.match(r'\d+', '123abc')
if m:
print(m.group()) # '123'
2.3 re.search(pattern, string, flags)
扫描整个字符串,返回第一个匹配,否则None。
m = re.search(r'\d+', 'abc123def')
print(m.group()) # '123'
2.4 re.findall(pattern, string, flags)
返回所有非重叠匹配的字符串列表或元组列表(有分组时)。
re.findall(r'\d+', 'a1b22c333') # ['1', '22', '333']
2.5 re.finditer(pattern, string, flags)
返回迭代器,每个元素是匹配对象,适合大文本。
for m in re.finditer(r'\d+', 'a1b22c333'):
print(m.group())
2.6 re.sub(pattern, repl, string, count=0, flags=0)
替换匹配的子串。repl可以是字符串或函数。
re.sub(r'\d+', 'X', 'a1b22c333') # 'aXbXcX'
2.7 re.split(pattern, string, maxsplit=0, flags=0)
按匹配模式分割字符串,返回列表。
re.split(r'[,;]', 'a,b;c') # ['a', 'b', 'c']
三、匹配对象的方法
当match()或search()成功时,返回Match对象,常用方法:
group():返回匹配的整个字符串。group(n):返回第n个分组的子串。groups():返回所有分组(元组)。groupdict():返回命名分组组成的字典。start()、end()、span():匹配的起始、结束索引。
m = re.search(r'(\d{3})-(\d{4})', '电话: 123-4567')
print(m.group()) # '123-4567'
print(m.group(1)) # '123'
print(m.group(2)) # '4567'
print(m.groups()) # ('123', '4567')
四、分组与反向引用
4.1 普通分组 ()
括号内的表达式作为一个分组,可以提取或引用。
4.2 非捕获分组 (?:...)
只分组,不捕获,不保存为单独的组。
re.search(r'(?:https?://)(\w+)', 'http://python.org').group(1) # 'python'
4.3 命名分组 (?P<name>...)
给分组起名字,方便引用。
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', '2025-03')
print(m.group('year')) # '2025'
4.4 反向引用 \number
在正则表达式内部引用之前捕获的分组。
# 匹配重复的单词
re.search(r'\b(\w+)\s+\1\b', 'hello hello world').group() # 'hello hello'
五、编译标志(flags)
标志可以改变正则表达式的行为,可以组合使用(re.I | re.M)。
| 标志 | 缩写 | 作用 |
|---|---|---|
re.IGNORECASE | re.I | 忽略大小写 |
re.MULTILINE | re.M | 多行模式,^和$匹配每行的开头/结尾 |
re.DOTALL | re.S | 使.匹配换行符 |
re.UNICODE | re.U | 默认启用,让\w等匹配Unicode字符 |
re.VERBOSE | re.X | 允许正则表达式内写注释和换行 |
pattern = re.compile(r'''
\d{3} # 区号
- # 分隔符
\d{4} # 号码
''', re.VERBOSE)
六、常见模式举例
| 需求 | 正则表达式 |
|---|---|
| 邮箱地址 | \w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+ |
| IPv4地址 | \b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b |
| 手机号(中国) | 1[3-9]\d{9} |
| 日期 YYYY-MM-DD | \d{4}-\d{2}-\d{2} |
| URL | https?://[^\s]+ |
💻 代码案例实操
案例1:基础匹配与查找
"""
basic_regex.py
演示match、search、findall、finditer
"""
import re
text = "Python 3.9 发布于 2020-10-05,Python 3.10 发布于 2021-10-04"
# match 从开头匹配
m = re.match(r'Python', text)
print("match:", m.group() if m else None) # Python
# search 查找第一个
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)
print("search:", m.group()) # 2020-10-05
print("年:", m.group(1), "月:", m.group(2), "日:", m.group(3))
# findall 全部匹配
dates = re.findall(r'\d{4}-\d{2}-\d{2}', text)
print("findall:", dates) # ['2020-10-05', '2021-10-04']
# finditer 迭代器
for m in re.finditer(r'Python (\d\.\d)', text):
print(f"版本: {m.group(1)}, 位置: {m.span()}")
案例2:替换与分割
"""
sub_split_demo.py
演示sub和split的用法
"""
import re
text = "姓名: 张三, 年龄: 25, 城市: 北京"
# 替换数字为 *
masked = re.sub(r'\d+', '*', text)
print(masked) # 姓名: 张三, 年龄: *, 城市: 北京
# 用正则分割
items = re.split(r'[,:]', text)
print([item.strip() for item in items]) # ['姓名', '张三', '年龄', '25', '城市', '北京']
# 使用函数进行动态替换
def add_one(match):
num = int(match.group())
return str(num + 1)
result = re.sub(r'\d+', add_one, "版本 3.9 -> 4.0")
print(result) # 版本 4.10 -> 5.0(注意替换后小数部分未保持,仅示范)
案例3:邮箱验证
"""
email_validation.py
验证邮箱格式(简化版)
"""
import re
def is_valid_email(email):
# 简单模式:允许字母数字+._-,@后域名有效
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
emails = ["user@example.com", "user.name@domain.co.uk", "invalid@.com", "user@domain"]
for e in emails:
print(f"{e}: {is_valid_email(e)}")
案例4:提取日志中的IP和状态码
"""
log_extract.py
从Nginx日志行提取IP、请求方法、URL、状态码
"""
import re
log_line = '192.168.1.1 - - [10/Oct/2025:13:55:36 +0800] "GET /index.html HTTP/1.1" 200 2326'
# 解析日志
pattern = r'(\d+\.\d+\.\d+\.\d+) - - \[.*?\] "(\w+) (.*?) HTTP/.*?" (\d+)'
match = re.search(pattern, log_line)
if match:
ip = match.group(1)
method = match.group(2)
url = match.group(3)
status = match.group(4)
print(f"IP: {ip}, Method: {method}, URL: {url}, Status: {status}")
案例5:贪婪与非贪婪对比
"""
greedy_vs_non_greedy.py
演示量词的贪婪与非贪婪
"""
import re
html = "<div><p>段落1</p><p>段落2</p></div>"
# 贪婪:尽可能匹配更多字符
greedy = re.search(r'<.*>', html)
print("贪婪:", greedy.group()) # 整个字符串
# 非贪婪:最小匹配
non_greedy = re.search(r'<.*?>', html)
print("非贪婪:", non_greedy.group()) # <div>
# 提取所有标签
tags = re.findall(r'<.*?>', html)
print("所有标签:", tags) # ['<div>', '<p>', '</p>', '<p>', '</p>', '</div>']
案例6:分组与反向引用——匹配重复单词
"""
backreference_demo.py
使用反向引用查找重复单词
"""
import re
text = "This is is a test test sentence."
# 匹配重复的单词(单词边界内,至少重复一次)
pattern = r'\b(\w+)\s+\1\b'
matches = re.findall(pattern, text)
print("重复单词:", matches) # ['is', 'test']
# 替换重复为单个单词
result = re.sub(pattern, r'\1', text)
print("修正后:", result) # "This is a test sentence."
案例7:电话号码提取(支持多种格式)
"""
phone_extract.py
提取文本中的中国手机号(简单版)和固定电话
"""
import re
text = "联系我: 13812345678 或 010-12345678, 也可拨打 139-0000-1111"
# 手机号:1开头,第二位3-9,后面9位数字
mobile_pattern = r'1[3-9]\d{9}'
mobiles = re.findall(mobile_pattern, text)
print("手机号:", mobiles)
# 固定电话:区号-号码,区号3-4位,号码7-8位
phone_pattern = r'\d{3,4}-\d{7,8}'
phones = re.findall(phone_pattern, text)
print("固定电话:", phones)
# 综合提取所有联系方式
all_contacts = re.findall(r'1[3-9]\d{9}|\d{3,4}-\d{7,8}', text)
print("所有号码:", all_contacts)
案例8:HTML标签清理(提取文本)
"""
clean_html.py
移除HTML标签,提取纯文本
"""
import re
html = """
<h1>标题</h1>
<p>这是一个<b>重要</b>的段落。</p>
<a href="http://example.com">链接</a>
"""
# 移除标签
clean = re.sub(r'<[^>]+>', '', html)
print("清理后:\n", clean)
# 提取所有链接的href属性
links = re.findall(r'href=["\']([^"\']+)["\']', html)
print("链接:", links)
案例9:使用re.VERBOSE编写复杂正则
"""
verbose_regex.py
使用VERBOSE标志写可读的正则表达式
"""
import re
# 匹配日期格式 YYYY-MM-DD,但要求月份01-12,日期01-31
date_pattern = re.compile(r'''
^(?P<year>\d{4}) # 年份
-(?P<month>0[1-9]|1[0-2]) # 月份01-12
-(?P<day>0[1-9]|[12][0-9]|3[01]) # 日期01-31
$''', re.VERBOSE)
dates = ["2025-03-15", "2025-02-30", "2025-13-01"]
for d in dates:
m = date_pattern.match(d)
if m:
print(f"{d} 有效,年={m.group('year')} 月={m.group('month')} 日={m.group('day')}")
else:
print(f"{d} 无效")
案例10:使用re.compile提高性能(处理大文件)
"""
compile_performance.py
预编译正则表达式,在循环中重复使用
"""
import re
# 预编译
ip_pattern = re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b')
def extract_ips(text):
return ip_pattern.findall(text)
# 模拟处理大文本
with open('large_log.txt', 'r') as f:
for line in f:
ips = extract_ips(line)
# 处理ips...
# 预编译显著提高重复匹配的效率
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | 忘记转义特殊字符(如.、*、?等) | 匹配结果不准确 | 在特殊字符前加\,或使用re.escape() |
| 2 | 使用match而不是search | 只能匹配开头,中间内容匹配不到 | 明确需求,使用search扫描全文 |
| 3 | 贪婪匹配导致匹配过多字符 | 如<.*>匹配了整个HTML而不是一个标签 | 使用非贪婪<.*?> |
| 4 | 分组捕获时未使用(?:)导致生成多余分组 | 影响groups()结果 | 不需要捕获时用(?:...) |
| 5 | 用\d匹配数字时包含非ASCII数字(如阿拉伯数字) | 国际化问题 | 使用re.ASCII标志或[0-9] |
| 6 | 使用re.sub时替换字符串中不处理分组引用 | 替换内容不包含匹配的组 | repl中使用\1,或使用函数 |
| 7 | 在循环内编译正则表达式 | 性能低下 | 使用re.compile一次,反复使用 |
| 8 | 忘记标志组合使用re.I | re.M | 默认标志不生效 | 用` |
| 9 | 多行模式re.M下^和$的行为变化 | 匹配了每行而非整个字符串 | 明确是否需要 |
| 10 | 正则表达式过于复杂,难以维护 | 代码可读性差,易出错 | 使用re.VERBOSE写注释和换行 |
📝 课后实战练习题
第1题:验证手机号
编写正则表达式,验证中国大陆手机号(以1开头,第二位3-9,共11位数字)。测试用例:13812345678(有效)、12345678901(无效)、1381234567(无效)。
第2题:提取HTML中的所有图片URL
给定HTML字符串,提取所有<img>标签中的src属性值。考虑属性值可能使用单引号、双引号或无引号。
第3题:分割句子
给定文本,以句号、感叹号、问号后面的空格分割(但不破坏省略号)。用re.split实现。
第4题:日期格式统一
将文本中的日期格式从DD/MM/YYYY转换为YYYY-MM-DD。例如15/03/2025转换为2025-03-15。使用re.sub和分组。
第5题:词频统计中的单词清洗
给定文本,使用正则表达式提取所有单词(只包含字母,忽略数字和标点),转为小写,统计频率。
第6题:有效的数字字符串
编写正则,匹配科学计数法表示的数字,如1.23e-4、-5.6E+7,包括整数、小数、负号、指数部分可选。验证"123", "12.34", "1e2", "-0.5E-3"。
第7题:用户名验证
用户名要求:字母开头,后跟字母、数字、下划线,长度6-20。编写正则并测试。
第8题:日志过滤(综合)
给定包含多行日志的文件,每行格式为:[时间] 级别 消息。提取所有ERROR级别的行,并提取其中的错误代码(形如ERR123)。
🔜 下节课预告
正则表达式是文本处理的利器。下一节课我们将学习数据解析与序列化:JSON、CSV、XML等常见数据格式的处理方法。
第37课:JSON/CSV/XML数据解析与序列化全套实战教程
内容包括:
json模块:dump/load、dumps/loadscsv模块:reader/writer、DictReader/DictWriterxml.etree.ElementTree:解析与生成XML- 实战:API响应处理、Excel互转、配置文件读写
数据交换格式是现代编程的必备知识,学会它们你将能处理各种外部数据。
🌟 学习鼓励:正则表达式学习曲线较陡,但一旦掌握,处理字符串的效率将成倍提升。不要死记硬背,多动手练习,尤其是
findall、sub和分组提取。建议使用在线正则测试工具(如regex101.com)辅助学习。坚持练习,你会爱上这门强大的技术!
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
1万+

被折叠的 条评论
为什么被折叠?



