正则表达式:文本处理的瑞士军刀
🎯 前言:当文本遇上神奇的密码
想象一下,你是一个图书管理员,面对着一堆乱七八糟的书籍信息:
- “联系电话:138-1234-5678”
- “邮箱地址:zhang.san@gmail.com”
- “身份证号:110101199001011234”
- “QQ号码:12345678”
如果让你一个个手工整理,估计你会疯掉。但是,如果你掌握了正则表达式这把"瑞士军刀",这些看似繁琐的文本处理任务就会变得轻松愉快!
正则表达式(Regular Expression,简称 regex 或 regexp)就像是一种神奇的"文本搜索密码",它能够帮你在海量文本中精准定位你想要的内容。今天我们就来学习这门让文本处理变得如丝般顺滑的技艺!
📚 目录
🧠 什么是正则表达式
生活中的"模式匹配"
在解释正则表达式之前,我们先来看看生活中的例子。你有没有这样的经历:
- 在一堆照片中找出所有包含"生日"的文件名
- 在通讯录中找出所有138开头的手机号
- 在邮件中找出所有的网址链接
这些其实都是"模式匹配"的过程,而正则表达式就是用来描述这些"模式"的一种特殊语言。
正则表达式的本质
正则表达式本质上就是一个字符串,但这个字符串有特殊的含义——它描述了一种文本模式。比如:
\d{3}-\d{4}-\d{4}
描述了"3位数字-4位数字-4位数字"的模式\w+@\w+\.\w+
描述了基本的邮箱格式模式
📖 基础语法:学会写"文本密码"
字符匹配:最基本的积木
import re
# 直接匹配字符
text = "Hello World"
pattern = "Hello"
result = re.search(pattern, text)
print(result) # <re.Match object; span=(0, 5), match='Hello'>
特殊字符:正则表达式的"魔法符号"
1. 点号(.):万能替身
# . 可以匹配任意单个字符(除了换行符)
pattern = "H.llo"
text1 = "Hello" # 匹配
text2 = "Hallo" # 匹配
text3 = "H@llo" # 匹配
text4 = "Hllo" # 不匹配
for text in [text1, text2, text3, text4]:
if re.search(pattern, text):
print(f"'{text}' 匹配成功!")
else:
print(f"'{text}' 匹配失败!")
2. 星号(*):贪婪的小怪兽
# * 表示前面的字符可以出现0次或多次
pattern = "ab*c"
texts = ["ac", "abc", "abbc", "abbbc", "axc"]
for text in texts:
if re.search(pattern, text):
print(f"'{text}' 匹配成功!")
else:
print(f"'{text}' 匹配失败!")
3. 加号(+):至少要有一个
# + 表示前面的字符至少出现1次
pattern = "ab+c"
texts = ["ac", "abc", "abbc", "abbbc"]
for text in texts:
if re.search(pattern, text):
print(f"'{text}' 匹配成功!")
else:
print(f"'{text}' 匹配失败!")
4. 问号(?):可有可无的存在
# ? 表示前面的字符可以出现0次或1次
pattern = "colou?r"
texts = ["color", "colour", "colouur"]
for text in texts:
if re.search(pattern, text):
print(f"'{text}' 匹配成功!")
else:
print(f"'{text}' 匹配失败!")
字符类:给字符分分类
1. 方括号:自定义字符集合
# [abc] 匹配a、b、c中的任意一个
pattern = "[abc]at"
texts = ["cat", "bat", "rat", "hat"]
for text in texts:
if re.search(pattern, text):
print(f"'{text}' 匹配成功!")
else:
print(f"'{text}' 匹配失败!")
2. 范围表示:偷个懒的写法
# [a-z] 匹配任意小写字母
# [0-9] 匹配任意数字
# [A-Za-z0-9] 匹配任意字母或数字
pattern = "[0-9]+" # 匹配一个或多个数字
text = "我今年25岁,电话是138-1234-5678"
numbers = re.findall(pattern, text)
print(numbers) # ['25', '138', '1234', '5678']
3. 预定义字符类:常用的简写
# \d 等价于 [0-9]
# \w 等价于 [a-zA-Z0-9_]
# \s 等价于 [ \t\n\r\f\v](空白字符)
text = "用户名:zhang123,年龄:25岁"
# 提取数字
numbers = re.findall(r'\d+', text)
print(f"数字:{numbers}") # ['123', '25']
# 提取单词字符
words = re.findall(r'\w+', text)
print(f"单词:{words}") # ['zhang123', '25']
💻 Python中的正则表达式模块
re模块:Python的正则表达式工具箱
import re
# 常用函数一览
text = "今天是2024年1月1日,明天是2024年1月2日"
# 1. re.search() - 找到第一个匹配
result = re.search(r'\d{4}年\d{1,2}月\d{1,2}日', text)
if result:
print(f"找到了:{result.group()}") # 找到了:2024年1月1日
# 2. re.findall() - 找到所有匹配
dates = re.findall(r'\d{4}年\d{1,2}月\d{1,2}日', text)
print(f"所有日期:{dates}") # ['2024年1月1日', '2024年1月2日']
# 3. re.sub() - 替换匹配的内容
new_text = re.sub(r'\d{4}年', '2025年', text)
print(f"替换后:{new_text}") # 今天是2025年1月1日,明天是2025年1月2日
# 4. re.split() - 按模式分割字符串
data = "苹果,香蕉;橘子:葡萄"
fruits = re.split(r'[,:;]', data)
print(f"水果列表:{fruits}") # ['苹果', '香蕉', '橘子', '葡萄']
编译正则表达式:提高效率的小技巧
# 如果同一个正则表达式要使用多次,可以先编译
pattern = re.compile(r'\d{3}-\d{4}-\d{4}')
phones = [
"我的电话是138-1234-5678",
"客服热线:400-1234-5678",
"传真号码:010-8888-9999",
"这不是电话号码"
]
for phone in phones:
match = pattern.search(phone)
if match:
print(f"找到电话:{match.group()}")
else:
print("未找到电话号码")
🚀 实战案例:从简单到复杂
案例1:验证邮箱格式
def validate_email(email):
"""
验证邮箱格式是否正确
"""
# 基础版本
basic_pattern = r'\w+@\w+\.\w+'
# 进阶版本(更严格)
advanced_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if re.match(advanced_pattern, email):
return True
return False
# 测试不同的邮箱
emails = [
"zhangsan@gmail.com", # 正确
"li.si@163.com", # 正确
"wang@company.com.cn", # 正确
"invalid-email", # 错误
"@gmail.com", # 错误
"test@", # 错误
]
for email in emails:
result = "✅ 有效" if validate_email(email) else "❌ 无效"
print(f"{email}: {result}")
案例2:提取网页中的链接
def extract_links(html_content):
"""
从HTML内容中提取所有链接
"""
# 匹配 <a href="..."> 标签中的链接
pattern = r'<a\s+href=["\'](.*?)["\']'
links = re.findall(pattern, html_content, re.IGNORECASE)
return links
# 示例HTML内容
html = '''
<html>
<body>
<a href="https://www.python.org">Python官网</a>
<a href="mailto:admin@example.com">联系我们</a>
<a href="/about">关于我们</a>
<a HREF='https://github.com'>GitHub</a>
</body>
</html>
'''
links = extract_links(html)
print("提取到的链接:")
for i, link in enumerate(links, 1):
print(f"{i}. {link}")
案例3:解析日志文件
def parse_log_file(log_content):
"""
解析服务器日志文件
"""
# 典型的Apache日志格式
# 127.0.0.1 - - [25/Dec/2023:10:00:00 +0000] "GET /index.html HTTP/1.1" 200 1234
pattern = r'(\d+\.\d+\.\d+\.\d+).*?\[(.*?)\].*?"(\w+)\s+(.*?)\s+HTTP.*?"\s+(\d+)\s+(\d+)'
matches = re.findall(pattern, log_content)
logs = []
for match in matches:
ip, timestamp, method, url, status, size = match
logs.append({
'ip': ip,
'timestamp': timestamp,
'method': method,
'url': url,
'status': int(status),
'size': int(size)
})
return logs
# 示例日志内容
log_content = '''
127.0.0.1 - - [25/Dec/2023:10:00:00 +0000] "GET /index.html HTTP/1.1" 200 1234
192.168.1.1 - - [25/Dec/2023:10:05:00 +0000] "POST /login HTTP/1.1" 302 0
10.0.0.1 - - [25/Dec/2023:10:10:00 +0000] "GET /api/users HTTP/1.1" 404 567
'''
logs = parse_log_file(log_content)
print("解析到的日志:")
for log in logs:
print(f"IP: {log['ip']}, 状态: {log['status']}, URL: {log['url']}")
案例4:数据清洗与格式化
def clean_phone_numbers(text):
"""
清洗和格式化电话号码
"""
# 匹配各种格式的电话号码
patterns = [
r'(\d{3})-(\d{4})-(\d{4})', # 138-1234-5678
r'(\d{3})\s+(\d{4})\s+(\d{4})', # 138 1234 5678
r'(\d{3})\.(\d{4})\.(\d{4})', # 138.1234.5678
r'(\d{11})', # 13812345678
]
def format_phone(match):
if len(match.groups()) == 3:
return f"{match.group(1)}-{match.group(2)}-{match.group(3)}"
else:
# 对于11位连续数字,分割为3-4-4格式
phone = match.group(1)
return f"{phone[:3]}-{phone[3:7]}-{phone[7:]}"
result = text
for pattern in patterns:
result = re.sub(pattern, format_phone, result)
return result
# 测试数据
messy_text = '''
联系方式:
张三:138 1234 5678
李四:139.5678.9012
王五:13687654321
赵六:158-7890-1234
'''
cleaned_text = clean_phone_numbers(messy_text)
print("清洗后的文本:")
print(cleaned_text)
🔧 高级技巧:让你的正则更强大
1. 分组捕获:抓住重要信息
# 使用括号创建捕获组
pattern = r'(\d{4})-(\d{2})-(\d{2})'
text = "今天是2024-01-15,明天是2024-01-16"
for match in re.finditer(pattern, text):
print(f"完整匹配:{match.group(0)}")
print(f"年份:{match.group(1)}")
print(f"月份:{match.group(2)}")
print(f"日期:{match.group(3)}")
print("---")
2. 命名捕获组:给组起个名字
# 使用 (?P<name>pattern) 创建命名组
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
text = "生日:1990-05-20"
match = re.search(pattern, text)
if match:
print(f"年份:{match.group('year')}")
print(f"月份:{match.group('month')}")
print(f"日期:{match.group('day')}")
# 也可以用字典的方式访问
date_dict = match.groupdict()
print(f"日期字典:{date_dict}")
3. 非捕获组:只匹配不捕获
# 使用 (?:pattern) 创建非捕获组
pattern = r'(?:Mr|Mrs|Ms)\.\s+(\w+)'
text = "Mr. Smith 和 Mrs. Johnson 来了"
matches = re.findall(pattern, text)
print(f"姓名:{matches}") # ['Smith', 'Johnson']
4. 前瞻和后瞻:偷看前后的内容
# 正向前瞻 (?=pattern)
pattern = r'\d+(?=元)' # 匹配后面跟着"元"的数字
text = "价格是100元,重量是50公斤"
prices = re.findall(pattern, text)
print(f"价格:{prices}") # ['100']
# 负向前瞻 (?!pattern)
pattern = r'\d+(?!元)' # 匹配后面不跟着"元"的数字
weights = re.findall(pattern, text)
print(f"非价格数字:{weights}") # ['50']
5. 贪婪与非贪婪匹配
text = '<div>内容1</div><div>内容2</div>'
# 贪婪匹配(默认)
greedy_pattern = r'<div>.*</div>'
greedy_match = re.search(greedy_pattern, text)
print(f"贪婪匹配:{greedy_match.group()}")
# 结果:<div>内容1</div><div>内容2</div>
# 非贪婪匹配
non_greedy_pattern = r'<div>.*?</div>'
non_greedy_matches = re.findall(non_greedy_pattern, text)
print(f"非贪婪匹配:{non_greedy_matches}")
# 结果:['<div>内容1</div>', '<div>内容2</div>']
🎯 常见应用场景
1. 数据验证工具箱
class DataValidator:
"""数据验证工具类"""
@staticmethod
def validate_phone(phone):
"""验证手机号码"""
pattern = r'^1[3-9]\d{9}$'
return bool(re.match(pattern, phone))
@staticmethod
def validate_id_card(id_card):
"""验证身份证号码"""
pattern = r'^\d{17}[\dXx]$'
return bool(re.match(pattern, id_card))
@staticmethod
def validate_password(password):
"""验证密码强度(至少8位,包含字母和数字)"""
pattern = r'^(?=.*[a-zA-Z])(?=.*\d).{8,}$'
return bool(re.match(pattern, password))
@staticmethod
def validate_url(url):
"""验证URL格式"""
pattern = r'^https?://[^\s/$.?#].[^\s]*$'
return bool(re.match(pattern, url))
# 测试验证器
validator = DataValidator()
test_data = {
"手机号": "13812345678",
"身份证": "110101199001011234",
"密码": "password123",
"网址": "https://www.example.com"
}
for data_type, value in test_data.items():
if data_type == "手机号":
result = validator.validate_phone(value)
elif data_type == "身份证":
result = validator.validate_id_card(value)
elif data_type == "密码":
result = validator.validate_password(value)
elif data_type == "网址":
result = validator.validate_url(value)
status = "✅ 有效" if result else "❌ 无效"
print(f"{data_type} {value}: {status}")
2. 文本信息提取器
class TextExtractor:
"""文本信息提取器"""
def __init__(self):
self.patterns = {
'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
'phone': r'1[3-9]\d{9}',
'url': r'https?://[^\s]+',
'date': r'\d{4}[-/]\d{1,2}[-/]\d{1,2}',
'money': r'¥?\d+(?:\.\d{2})?',
'qq': r'[1-9]\d{4,11}',
'ip': r'\b(?:\d{1,3}\.){3}\d{1,3}\b'
}
def extract_all(self, text):
"""提取文本中的所有信息"""
results = {}
for info_type, pattern in self.patterns.items():
matches = re.findall(pattern, text)
if matches:
results[info_type] = matches
return results
# 示例文本
sample_text = '''
个人信息:
姓名:张三
电话:13812345678
邮箱:zhangsan@gmail.com
QQ:123456789
生日:1990-05-20
网站:https://www.example.com
IP地址:192.168.1.1
余额:¥1234.56
'''
extractor = TextExtractor()
extracted_info = extractor.extract_all(sample_text)
print("提取到的信息:")
for info_type, values in extracted_info.items():
print(f"{info_type}: {values}")
3. 日志分析工具
class LogAnalyzer:
"""日志分析工具"""
def __init__(self):
# 定义不同类型的日志格式
self.log_patterns = {
'apache': r'(\d+\.\d+\.\d+\.\d+).*?\[(.*?)\].*?"(\w+)\s+(.*?)\s+HTTP.*?"\s+(\d+)\s+(\d+)',
'nginx': r'(\d+\.\d+\.\d+\.\d+).*?\[(.*?)\].*?"(\w+)\s+(.*?)\s+HTTP.*?"\s+(\d+)\s+(\d+)',
'error': r'\[(.*?)\]\s+\[(\w+)\]\s+(.*)',
'python': r'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}),\d+\s+(\w+)\s+(.*)'
}
def analyze_access_log(self, log_content):
"""分析访问日志"""
pattern = self.log_patterns['apache']
matches = re.findall(pattern, log_content)
analysis = {
'total_requests': len(matches),
'unique_ips': set(),
'status_codes': {},
'popular_pages': {},
'methods': {}
}
for match in matches:
ip, timestamp, method, url, status, size = match
analysis['unique_ips'].add(ip)
# 统计状态码
if status in analysis['status_codes']:
analysis['status_codes'][status] += 1
else:
analysis['status_codes'][status] = 1
# 统计热门页面
if url in analysis['popular_pages']:
analysis['popular_pages'][url] += 1
else:
analysis['popular_pages'][url] = 1
# 统计请求方法
if method in analysis['methods']:
analysis['methods'][method] += 1
else:
analysis['methods'][method] = 1
analysis['unique_ips'] = len(analysis['unique_ips'])
return analysis
# 示例日志分析
log_content = '''
127.0.0.1 - - [25/Dec/2023:10:00:00 +0000] "GET /index.html HTTP/1.1" 200 1234
192.168.1.1 - - [25/Dec/2023:10:05:00 +0000] "POST /login HTTP/1.1" 302 0
10.0.0.1 - - [25/Dec/2023:10:10:00 +0000] "GET /api/users HTTP/1.1" 404 567
127.0.0.1 - - [25/Dec/2023:10:15:00 +0000] "GET /about HTTP/1.1" 200 890
'''
analyzer = LogAnalyzer()
analysis = analyzer.analyze_access_log(log_content)
print("日志分析结果:")
print(f"总请求数:{analysis['total_requests']}")
print(f"唯一IP数:{analysis['unique_ips']}")
print(f"状态码分布:{analysis['status_codes']}")
print(f"热门页面:{analysis['popular_pages']}")
print(f"请求方法:{analysis['methods']}")
⚡ 性能优化与最佳实践
1. 编译正则表达式
import re
import time
# 测试编译与不编译的性能差异
text = "这是一个测试文本,包含很多数字:12345,67890,13579,24680" * 1000
def test_without_compile():
"""不编译正则表达式"""
start_time = time.time()
for _ in range(1000):
re.findall(r'\d+', text)
end_time = time.time()
return end_time - start_time
def test_with_compile():
"""编译正则表达式"""
pattern = re.compile(r'\d+')
start_time = time.time()
for _ in range(1000):
pattern.findall(text)
end_time = time.time()
return end_time - start_time
print(f"不编译耗时:{test_without_compile():.4f}秒")
print(f"编译后耗时:{test_with_compile():.4f}秒")
2. 避免回溯问题
# 不好的正则表达式(可能导致回溯)
bad_pattern = r'(a+)+b'
# 好的正则表达式
good_pattern = r'a+b'
# 测试文本(会导致回溯问题)
problematic_text = 'a' * 20
# 建议:使用具体的量词而不是嵌套的量词
3. 使用正确的函数
text = "Hello World! This is a test."
# 如果只需要知道是否匹配,使用 re.search()
if re.search(r'test', text):
print("找到了 'test'")
# 如果需要所有匹配,使用 re.findall()
words = re.findall(r'\w+', text)
print(f"所有单词:{words}")
# 如果需要替换,使用 re.sub()
new_text = re.sub(r'test', 'example', text)
print(f"替换后:{new_text}")
🔧 常见问题与解决方案
问题1:特殊字符需要转义
# 错误:直接使用特殊字符
# pattern = r'$100' # $ 是特殊字符,表示行尾
# 正确:使用转义字符
pattern = r'\$100' # 正确匹配 "$100"
text = "价格是$100"
result = re.search(pattern, text)
print(result.group() if result else "未找到")
问题2:中文字符处理
# 匹配中文字符
chinese_pattern = r'[\u4e00-\u9fff]+'
text = "Hello 世界!今天天气真好。"
chinese_words = re.findall(chinese_pattern, text)
print(f"中文内容:{chinese_words}") # ['世界', '今天天气真好']
问题3:多行文本处理
multiline_text = '''第一行
第二行
第三行'''
# 默认情况下,. 不匹配换行符
pattern1 = r'第一行.*第三行'
result1 = re.search(pattern1, multiline_text)
print(f"默认模式:{result1}") # None
# 使用 re.DOTALL 标志让 . 匹配换行符
pattern2 = r'第一行.*第三行'
result2 = re.search(pattern2, multiline_text, re.DOTALL)
print(f"DOTALL模式:{result2.group() if result2 else None}")
📖 扩展阅读
推荐资源
- 在线正则表达式测试工具:regex101.com
- Python re模块官方文档:docs.python.org/3/library/re.html
- 正则表达式教程:regexone.com
- 《正则表达式必知必会》:经典的正则表达式入门书籍
实用工具
# 正则表达式调试器
def regex_debugger(pattern, text):
"""正则表达式调试器"""
try:
compiled = re.compile(pattern)
matches = compiled.findall(text)
print(f"模式:{pattern}")
print(f"文本:{text}")
print(f"匹配结果:{matches}")
print(f"匹配数量:{len(matches)}")
except re.error as e:
print(f"正则表达式语法错误:{e}")
# 使用示例
regex_debugger(r'\d+', '我有123个苹果和456个橘子')
🎬 下集预告
在下一篇文章《多线程与并发:让程序同时做多件事》中,我们将探索:
- 如何让Python程序同时处理多个任务
- 线程和进程的区别和使用场景
- 异步编程的魅力
- 并发编程的陷阱和最佳实践
想象一下,如果你的程序能够同时下载文件、处理数据、响应用户请求,那该多么高效!
📝 总结与思考题
核心要点回顾
- 正则表达式是处理文本的强大工具,就像文本处理的"瑞士军刀"
- 基础语法包括字符匹配、量词、字符类等
- Python的re模块提供了丰富的函数来使用正则表达式
- 实际应用包括数据验证、信息提取、日志分析等
- 性能优化通过编译正则表达式和选择合适的函数
实践作业
- 写一个函数,从一段文本中提取所有的IP地址
- 创建一个密码强度检查器,要求包含大小写字母、数字和特殊字符
- 解析一个CSV格式的字符串,提取每一列的数据
- 编写一个简单的模板引擎,用正则表达式替换模板中的变量
思考题
- 为什么说正则表达式是"写时简单,读时困难"?如何提高正则表达式的可读性?
- 在什么情况下应该使用正则表达式,什么情况下应该使用字符串的普通方法?
- 如何平衡正则表达式的功能性和性能?
记住:正则表达式虽然强大,但也要适度使用。正如一句名言所说:"如果你有一个问题,想用正则表达式解决,那么你就有两个问题了。"但是,如果你真正掌握了正则表达式,它就会成为你文本处理的得力助手!
🎉 恭喜你!你已经掌握了正则表达式这把文本处理的"瑞士军刀"。现在你可以轻松地在茫茫文本海洋中找到你需要的信息,就像拥有了一双火眼金睛!