Python正则表达式入门:从模式匹配到数据提取实战
文章目录
- Python正则表达式入门:从模式匹配到数据提取实战
-
- 前言
- [一、`re` 模块快速上手](#一、
re模块快速上手) -
- [1.1 第一个正则示例](#1.1 第一个正则示例)
- [1.2 原生字符串 `r"..."`](#1.2 原生字符串
r"...")
- 二、核心匹配函数
-
- [2.1 `search()`:搜索第一个匹配](#2.1
search():搜索第一个匹配) - [2.2 `match()`:从字符串开头匹配](#2.2
match():从字符串开头匹配) - [2.3 `findall()`:查找所有匹配](#2.3
findall():查找所有匹配) - [2.4 `finditer()`:返回迭代器(推荐大文本使用)](#2.4
finditer():返回迭代器(推荐大文本使用)) - [2.5 `sub()`:替换匹配内容](#2.5
sub():替换匹配内容)
- [2.1 `search()`:搜索第一个匹配](#2.1
- 三、常用正则模式速查
-
- [3.1 字符类](#3.1 字符类)
- [3.2 量词](#3.2 量词)
- [3.3 边界与锚点](#3.3 边界与锚点)
- [3.4 贪婪与非贪婪](#3.4 贪婪与非贪婪)
- 四、捕获组与高级用法
-
- [4.1 分组捕获](#4.1 分组捕获)
- [4.2 命名分组](#4.2 命名分组)
- [4.3 前瞻与后顾](#4.3 前瞻与后顾)
- [4.4 常用模式速查表](#4.4 常用模式速查表)
- 五、编译正则:提升性能
- 六、实战案例:简易网页爬虫辅助工具
- 总结
- [✅ 亮点总结](#✅ 亮点总结)
- 适用场景
- 扩展方向
前言
正则表达式(Regular Expression)是处理文本的瑞士军刀。无论是验证用户输入、提取网页数据、清洗日志文件,还是批量替换文本,正则表达式都能用几行代码完成看似复杂的任务。Python内置的 re 模块提供了完整的正则表达式支持。
学习正则表达式的现实意义:在面试中,正则表达式是常见考点;在爬虫开发中,它是数据提取的核心工具;在日志分析中,它是错误排查的利器。然而,正则表达式也有一个"名声"------它看起来像天书,容易把简单问题复杂化。本文的目标是让你掌握正则的核心套路,学会"用合适的工具做合适的事",避免陷入过度依赖正则的陷阱。本文将从零带你掌握正则表达式的核心用法。
一、re 模块快速上手
1.1 第一个正则示例
正则表达式最简单的入门方式就是从一个具体的例子开始。下面这个例子演示了如何从一段文本中提取手机号码------这是正则表达式最经典的应用场景之一。
python
import re
text = "我的手机号是13812345678,请尽快联系我"
# 匹配中国大陆手机号
pattern = r"1[3-9]\d{9}"
match = re.search(pattern, text)
if match:
print(f"找到手机号: {match.group()}") # 找到手机号: 13812345678
print(f"起始位置: {match.start()}") # 7
print(f"结束位置: {match.end()}") # 18
关键解析 :re.search() 在整个字符串中搜索第一个匹配,返回一个 Match 对象。如果没找到匹配,它返回 None------所以在生产代码中务必先检查返回值是否为 None ,否则直接调用 .group() 会抛出 AttributeError。
1.2 原生字符串 r"..."
正则表达式中大量使用反斜杠,使用原生字符串 r"..." 可以避免转义混乱:
python
# 繁琐的普通字符串
pattern1 = "\\d+\\.\\d+"
# 简洁的原生字符串(推荐)
pattern2 = r"\d+\.\d+"
print(pattern1 == pattern2) # True
二、核心匹配函数
re 模块提供了多个匹配函数,各自有不同的语义和使用场景。理解它们的区别是正则入门的关键。常见面试题 :"match() 和 search() 有什么区别?"------答案很简单,match() 只从字符串开头匹配,search() 搜索整个字符串。
2.1 search():搜索第一个匹配
python
text = "价格:¥19.9,折扣价:¥9.9"
pattern = r"¥(\d+\.\d+)"
match = re.search(pattern, text)
print(match.group()) # ¥19.9
print(match.group(1)) # 19.9 (第一个捕获组)
2.2 match():从字符串开头匹配
python
# match() 只匹配开头
print(re.match(r"\d+", "123abc")) # <re.Match object>
print(re.match(r"\d+", "abc123")) # None (不在开头)
# search() 搜索任意位置
print(re.search(r"\d+", "abc123")) # <re.Match object>
2.3 findall():查找所有匹配
python
text = "我有苹果5个,橘子12个,香蕉8个"
pattern = r"(\w+)(\d+)个"
# 有捕获组时返回元组列表
result = re.findall(pattern, text)
print(result)
# [('苹果', '5'), ('橘子', '12'), ('香蕉', '8')]
for fruit, count in result:
print(f"{fruit}: {count}个")
2.4 finditer():返回迭代器(推荐大文本使用)
python
text = "Python 3.9, Python 3.10, Python 3.11"
pattern = r"Python (\d+\.\d+)"
for match in re.finditer(pattern, text):
print(f"版本 {match.group(1)} 位于位置 {match.start()}")
2.5 sub():替换匹配内容
python
# 脱敏手机号
text = "联系人: 张三 13812345678, 李四 13987654321"
masked = re.sub(r"(\d{3})\d{4}(\d{4})", r"\1****\2", text)
print(masked) # 联系人: 张三 138****5678, 李四 139****4321
# 使用函数进行替换
def censor(match):
word = match.group()
return word[0] + "*" * (len(word) - 1)
text = "这个项目简直太棒了,效率非常高"
result = re.sub(r"[了得]", censor, text)
print(result) # 这个项目简直太棒*,效率非常高
三、常用正则模式速查
正则表达式的元字符是构建匹配模式的"积木"。这些符号初看可能很抽象,但每个都有明确的使用场景。学习技巧 :不要试图一次记住所有元字符------先熟练 \d、\w、.、*、+、? 这几个最常用的,剩下的需要时再查即可。
3.1 字符类
python
patterns = {
r"\d": "匹配任意数字 [0-9]",
r"\D": "匹配任意非数字",
r"\w": "匹配字母数字下划线 [a-zA-Z0-9_]",
r"\W": "匹配非字母数字下划线",
r"\s": "匹配空白字符(空格、制表符、换行等)",
r"\S": "匹配非空白字符",
r".": "匹配任意字符(除换行符)",
}
test = "A 1 _ 中 @"
print(re.findall(r"\d", test)) # ['1']
print(re.findall(r"\w", test)) # ['A', '1', '_', '中']
print(re.findall(r"\s", test)) # [' ', ' ', ' ', ' ']
3.2 量词
python
test_cases = {
r"a*": "a出现0次或多次",
r"a+": "a出现1次或多次",
r"a?": "a出现0次或1次",
r"a{3}": "a恰好出现3次",
r"a{2,4}": "a出现2到4次",
r"a{2,}": "a出现至少2次",
}
print(re.findall(r"a*", "baaac")) # ['', 'aaa', '', '']
print(re.findall(r"a+", "baaac")) # ['aaa']
print(re.findall(r"a{2,3}", "baaaaac")) # ['aaa']
print(re.findall(r"\d{3,4}", "电话: 010-1234567")) # ['010', '1234']
3.3 边界与锚点
边界匹配是正则表达式中的重要概念,它不消耗字符,只匹配"位置"。^ 匹配行首,$ 匹配行尾,\b 匹配单词边界。理解边界匹配对于精确匹配非常重要------比如你想匹配独立的单词 "cat" 而不是 "category" 中的 "cat",就需要用到 \bcat\b。re.MULTILINE 标志让 ^ 和 $ 匹配每行的开头和结尾(而非整个字符串的开头和结尾)。
python
test = """第一行
第二行
第三行"""
# ^ 匹配行首,$ 匹配行尾
print(re.findall(r"^第.", test, re.MULTILINE))
# ['第一', '第二', '第三']
# \b 匹配单词边界
text = "cat category scatter cat"
print(re.findall(r"\bcat\b", text)) # ['cat', 'cat']
3.4 贪婪与非贪婪
这是正则表达式中最经典的"坑"之一。贪婪匹配 是正则的默认行为------量词(*, +, ?, {})会尽可能多地匹配字符。这在很多场景下会导致意料之外的结果,比如在HTML中使用 .* 匹配时可能跨过多个标签。非贪婪匹配 通过在量词后加 ? 实现(如 *?, +?, ??),它会尽可能少地匹配。一个实用的经验法则:在HTML/XML解析、引号内容提取等场景中,几乎总是应该使用非贪婪匹配。
python
html = "<div>内容1</div><div>内容2</div>"
# 贪婪匹配(默认):匹配尽可能多的内容
greedy = re.findall(r"<div>.*</div>", html)
print(greedy) # ['<div>内容1</div><div>内容2</div>']
# 非贪婪匹配:加 ? 匹配尽可能少的内容
lazy = re.findall(r"<div>.*?</div>", html)
print(lazy) # ['<div>内容1</div>', '<div>内容2</div>']
四、捕获组与高级用法
4.1 分组捕获
python
# 提取日期
text = "今天是2024-01-15,明天是2024-01-16"
pattern = r"(\d{4})-(\d{2})-(\d{2})"
for match in re.finditer(pattern, text):
year, month, day = match.groups()
print(f"{year}年{month}月{day}日")
4.2 命名分组
python
# 使用 (?P<name>...) 给分组命名
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
text = "出生日期: 1995-08-22"
match = re.search(pattern, text)
if match:
print(match.group("year")) # 1995
print(match.group("month")) # 08
print(match.group("day")) # 22
print(match.groupdict())
# {'year': '1995', 'month': '08', 'day': '22'}
4.3 前瞻与后顾
python
# 正向肯定前瞻 (?=...):后面必须跟着...
text = "100元 200美元 300元 400欧元"
# 匹配后面跟着"元"的数字
result = re.findall(r"\d+(?=元)", text)
print(result) # ['100', '300']
# 正向肯定后顾 (?<=...):前面必须是...
# 匹配前面是"$"的数字
text = "商品: $199, $299, ¥599"
result = re.findall(r"(?<=\$)\d+", text)
print(result) # ['199', '299']
# 负向前瞻 (?!...):后面不能跟着...
text = "Python3 Python2 Python"
result = re.findall(r"Python(?!\d)", text)
print(result) # ['Python'] (不匹配后面有数字的)
4.4 常用模式速查表
| 模式 | 含义 | 示例 |
|---|---|---|
\d{3,4} |
3-4位数字 | 区号 |
[\u4e00-\u9fa5] |
中文字符 | 匹配中文 |
[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} |
Email地址 | 基本邮箱匹配 |
1[3-9]\d{9} |
中国大陆手机号 | 13812345678 |
\d{17}[\dXx] |
18位身份证 | 身份证号 |
https?://[^\s]+ |
URL | 网址 |
五、编译正则:提升性能
频繁使用的正则表达式应该预编译 。re.compile() 将正则模式编译为一个正则对象,后续可以反复使用,避免每次调用 re.match() 时重复解析和编译。在大批量文本处理中,预编译能带来显著的性能提升------在循环内重复使用同一个正则表达式时,编译版本比未编译版本快30%-50%。最佳实践:将编译后的正则对象定义为模块级别的常量,在整个程序中复用。
python
import time
# 预编译
email_pattern = re.compile(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
)
emails = [
"user@example.com",
"invalid-email",
"test@company.co.uk",
"no@domain",
"good@email.org",
]
for email in emails:
is_valid = bool(email_pattern.match(email))
status = "✓" if is_valid else "✗"
print(f"{status} {email}")
# 性能测试
test_text = "hello@world.com " * 10000
start = time.time()
for _ in range(1000):
re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', "test@example.com")
print(f"未编译耗时: {time.time() - start:.3f}s")
start = time.time()
for _ in range(1000):
email_pattern.match("test@example.com")
print(f"编译后耗时: {time.time() - start:.3f}s")
六、实战案例:简易网页爬虫辅助工具
下面的实战案例将前面学到的知识整合成几个实用的函数------邮箱提取、URL提取、HTML表格解析、敏感信息脱敏。这些函数都是实际爬虫和数据清洗项目中可以直接复用的工具代码。
python
import re
def extract_emails(text):
"""从文本中提取所有邮箱地址"""
pattern = re.compile(
r'\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b'
)
return pattern.findall(text)
def extract_urls(text):
"""从文本中提取所有URL"""
pattern = re.compile(
r'https?://[^\s<>"\']+|www\.[^\s<>"\']+'
)
return pattern.findall(text)
def parse_html_table(html):
"""解析简易HTML表格"""
# 提取所有行
rows = re.findall(r'<tr>(.*?)</tr>', html, re.DOTALL)
table_data = []
for row in rows:
# 提取每行中的单元格
cells = re.findall(r'<t[dh]>(.*?)</t[dh]>', row, re.DOTALL)
# 清理HTML标签
clean_cells = [re.sub(r'<.*?>', '', cell).strip() for cell in cells]
table_data.append(clean_cells)
return table_data
def mask_sensitive_info(text):
"""脱敏处理:隐藏身份证号、手机号、银行卡号"""
text = re.sub(r'(\d{3})\d{4}(\d{4})', r'\1****\2', text)
text = re.sub(r'(\d{6})\d{8}(\d{4})', r'\1********\2', text)
text = re.sub(r'(\d{4})\s?(\d{4})\s?(\d{4})\s?(\d{4})',
r'\1 **** **** \4', text)
return text
# 测试
sample = """
联系人: 张三, 邮箱: zhang@test.com
官网: https://www.example.com
手机: 13812345678
身份证: 320102199001011234
<table>
<tr><th>姓名</th><th>分数</th></tr>
<tr><td>张三</td><td>95</td></tr>
<tr><td>李四</td><td>88</td></tr>
</table>
"""
print("提取邮箱:", extract_emails(sample))
print("提取URL:", extract_urls(sample))
print("解析表格:", parse_html_table(sample))
print("脱敏后:", mask_sensitive_info(sample))
总结
正则表达式是文本处理的利器,Python re 模块的核心API:
search()查找第一个匹配,findall()查找所有匹配sub()替换匹配内容,split()按模式分割字符串- 使用
r"..."原生字符串避免转义困扰 - 频繁使用的正则用
re.compile()预编译提升性能 - 非贪婪匹配
*?和+?在HTML解析等场景中非常实用
正则表达式初看可能有些晦涩,但一旦掌握,处理文本的效率和优雅程度都会显著提升。建议收藏本文用作速查参考。下一篇我们将进入多线程与并发编程的世界。
✅ 亮点总结
- 系统讲解
re模块五大核心函数(search/findall/sub/split/match),附带速查表 - 元字符、量词、分组、零宽断言循序渐进,从基础模式到复杂匹配
- 预编译
re.compile()提升性能,非贪婪匹配解决 HTML 解析中的常见坑 - 实战案例:邮箱/URL 提取、敏感信息脱敏、HTML 表格解析,即学即用
适用场景
- 数据清洗:从日志/爬虫结果中提取结构化字段(日期、IP、金额等)
- 表单验证:校验用户输入的手机号、邮箱、身份证号等格式合法性
- 代码重构:批量替换项目中的旧 API 调用、统一代码风格
扩展方向
- 学习正则表达式的性能优化,了解回溯陷阱和原子组
- 结合
re的 flags 参数掌握多行匹配、忽略大小写等高级模式 - 探索
regex第三方库,支持更丰富的正则特性(如递归匹配、模糊匹配)