Python正则表达式re模块用法详解 从基础语法到实战技巧解决文本匹配数据清洗等常见问题
引言:正则表达式在Python中的重要性
正则表达式(Regular Expression)是处理文本匹配和数据清洗的利器,而Python的
re
模块提供了强大且灵活的正则表达式支持。在数据科学、网络爬虫、日志分析和自动化脚本中,正则表达式几乎无处不在。它能帮助我们快速定位、提取、替换复杂文本模式,极大提升开发效率。
本文将从
re
模块的基础语法讲起,逐步深入到高级用法和实战技巧,涵盖文本匹配、数据清洗等常见场景,并通过大量代码示例帮助你彻底掌握这一工具。
1. re模块基础语法
1.1 正则表达式的基本构成
正则表达式由普通字符(如字母、数字)和特殊字符(元字符)组成。普通字符匹配自身,而元字符具有特殊含义。
常用元字符表:
元字符
说明
.
匹配除换行符外的任意字符
^
匹配字符串开头
$
匹配字符串结尾
*
匹配前一个字符0次或多次
+
匹配前一个字符1次或多次
?
匹配前一个字符0次或1次
{n}
匹配前一个字符n次
{n,}
匭配前一个字符至少n次
{n,m}
匹配前一个字符n到m次
[]
字符集,匹配方括号内的任意字符
|
或运算符,匹配左右任意一个表达式
()
分组,将多个字符作为一个整体处理
1.2 使用re模块的基本步骤
Python中使用正则表达式通常遵循以下步骤:
导入
re
模块
定义正则表达式字符串
使用
re
模块的函数进行匹配、查找或替换
import re
# 示例:匹配数字
pattern = r’\d+’
text = “我有2024本书和3个苹果”
matches = re.findall(pattern, text)
print(matches) # 输出: [‘2024’, ‘3’]
注意:
在Python中,正则表达式字符串前加
r
表示原始字符串,避免转义字符的干扰。
2. re模块核心函数详解
re
模块提供了多个函数,用于不同的文本处理需求。下面详细介绍每个函数的用法和示例。
2.1 re.match():从开头匹配
re.match(pattern, string, flags=0)
从字符串
开头
匹配,如果开头不匹配则返回
None
。
import re
text = “Hello 2024 World”
pattern = r’\d+’
# match只检查开头
result = re.match(pattern, text)
print(result) # 输出: None,因为开头是字母不是数字
# 正确用法:匹配开头的数字
text2 = “2024 Hello World”
result2 = re.match(pattern, text2)
if result2:
print(result2.group()) # 输出: 2024
2.2 re.search():扫描整个字符串
re.search(pattern, string, flags=0)
扫描整个字符串,返回
第一个匹配
的对象。
import re
text = “Hello 2024 World, I have 100 apples”
pattern = r’\d+’
result = re.search(pattern, text)
if result:
print(result.group()) # 输出: 2024
print(result.span()) # 输出: (6, 10) 位置信息
2.3 re.findall():返回所有匹配项
re.findall(pattern, string, flags=0)
返回一个列表,包含所有匹配的子串。
import re
text = “订单号:A123, B456, C789″
pattern = r'[A-Z]\d+’
matches = re.findall(pattern,订单号:A123, B456, C789”)
print(matches) # 输出: [‘A123’, ‘B456’, ‘B789′]
2.4 re.finditer():返回迭代器
re.finditer(pattern, string, flags=0)
返回一个迭代器,每个元素是匹配对象,适合处理大文本。
import re
text = “日期:2024-01-15, 2024-02-20″
pattern = r’\d{4}-\d{2}-\d{2}’
for match in re.finditer(pattern, text):
print(f”匹配到: {match.group()} 位置: {match.span()}”)
# 输出:
# 匹配到: 2024-01-15 位置: (3, 13)
# 匹配到: 2024-02-20 位置: (15, 25)
2.5 re.sub():替换匹配内容
re.sub(pattern, repl, string, count=0, flags=0)
将匹配的内容替换为指定字符串。
import re
text = “电话:138-1234-5678,手机:139-8765-4321”
pattern = r’\d{3}-\d{4}-\d{4}’
# 替换为***
new_text = re.sub(pattern, ‘***-****-****’, text)
print(new_text) # 输出: 电话:***-****-****,手机:***-****-****
# 使用函数替换
def mask_phone(match):
return match.group()[:3] + ‘-****-‘ + match.group()[-4:]
new_text2 = re.sub(pattern, mask_phone, text)
print(new_text2) # 输出: 138-****-5678, 139-****-4321
2.6 re.split():分割字符串
re.split(pattern, string, maxsplit=0, flags=0)
按模式分割字符串。
import re
text = “苹果,香蕉;橘子|西瓜”
pattern = r'[,;|]’
fruits = re.split(pattern, text)
print(fruits) # 输出: [‘苹果’, ‘香蕉’, ‘橘子’, ‘西瓜’]
2.7 re.compile():预编译正则表达式
当同一个正则表达式需要多次使用时,预编译可以提高效率。
import re
# 预编译
pattern = re.compile(r’\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b’)
text = “联系邮箱:user@example.com 或者 admin@test.org”
matches = pattern.findall(text)
print(matches) # 输出: [‘user@example.com’, ‘admin@test.org’]
# 使用编译后的对象调用其他方法
match = pattern.search(text)
if match:
print(match.group())
3. 分组与捕获
分组是正则表达式的强大功能,允许我们提取匹配内容的特定部分。
3.1 基本分组
使用
()
创建分组,
group(0)
返回整个匹配,
group(1)
返回第一个分组。
import re
text = “姓名:张三,年龄:25岁”
pattern = r’姓名:(\w+),年龄:(\d+)岁’
match = re.search(pattern, text)
if match:
print(“完整匹配:”, match.group(0)) # 姓名:张三,年龄:25岁
print(“姓名:”, match.group(1)) # 张三
print(“年龄:”, match.group(2)) # 25
print(“所有分组:”, match.groups()) # (‘张三’, ’25’)
3.2 命名分组
Python支持命名分组,语法为
(?Ppattern)
,通过名字访问更清晰。
import re
text = “订单:A123,金额:¥500”
pattern = r’订单:(?P[A-Z]\d+),金额:¥(?P\d+)’
match = re.search(pattern, text)
if match:
print(“订单ID:”, match.group(‘order_id’)) # A123
分组:(?Ppattern) 创建一个命名分组,可以通过名字访问。
print(“金额:”, match.group(‘amount’)) # 500
print(“所有命名分组:”, match.groupdict()) # {‘order_id’: ‘A123’, ‘amount’: ‘500’}
3.3 非捕获分组
使用
(?:)
创建非捕获分组,只匹配不捕获,提高效率。
import re
text = “2024-01-15 和 2024-02-20”
pattern = r'(?:\d{4})-(\d{2})-(\d{2})’
matches = re.findall(pattern, text)
print(matches) # 输出: [(’01’, ’15’), (’02’, ’20’)],年份未被捕获
3.4 分组的后向引用
后向引用允许在正则表达式中引用前面的分组。
import re
# 匹配重复单词
text = “这个 这个 问题 问题”
pattern = r'(\w+)\s+\1′ # \1 引用第一个分组
matches = re.findall(pattern, text)
print(matches) # 输出: [‘这个’, ‘问题’]
3.5 分组的前后行断言
前后行断言(Lookaround)用于匹配特定条件但不包含在最终匹配中。
正向先行断言
(?=…)
:匹配后面的模式
负向先行断言
(?!…)
:不匹配后面的模式
正向后行断言
(?<=...)
:匹配前面的模式
负向后行断言
(?内容1
内容2
”
pattern = r’
.*
‘
# 贪婪匹配:匹配整个字符串
match = re.search(pattern, text)
print(match.group()) # 输出:
内容1
内容2
5.2 非贪婪匹配示例
import re
text = “
内容1
内容2
”
pattern = r’
.*?
‘
# 非贪婪匹配:只匹配第一个
matches = re.findall(pattern, text)
print(matches) # 输出: [‘
内容1
‘, ‘
内容2
‘]
5.3 贪婪与非贪婪对比
import re
text = “abc123def456”
pattern_greedy = r’\d+.*\d+’ # 贪婪:匹配所有数字和中间内容
pattern_non_greedy = r’\d+.*?\d+’ # 非贪婪:匹配第一个数字和最后一个数字之间的内容
print(re.search(pattern_greedy, text).group()) # 输出: 123def456
print(re.search(pattern_non_greedy, text).group()) # 123def456
# 注意:这个例子中贪婪和非贪婪结果相同,因为.*?在遇到第一个\d+后立即停止
# 更明显的例子:
text2 = “123456”
pattern_greedy = r’\d+.*’ # 匹配所有
pattern_non_greedy = r’\d+.*?’ # 匹配123456(因为.*?匹配0次)
print(re.search(pattern_greedy, text2).group()) # 123456
print(re.search(pattern_non_greedy, text2).group()) # 123456
# 需要更复杂的例子:
text3 = “a1b2c3d”
pattern_greedy = r’a.*c’ # a1b2c
pattern_non_greedy = r’a.*?c’ # a1b2c
print(re.search(pattern_greedy, text3).group()) # a1b2c
print(re.search(pattern_non_greedy, text3).group()) # a1b2c
# 真正的区别:
text4 = “abc123def456ghi”
pattern_greedy = r’\d+.*\d+’ # 123def456
pattern_non_greedy = r’\d+.*?\d+’ # 123def456
print(re.search(pattern_greedy, text4).group()) # 123def456
print(re.search(pattern_non_greedy, text4).group()) # 123def456
# 让我们用HTML标签来展示区别:
text5 = “
第一
第二
”
pattern_greedy = r’
.*
‘ # 匹配整个字符串
pattern_non_greedy = r’
.*?
‘ # 匹配第一个标签
print(re.search(pattern_greedy, text5).group()) #
第一
第二
print(re.search(pattern_non_greedy, text5).group()) #
第一
5.4 实际应用场景:提取HTML内容
import re
html = “
段落1
段落2
段落3
”
# 错误:贪婪匹配会匹配整个字符串
wrong = re.findall(r’
.*
‘, html)
print(wrong) # 输出: [‘
段落1
段落2
段落3
‘]
# 正确:非贪婪匹配每个标签
correct = re.findall(r’
.*?
‘, html)
print(correct) # 输出: [‘
段落1
‘, ‘
段落2
‘, ‘
段落3
‘]
5.5 贪婪与非贪婪的性能考虑
在处理大文本时,非贪婪模式通常性能更好,因为它会尽快结束匹配。但有时贪婪模式更高效,例如匹配引号内的内容:
import re
text = ‘字符串:”abc”def”ghi”‘
pattern_greedy = r’”[^”]*”‘ # 匹配引号内的内容
pattern_non_greedy = r’”.*?”‘ # 非贪婪,但可能匹配更短
print(re.findall(pattern_greedy, text)) # 输出: [‘”abc”‘, ‘”ghi”‘]
print(re.findall(pattern_non_greedy, text)) # 输出: [‘”abc”‘, ‘”ghi”‘]
# 这个例子中两者结果相同,因为[^”]*明确界定了边界
6. 实战技巧:文本匹配与数据清洗
6.1 邮箱地址提取
import re
text = “联系邮箱:user@example.com,备用邮箱:admin@test.org,错误邮箱:invalid@.com”
pattern = r’\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b’
emails = re.findall(pattern, text)
print(emails) # 输出: [‘user@example.com’, ‘admin@test.org’]
6.2 电话号码提取与格式化
import re
text = “电话:13812345678,手机:139-8765-4321,座机:010-12345678″
pattern = r'(\d{3})-?(\d{4})-?(\d{4})|\d{11}’
def format_phone(match):
if match.group(1): # 有分隔符的格式
return f”{match.group(1)}-{match.group(2)}-{match.group(3)}”
else: # 11位连续数字
full = match.group()
return f”{full[:3]}-{full[3:7]}-{full[7:]}”
formatted = re.sub(pattern, format_phone, text)
print(formatted) # 输出: 电话:138-1234-5678,手机:139-8765-4321,座机:010-12345678
6.3 清除HTML标签
import re
html = “
”
# 方法1:使用非贪婪匹配
clean_text = re.sub(r’<.*?>‘, ”, html)
print(clean_text) # 输出: 标题内容
# 方法2:更健壮的版本(处理属性中的>)
html2 = “
match = re.search(pattern, log)
if match:
date, time, level, message = match.groups()
print(f”日期: {date}”)
日期: 2024-01-15
print(f”时间: {time}”)
时间: 10:30:25
print(f”级别: {level}”)
级别: ERROR
print(f”消息: {message}”)
消息: 文件未找到:/path/to/file
6.6 复杂数据提取:CSV解析
import re
csv_line = ‘123,”Smith, John”, “New York, NY”, 5000.00′
# 匹配:数字,引号字符串,引号字符串,浮点数
pattern = r'(\d+),”([^”]+)”,”([^”]+)”,(\d+\.\d+)’
match = re.search(pattern, csv_line)
if match:
id, name, city, salary = match.groups()
print(f”ID: {id}, Name: {name}, City: {city}, Salary: {salary}”)
# 输出: ID: 123, Name: Smith, John, City: New York, NY, Salary: 5000.00
6.7 多条件匹配:使用|运算符
import re
text = “颜色:红色, 蓝色, 绿色, 黄色”
pattern = r’红色|蓝色|绿色’
colors = re.findall(pattern, text)
print(colors) # 输出: [‘红色’, ‘蓝色’, ‘绿色’]
6.8 使用分组进行复杂替换
import re
text = “2024-01-15 10:30:25″
# 将日期格式从 YYYY-MM-DD 改为 DD/MM/YYYY
pattern = r'(\d{4})-(\d{2})-(\d{2})’
def swap_date(match):
return f”{match.group(3)}/{match.group(2)}/{match.group(1)}”
new_text = re.sub(pattern, swap_date, text)
print(new_text) # 输出: 15/01/2024 10:30:25
6.9 处理Unicode字符
import re
text = “中文测试:你好世界,123abc”
pattern = r'[\u4e00-\u9fff]+’ # 匹配中文字符
chinese = re.findall(pattern, text)
print(chinese) # 输出: [‘中文测试’, ‘你好世界’]
6.10 处理多行文本块
import re
text = “””开始
内容行1
内容行2
结束”””
# 匹配从”开始”到”结束”之间的所有内容
pattern = r’开始(.*?)结束’
match = re.search(pattern, text, re.DOTALL)
if match:
print(match.group(1).strip()) # 输出: 内容行1 内容行2
7. 高级技巧与最佳实践
7.1 避免回溯灾难
回溯灾难是指正则表达式在匹配失败时进行大量不必要的回溯,导致性能急剧下降。常见于嵌套量词和贪婪匹配。
危险模式:
# 危险:嵌套量词可能导致回溯灾难
pattern = r'(a+)+b’
text = ‘aaaaaaaaaaaaaaaaaaaaaaaaaaaaac’ # 大量a后跟c,匹配失败
# 解决方案:使用原子分组(Python中不支持)或占有量词(Python中不支持)
# 替代方案:优化正则表达式结构
pattern = r’a+b’ # 简化结构
实际例子:
import re
import time
# 危险模式
dangerous_pattern = r'(a+)+b’
text = ‘a’ * 30 + ‘c’ # 30个a后跟c
start = time.time()
re.match(dangerous_pattern, text)
print(f”危险模式耗时: {time.time() – start:.4f}秒”)
# 安全模式
safe_pattern = r’a+b’
start = time.time()
re.match(safe_pattern, text)
print(f”安全模式耗时: {1:.4f}秒”)
7.2 使用原始字符串避免转义问题
import re
# 错误:需要双重转义
wrong = re.compile(‘\\d+\\.\\d+’) # 匹配浮点数
# 正确:使用原始字符串
right = re.compile(r’\d+\.\d+’) # 匹配浮点数
text = “3.14”
print(wrong.search(text).group()) # 3.14
print(right.search(text).group()) # 3.14
7.3 预编译正则表达式提升性能
import re
import time
# 未预编译
def without_compile():
text = “abc123def456ghi789”
for i in range(10000):
re.findall(r’\d+’, text)
# 预编译
pattern = re.compile(r’\d+’)
def with_compile():
text = “abc123def456ghi789″
for i in range(10000):
pattern.findall(text)
# 性能对比
start = time.time()
without_compile()
print(f”未预编译: {time.time() – start:.4f}秒”)
start = time.time()
with_compile()
print(f”预编译: {time.time() – start:.4f}秒”)
7.4 处理大型文本文件
import re
# 逐行处理大文件
def process_large_file(file_path):
pattern = re.compile(r’\d{4}-\d{2}-\d{2}’)
with open(file_path, ‘r’, encoding=’utf-8′) as f:
for line in f:
if pattern.search(line):
# 处理包含日期的行
print(line.strip())
# 示例:处理日志文件
# process_large_file(‘large_log.txt’)
7.5 调试正则表达式
import re
def debug_regex(pattern, text):
“””调试正则表达式”””
try:
compiled = re.compile(pattern)
print(f”模式: {pattern}”)
print(f”文本: {text}”)
print(f”匹配: {compiled.findall(text)}”)
return compiled
except re.error as e:
print(f”错误: {e}”)
return None
# 使用示例
debug_regex(r’\d+’, ‘abc123def’)
# 输出:
# 模式: \d+
# 文本: abc123def
# 匹配: [‘123′]
7.6 处理特殊场景:匹配中文、邮箱、URL等
import re
text = “””
邮箱:user@example.com
URL:https://www.example.com/path?query=1
中文:你好世界
日期:2024-01-15
“””
# 匹配邮箱
email_pattern = r’\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b’
print(“邮箱:”, re.findall(email_pattern, text))
# 匹配URL
url_pattern = r’https?://(?:[-\w.])+(?:[:]\d+)?(?:/(?:[\w/_.])*(?:\?\S+)?)?’
print(“URL:”, re.findall(url_pattern, text))
# 匹配中文
chinese_pattern = r'[\u4e00-\u9fff]+’
print(“中文:”, re.findall(chinese_pattern, text))
# 匹配日期
date_pattern = r’\d{4}-\d{2}-\d{2}’
print(“日期:”, re.findall(date_pattern, text))
7.7 使用re.VERBOSE提高可读性
import re
# 复杂的正则表达式
pattern = re.compile(r”’
\b # 单词边界
[A-Za-z0-9._%+-]+ # 用户名
@ # @符号
[A-Za-z0-9.-]+ # 域名
\. # 点
[A-Z|a-z]{2,} # 顶级域名
\b # 单词边界
”’, re.VERBOSE)
text = “联系邮箱:user@example.com”
matches = pattern.findall(text)
print(matches) # 输出: [‘user@example.com’]
7.8 处理Unicode字符集
import re
# 匹配中文字符
chinese_pattern = re.compile(r'[\u4e00-\u9fff]’)
# 匹配emoji(Unicode范围)
emoji_pattern = re.compile(r'[\U0001F600-\U0001F64F]’)
text = “你好😊世界🌍”
print(“中文字符:”, chinese_pattern.findall(text)) # [‘你’, ‘好’, ‘世’, ‘界’]
print(“Emoji:”, emoji_pattern.findall(text)) # [‘😊’, ‘🌍’]
7.9 性能优化:避免过度使用正则
import re
import time
# 不好的做法:用正则匹配固定前缀
def bad_example(text):
return re.match(r’prefix_’, text) is not None
# 好的做法:直接字符串操作
def good_example(text):
return text.startswith(‘prefix_’)
# 性能对比
text = “prefix_12345″
start = time.time()
for i in range(100000):
bad_example(text)
print(f”正则耗时: {time.time() – start:.4f}秒”)
start = time.time()
for i in100000):
good_example(text)
print(f”字符串操作耗时: {time.time() – start:.4f}秒”)
7.10 常见错误与解决方案
import re
# 错误1:忘记转义特殊字符
try:
pattern = r’file.txt’ # .是元字符
re.search(pattern, ‘file.txt’) # 匹配fileatxt
except:
pass
# 正确:转义.
pattern = r’file\.txt’
# 错误2:在循环中重复编译正则
def bad():
text = “abc123def”
for i in range(1000):
re.findall(r’\d+’, text) # 每次都编译
def good():
pattern = re.compile(r’\d+’)
text = “abc123def”
for i in1000):
pattern.findall(text)
# 错误3:忽略Unicode
text = “中文123”
pattern = r’\w+’ # 在Python3中默认支持Unicode,但Python2需要re.UNICODE
print(re.findall(pattern, text)) # [‘中文123’]
8. 总结
Python的
re
模块是文本处理的利器,掌握它能极大提升开发效率。核心要点:
基础语法
:元字符、字符集、量词是构建正则的基础
核心函数
:match、search、findall、finditer、sub、split各司其职
分组捕获
:提取特定内容,支持命名分组和非捕获分组
标志位
:re.I、re.M、re.S等改变匹配行为
贪婪与非贪婪
:根据场景选择,避免匹配过多内容
实战技巧
:邮箱、电话、HTML、日志等场景的解决方案
性能优化
:预编译、避免回溯灾难、合理使用字符串操作
最佳实践
:使用原始字符串、调试技巧、处理Unicode
记住:
正则表达式不是万能的
,对于简单场景,字符串内置方法可能更高效。但在复杂模式匹配中,正则表达式无可替代。
最后建议
:使用在线正则表达式测试工具(如regex101.com)来调试和验证你的正则表达式,这能极大提高开发效率。