引言
在日常的编程工作中,我们经常需要处理文本数据,比如验证用户输入、提取特定信息、格式化文本等。正则表达式(Regular Expression,简称regex或regexp)是一种强大的文本处理工具,它使用特定的语法来描述文本模式,可以高效地完成复杂的字符串匹配、查找、替换等操作。
正则表达式几乎在所有的编程语言中都有实现,Python通过re模块提供了完整的正则表达式支持。掌握正则表达式不仅能够大大提高文本处理的效率,还能让我们写出更加简洁和优雅的代码。
在前面的章节中,我们学习了Python的基础语法、数据结构、面向对象编程、文件操作、标准库使用以及环境管理等内容。本章将深入学习正则表达式的基础知识和在Python中的应用,帮助你掌握这一重要的文本处理工具。
学习目标
完成本章学习后,你将能够:
- 理解正则表达式的基本概念和工作原理
- 掌握常用的正则表达式元字符和语法
- 使用Python的re模块进行模式匹配和文本处理
- 编写复杂的正则表达式来解决实际问题
- 理解贪婪匹配和非贪婪匹配的区别
- 掌握正则表达式的编译和优化技巧
- 应用正则表达式解决常见的文本处理任务
核心知识点讲解
1. 正则表达式概述
正则表达式是一种用来描述字符串模式的形式化语言。它由普通字符(如字母、数字)和特殊字符(称为元字符)组成,用于匹配、查找和替换符合特定模式的文本。
正则表达式的特点:
- 简洁性:用简短的表达式描述复杂的文本模式
- 灵活性:可以匹配各种复杂的文本结构
- 高效性:现代正则表达式引擎经过优化,匹配速度快
- 通用性:几乎所有编程语言都支持正则表达式
2. 基本元字符
元字符是正则表达式中具有特殊含义的字符,它们构成了正则表达式的核心语法。
常用元字符:
.: 匹配除换行符外的任意字符^: 匹配字符串的开始$: 匹配字符串的结束*: 匹配前面的字符0次或多次+: 匹配前面的字符1次或多次?: 匹配前面的字符0次或1次{n}: 匹配前面的字符恰好n次{n,}: 匹配前面的字符至少n次{n,m}: 匹配前面的字符至少n次,至多m次[]: 字符类,匹配方括号内的任意字符|: 或操作符,匹配左边或右边的表达式(): 分组,将多个字符组合为一个单元\: 转义字符,用于匹配特殊字符本身
3. 字符类和预定义字符集
字符类允许我们匹配特定类型的字符,而预定义字符集提供了常用的字符类别快捷方式。
常用字符类:
[abc]: 匹配a、b或c中的任意一个字符[^abc]: 匹配除了a、b、c之外的任意字符[a-z]: 匹配任意小写字母[A-Z]: 匹配任意大写字母[0-9]: 匹配任意数字
预定义字符集:
\d: 匹配任意数字,等价于[0-9]\D: 匹配任意非数字字符,等价于[^0-9]\w: 匹配任意字母、数字或下划线,等价于[a-zA-Z0-9_]\W: 匹配任意非字母、数字或下划线字符\s: 匹配任意空白字符(空格、制表符、换行符等)\S: 匹配任意非空白字符
4. 量词和贪婪匹配
量词用于指定前面字符或组的重复次数,理解贪婪匹配和非贪婪匹配的区别非常重要。
量词类型:
-
贪婪量词:尽可能多地匹配字符
*: 0次或多次+: 1次或多次?: 0次或1次{n,m}: n到m次
-
非贪婪量词:尽可能少地匹配字符
*?: 0次或多次(非贪婪)+?: 1次或多次(非贪婪)??: 0次或1次(非贪婪){n,m}?: n到m次(非贪婪)
5. 分组和捕获
分组允许我们将多个字符组合为一个单元,并可以捕获匹配的内容用于后续处理。
分组类型:
- 捕获分组:
(...)- 匹配内容会被保存 - 非捕获分组:
(?:...)- 匹配但不保存内容 - 命名分组:
(?P<name>...)- 带名称的捕获分组 - 反向引用:
\1,\2等 - 引用前面捕获的分组
6. Python re模块
Python的re模块提供了完整的正则表达式支持,包含丰富的函数和方法。
主要函数:
re.match(): 从字符串开头匹配模式re.search(): 在整个字符串中搜索模式re.findall(): 查找所有匹配的子串re.finditer(): 返回匹配对象的迭代器re.sub(): 替换匹配的子串re.split(): 根据模式分割字符串re.compile(): 编译正则表达式对象
7. 匹配对象和标志
匹配对象包含了匹配结果的详细信息,而标志可以修改正则表达式的匹配行为。
匹配对象属性:
.group(): 返回匹配的字符串.groups(): 返回所有捕获分组的元组.start(): 返回匹配开始的位置.end(): 返回匹配结束的位置.span(): 返回匹配的起始和结束位置
常用标志:
re.IGNORECASE或re.I: 忽略大小写re.MULTILINE或re.M: 多行模式re.DOTALL或re.S: 使.匹配包括换行符在内的所有字符re.VERBOSE或re.X: 详细模式,允许添加注释和空白
代码示例与实战
实战1:基础正则表达式练习
import re
# 1. 基本匹配
text = "Hello, my email is example@test.com and phone is 138-1234-5678"
print("原文本:", text)
# 匹配邮箱地址
email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
emails = re.findall(email_pattern, text)
print("找到的邮箱:", emails)
# 匹配手机号码
phone_pattern = r'1[3-9]\d{9}'
phones = re.findall(phone_pattern, text)
print("找到的手机号:", phones)
# 2. 使用分组提取信息
phone_with_dash_pattern = r'(1[3-9]\d)-(\d{4})-(\d{4})'
match = re.search(phone_with_dash_pattern, text)
if match:
print("完整号码:", match.group(0))
print("区号:", match.group(1))
print("前四位:", match.group(2))
print("后四位:", match.group(3))
# 3. 命名分组
named_pattern = r'(?P<area>1[3-9]\d)-(?P<prefix>\d{4})-(?P<suffix>\d{4})'
match = re.search(named_pattern, text)
if match:
print("命名分组结果:")
print("区号:", match.group('area'))
print("前缀:", match.group('prefix'))
print("后缀:", match.group('suffix'))
实战2:文本验证和清理工具
import re
class TextValidator:
"""文本验证工具类"""
# 预定义的正则表达式模式
PATTERNS = {
'email': r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
'phone': r'^1[3-9]\d{9}$',
'chinese_phone': r'^(\+86)?1[3-9]\d{9}$',
'id_card': r'^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$',
'url': r'^https?://(?:[-\w.])+(?:\:[0-9]+)?(?:/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?$',
'ipv4': r'^(?:(?: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]?)$',
'strong_password': r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$'
}
@classmethod
def validate(cls, pattern_name, text):
"""验证文本是否符合指定模式"""
if pattern_name not in cls.PATTERNS:
raise ValueError(f"未知的模式名称: {pattern_name}")
pattern = cls.PATTERNS[pattern_name]
return bool(re.match(pattern, text))
@classmethod
def find_all(cls, pattern_name, text):
"""查找文本中所有符合指定模式的内容"""
if pattern_name not in cls.PATTERNS:
raise ValueError(f"未知的模式名称: {pattern_name}")
pattern = cls.PATTERNS[pattern_name]
return re.findall(pattern, text)
class TextCleaner:
"""文本清理工具类"""
@staticmethod
def remove_extra_spaces(text):
"""移除多余的空格"""
# 将多个连续空格替换为单个空格
return re.sub(r'\s+', ' ', text).strip()
@staticmethod
def remove_html_tags(text):
"""移除HTML标签"""
return re.sub(r'<[^>]+>', '', text)
@staticmethod
def extract_chinese(text):
"""提取中文字符"""
return ''.join(re.findall(r'[\u4e00-\u9fff]+', text))
@staticmethod
def mask_sensitive_info(text):
"""遮蔽敏感信息"""
# 遮蔽手机号中间4位
text = re.sub(r'(1[3-9]\d)(\d{4})(\d{4})', r'\1****\3', text)
# 遮蔽邮箱@前的部分(保留前3位和后2位)
text = re.sub(r'([a-zA-Z0-9._%+-]{3})[a-zA-Z0-9._%+-]*(@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})',
r'\1****\2', text)
return text
# 使用示例
if __name__ == "__main__":
# 验证功能
print("=== 文本验证示例 ===")
test_cases = [
('email', 'test@example.com'),
('email', 'invalid-email'),
('phone', '13812345678'),
('phone', '12345678901'),
('url', 'https://www.example.com'),
('url', 'not-a-url')
]
for pattern_name, text in test_cases:
result = TextValidator.validate(pattern_name, text)
print(f"{pattern_name}: {text} -> {'有效' if result else '无效'}")
# 清理功能
print("\n=== 文本清理示例 ===")
messy_text = """
这是一个 包含多余空格 的文本。
<p>这里有一些<strong>HTML标签</strong></p>
联系电话:13812345678,邮箱:example@test.com
这段文字包含English和中文混合内容。
"""
print("原文本:")
print(messy_text)
cleaned = TextCleaner.remove_extra_spaces(messy_text)
print("\n清理多余空格后:")
print(cleaned)
no_html = TextCleaner.remove_html_tags(cleaned)
print("\n移除HTML标签后:")
print(no_html)
chinese_only = TextCleaner.extract_chinese(no_html)
print("\n提取中文内容:")
print(chinese_only)
masked = TextCleaner.mask_sensitive_info(no_html)
print("\n遮蔽敏感信息后:")
print(masked)
实战3:日志分析工具
import re
from datetime import datetime
from collections import defaultdict, Counter
class LogAnalyzer:
"""日志分析工具"""
def __init__(self):
# Apache访问日志的正则表达式模式
self.apache_pattern = re.compile(
r'(?P<ip>\d+\.\d+\.\d+\.\d+)\s+' # IP地址
r'-\s+-\s+' # 远程用户标识符和认证用户标识符
r'$ (?P<datetime>[^\]]+) $\s+' # 时间戳
r'"(?P<method>\w+)\s+(?P<url>[^\s]+)\s+(?P<protocol>[^"]+)"\s+' # 请求行
r'(?P<status>\d+)\s+' # 状态码
r'(?P<size>\d+|-)\s+' # 响应大小
r'"(?P<referer>[^"]*)"\s+' # Referer
r'"(?P<user_agent>[^"]*)"' # User-Agent
)
# Nginx访问日志的正则表达式模式
self.nginx_pattern = re.compile(
r'(?P<ip>\d+\.\d+\.\d+\.\d+)\s+' # IP地址
r'-\s+-\s+' # 远程用户标识符和认证用户标识符
r'$ (?P<datetime>[^\]]+) $\s+' # 时间戳
r'"(?P<method>\w+)\s+(?P<url>[^\s]+)\s+(?P<protocol>[^"]+)"\s+' # 请求行
r'(?P<status>\d+)\s+' # 状态码
r'(?P<size>\d+)\s+' # 响应大小
r'"(?P<referer>[^"]*)"\s+' # Referer
r'"(?P<user_agent>[^"]*)"' # User-Agent
)
def parse_apache_log(self, log_line):
"""解析Apache日志行"""
match = self.apache_pattern.match(log_line)
if match:
return match.groupdict()
return None
def parse_nginx_log(self, log_line):
"""解析Nginx日志行"""
match = self.nginx_pattern.match(log_line)
if match:
return match.groupdict()
return None
def analyze_logs(self, log_lines, log_type='apache'):
"""分析日志"""
stats = {
'total_requests': 0,
'status_codes': Counter(),
'ips': Counter(),
'urls': Counter(),
'methods': Counter(),
'user_agents': Counter(),
'errors': [] # 存储解析失败的行
}
parse_func = self.parse_apache_log if log_type == 'apache' else self.parse_nginx_log
for line_num, line in enumerate(log_lines, 1):
parsed = parse_func(line.strip())
if parsed:
stats['total_requests'] += 1
# 统计状态码
stats['status_codes'][parsed['status']] += 1
# 统计IP地址
stats['ips'][parsed['ip']] += 1
# 统计URL
stats['urls'][parsed['url']] += 1
# 统计请求方法
stats['methods'][parsed['method']] += 1
# 统计User-Agent
if parsed['user_agent']:
stats['user_agents'][parsed['user_agent']] += 1
else:
stats['errors'].append(f"第{line_num}行解析失败: {line.strip()}")
return stats
def generate_report(self, stats):
"""生成分析报告"""
print("=" * 50)
print("日志分析报告")
print("=" * 50)
print(f"总请求数: {stats['total_requests']}")
print("\n状态码统计:")
for code, count in stats['status_codes'].most_common():
print(f" {code}: {count} 次")
print("\n访问最多的IP地址 (Top 10):")
for ip, count in stats['ips'].most_common(10):
print(f" {ip}: {count} 次")
print("\n访问最多的URL (Top 10):")
for url, count in stats['urls'].most_common(10):
print(f" {url}: {count} 次")
print("\n请求方法统计:")
for method, count in stats['methods'].most_common():
print(f" {method}: {count} 次")
print("\n最常见的User-Agent (Top 5):")
for ua, count in stats['user_agents'].most_common(5):
print(f" {ua[:50]}...: {count} 次")
if stats['errors']:
print(f"\n解析错误 ({len(stats['errors'])} 条):")
for error in stats['errors'][:5]: # 只显示前5个错误
print(f" {error}")
# 模拟日志数据
sample_logs = [
'192.168.1.1 - - [25/Dec/2023:10:00:00 +0000] "GET /index.html HTTP/1.1" 200 1234 "-" "Mozilla/5.0"',
'192.168.1.2 - - [25/Dec/2023:10:01:00 +0000] "POST /login HTTP/1.1" 200 567 "-" "Chrome/91.0.4472.124"',
'192.168.1.3 - - [25/Dec/2023:10:02:00 +0000] "GET /admin.php HTTP/1.1" 404 234 "-" "Mozilla/5.0"',
'192.168.1.1 - - [25/Dec/2023:10:03:00 +0000] "GET /style.css HTTP/1.1" 200 789 "-" "Safari/14.1.1"',
'192.168.1.4 - - [25/Dec/2023:10:04:00 +0000] "GET /script.js HTTP/1.1" 200 456 "-" "Firefox/89.0"',
'192.168.1.5 - - [25/Dec/2023:10:05:00 +0000] "GET /favicon.ico HTTP/1.1" 404 123 "-" "Bot/1.0"'
]
# 使用示例
if __name__ == "__main__":
analyzer = LogAnalyzer()
stats = analyzer.analyze_logs(sample_logs, 'apache')
analyzer.generate_report(stats)
实战4:数据提取和格式化工具
import re
from datetime import datetime
class DataExtractor:
"""数据提取工具"""
def __init__(self):
# 定义各种数据模式
self.patterns = {
'dates': [
r'\b\d{4}-\d{2}-\d{2}\b', # YYYY-MM-DD
r'\b\d{2}/\d{2}/\d{4}\b', # MM/DD/YYYY
r'\b\d{4}年\d{1,2}月\d{1,2}日\b' # YYYY年MM月DD日
],
'times': [
r'\b\d{2}:\d{2}:\d{2}\b', # HH:MM:SS
r'\b\d{1,2}:\d{2}\s*(?:AM|PM)\b' # H:MM AM/PM
],
'prices': [
r'\b¥?\d+(?:,\d{3})*(?:\.\d{2})?\b', # ¥1,234.56
r'\b\$\d+(?:,\d{3})*(?:\.\d{2})?\b' # $1,234.56
],
'percentages': [
r'\b\d+(?:\.\d+)?%\b' # 12.5%
]
}
def extract_dates(self, text):
"""提取日期"""
dates = []
for pattern in self.patterns['dates']:
matches = re.findall(pattern, text)
dates.extend(matches)
return dates
def extract_times(self, text):
"""提取时间"""
times = []
for pattern in self.patterns['times']:
matches = re.findall(pattern, text)
times.extend(matches)
return times
def extract_prices(self, text):
"""提取价格"""
prices = []
for pattern in self.patterns['prices']:
matches = re.findall(pattern, text)
prices.extend(matches)
return prices
def extract_percentages(self, text):
"""提取百分比"""
percentages = []
for pattern in self.patterns['percentages']:
matches = re.findall(pattern, text)
percentages.extend(matches)
return percentages
def extract_structured_data(self, text):
"""提取结构化数据"""
# 提取姓名和年龄的模式
name_age_pattern = r'姓名[::]\s*([^\s,,]+).*?年龄[::]\s*(\d+)'
matches = re.findall(name_age_pattern, text)
people = []
for name, age in matches:
people.append({
'name': name.strip(),
'age': int(age)
})
return people
class DataFormatter:
"""数据格式化工具"""
@staticmethod
def normalize_phone_numbers(text):
"""标准化电话号码格式"""
# 匹配各种电话号码格式并统一为 XXX-XXXX-XXXX 格式
def format_phone(match):
digits = re.sub(r'\D', '', match.group())
if len(digits) == 11 and digits.startswith('1'):
return f"{digits[:3]}-{digits[3:7]}-{digits[7:]}"
return match.group()
phone_pattern = r'(?:\+86[-\s]?)?1[3-9]\d[-\s]?\d{4}[-\s]?\d{4}|\b1[3-9]\d{9}\b'
return re.sub(phone_pattern, format_phone, text)
@staticmethod
def format_currency(amount_str):
"""格式化货币金额"""
# 移除货币符号和逗号,转换为浮点数
clean_amount = re.sub(r'[¥$,]', '', amount_str)
try:
amount = float(clean_amount)
return f"¥{amount:,.2f}"
except ValueError:
return amount_str
@staticmethod
def standardize_dates(text):
"""标准化日期格式为YYYY-MM-DD"""
# 处理 YYYY/MM/DD 格式
text = re.sub(r'\b(\d{4})/(\d{1,2})/(\d{1,2})\b', r'\1-\2-\3', text)
# 处理 MM/DD/YYYY 格式
text = re.sub(r'\b(\d{1,2})/(\d{1,2})/(\d{4})\b', r'\3-\1-\2', text)
# 处理中文日期格式
chinese_date_pattern = r'(\d{4})年(\d{1,2})月(\d{1,2})日'
def convert_chinese_date(match):
year, month, day = match.groups()
return f"{year}-{int(month):02d}-{int(day):02d}"
text = re.sub(chinese_date_pattern, convert_chinese_date, text)
return text
# 使用示例
if __name__ == "__main__":
# 数据提取示例
print("=== 数据提取示例 ===")
sample_text = """
今天是2023-12-25,也是12/25/2023。
现在时间是14:30:25。
商品价格:¥1,234.56 和 $99.99。
折扣率:15.5% 和 20%。
姓名:张三,年龄:25岁。
姓名:李四,年龄:30岁。
联系电话:138-1234-5678,13912345679,+86 137 1234 5678
"""
extractor = DataExtractor()
formatter = DataFormatter()
print("原文本:")
print(sample_text)
print("\n提取的日期:", extractor.extract_dates(sample_text))
print("提取的时间:", extractor.extract_times(sample_text))
print("提取的价格:", extractor.extract_prices(sample_text))
print("提取的百分比:", extractor.extract_percentages(sample_text))
print("提取的人员信息:", extractor.extract_structured_data(sample_text))
print("\n=== 数据格式化示例 ===")
print("标准化电话号码:")
formatted_text = formatter.normalize_phone_numbers(sample_text)
print(formatted_text)
print("\n标准化日期:")
standardized_dates = formatter.standardize_dates(sample_text)
print(standardized_dates)
print("\n格式化货币:")
prices = extractor.extract_prices(sample_text)
for price in prices:
formatted_price = formatter.format_currency(price)
print(f" {price} -> {formatted_price}")
小结与回顾
本章我们深入学习了正则表达式的基础知识和在Python中的应用:
-
正则表达式概念:理解了正则表达式作为一种描述文本模式的形式化语言,具有简洁、灵活、高效的特点。
-
基本元字符:掌握了常用的元字符如
.、^、$、*、+、?等的含义和用法。 -
字符类和预定义字符集:学会了使用字符类
[]和预定义字符集\d、\w、\s等来匹配特定类型的字符。 -
量词和贪婪匹配:理解了贪婪匹配和非贪婪匹配的区别,以及如何使用量词控制匹配次数。
-
分组和捕获:掌握了分组的使用方法,包括捕获分组、非捕获分组和命名分组。
-
Python re模块:熟悉了re模块的主要函数如
match()、search()、findall()、sub()等的使用。 -
匹配对象和标志:了解了匹配对象的属性和常用标志的作用。
通过本章的学习和实战练习,你应该已经掌握了正则表达式的基础知识,并能够在实际项目中运用正则表达式解决文本处理问题。正则表达式是一项非常实用的技能,在数据清洗、日志分析、文本验证等场景中都有广泛应用。
练习与挑战
基础练习
-
编写正则表达式匹配以下模式:
- IPv6地址
- MAC地址(如 00:1A:2B:3C:4D:5E)
- 邮政编码(中国6位数字)
- 身份证号码校验
-
使用re模块实现以下功能:
- 提取HTML文档中的所有链接
- 验证强密码(包含大小写字母、数字、特殊字符,长度至少8位)
- 解析CSV格式的文本数据
-
创建一个文本处理工具,支持:
- 批量替换文本中的敏感信息
- 统计文档中各单词的出现频率
- 提取文档中的所有电子邮件地址和电话号码
进阶挑战
- 开发一个完整的日志分析系统,支持多种日志格式的解析和统计分析
- 实现一个配置文件解析器,支持自定义语法和变量替换
- 创建一个代码注释提取工具,支持多种编程语言的注释格式
- 设计一个文本搜索引擎,支持正则表达式查询和高亮显示
项目实战
开发一个"智能文本处理平台",集成以下功能:
- 多种文本格式的导入和导出(TXT、CSV、JSON、XML等)
- 正则表达式模式库,支持常用模式的快速应用
- 可视化正则表达式构建器,降低使用门槛
- 批量文本处理功能,支持自定义处理流程
- 实时预览和调试功能,方便验证正则表达式效果
- 插件系统,支持扩展自定义处理模块
扩展阅读
-
Python官方文档 - re模块: docs.python.org/zh-cn/3/lib…
- 官方re模块的详细文档,包含所有函数和方法的说明
-
《精通正则表达式》 by Jeffrey E.F. Friedl:
- 正则表达式的权威著作,深入讲解正则表达式的原理和应用
-
Regex101: regex101.com/
- 在线正则表达式测试工具,支持多种语言和实时调试
-
RegExr: regexr.com/
- 另一个优秀的在线正则表达式学习和测试平台
-
Real Python - Regular Expressions: realpython.com/regex-pytho…
- 提供高质量的Python正则表达式教程和实际应用案例
-
MDN Web Docs - 正则表达式: developer.mozilla.org/zh-CN/docs/…
- JavaScript正则表达式的详细文档,概念相通
-
《Python数据科学手册》by Jake VanderPlas:
- 包含使用正则表达式进行数据清洗和处理的实际案例
-
正则表达式可视化工具: regexper.com/
- 将正则表达式可视化为图形,帮助理解复杂表达式的结构
通过深入学习这些扩展资源,你将进一步巩固对正则表达式的理解,并掌握更多高级用法和最佳实践。正则表达式虽然看起来复杂,但通过不断练习和应用,你会逐渐掌握这项强大的文本处理工具。