Appearance
Python 正则表达式
正则表达式(Regular Expression,简称 regex 或 regexp)是一种用于匹配字符串中字符组合的模式。在 Python 中,正则表达式通过 re 模块实现。本章节将详细介绍 Python 中正则表达式的概念、语法和使用方法。
什么是正则表达式?
正则表达式是一种用于描述字符串模式的工具,它可以:
- 匹配字符串:检查字符串是否符合特定模式
- 提取字符串:从字符串中提取符合特定模式的部分
- 替换字符串:替换字符串中符合特定模式的部分
- 分割字符串:根据特定模式分割字符串
正则表达式在文本处理、数据验证、日志分析等场景中非常有用。
正则表达式基础
基本字符
| 字符 | 描述 | 示例 |
|---|---|---|
| 普通字符 | 匹配自身 | a 匹配 "a" |
. | 匹配任意单个字符(除换行符) | a.c 匹配 "abc"、"adc" 等 |
^ | 匹配字符串开头 | ^abc 匹配以 "abc" 开头的字符串 |
$ | 匹配字符串结尾 | abc$ 匹配以 "abc" 结尾的字符串 |
* | 匹配前面的字符 0 次或多次 | ab* 匹配 "a"、"ab"、"abb" 等 |
+ | 匹配前面的字符 1 次或多次 | ab+ 匹配 "ab"、"abb" 等,不匹配 "a" |
? | 匹配前面的字符 0 次或 1 次 | ab? 匹配 "a"、"ab",不匹配 "abb" |
{n} | 匹配前面的字符恰好 n 次 | ab{2} 匹配 "abb" |
{n,} | 匹配前面的字符至少 n 次 | ab{2,} 匹配 "abb"、"abbb" 等 |
{n,m} | 匹配前面的字符 n 到 m 次 | ab{2,3} 匹配 "abb"、"abbb" |
[] | 匹配字符集中的任意一个字符 | [abc] 匹配 "a"、"b" 或 "c" |
[^] | 匹配不在字符集中的任意一个字符 | [^abc] 匹配除 "a"、"b"、"c" 以外的任意字符 |
| ` | ` | 匹配两个模式中的任意一个 |
() | 捕获组,匹配括号内的模式 | (ab)+ 匹配 "ab"、"abab" 等 |
转义字符
当需要匹配特殊字符本身时,需要使用反斜杠 \ 进行转义:
| 转义字符 | 描述 | 示例 |
|---|---|---|
\d | 匹配任意数字,等价于 [0-9] | \d+ 匹配一个或多个数字 |
\D | 匹配任意非数字,等价于 [^0-9] | \D+ 匹配一个或多个非数字 |
\w | 匹配任意字母、数字或下划线,等价于 [a-zA-Z0-9_] | \w+ 匹配一个或多个字母、数字或下划线 |
\W | 匹配任意非字母、数字或下划线,等价于 [^a-zA-Z0-9_] | \W+ 匹配一个或多个非字母、数字或下划线 |
\s | 匹配任意空白字符(空格、制表符、换行符等) | \s+ 匹配一个或多个空白字符 |
\S | 匹配任意非空白字符 | \S+ 匹配一个或多个非空白字符 |
\b | 匹配单词边界 | \bword\b 匹配完整的单词 "word" |
\B | 匹配非单词边界 | \Bword\B 匹配单词内部的 "word" |
\ | 匹配反斜杠本身 | \\ 匹配 "\" |
贪婪与非贪婪匹配
默认情况下,正则表达式是贪婪的,即尽可能多地匹配字符。在量词后面添加 ? 可以使其变为非贪婪模式:
| 贪婪模式 | 非贪婪模式 | 描述 |
|---|---|---|
* | *? | 匹配 0 次或多次,尽可能少 |
+ | +? | 匹配 1 次或多次,尽可能少 |
? | ?? | 匹配 0 次或 1 次,尽可能少 |
{n,} | {n,}? | 匹配至少 n 次,尽可能少 |
{n,m} | {n,m}? | 匹配 n 到 m 次,尽可能少 |
re 模块的使用
Python 中的正则表达式功能通过 re 模块实现。
常用函数
| 函数 | 描述 | 返回值 |
|---|---|---|
re.match(pattern, string, flags=0) | 从字符串开头匹配模式 | 匹配对象或 None |
re.search(pattern, string, flags=0) | 在字符串中搜索模式 | 匹配对象或 None |
re.findall(pattern, string, flags=0) | 查找所有匹配的子串 | 匹配的子串列表 |
re.finditer(pattern, string, flags=0) | 查找所有匹配的子串并返回迭代器 | 匹配对象的迭代器 |
re.sub(pattern, repl, string, count=0, flags=0) | 替换匹配的子串 | 替换后的字符串 |
re.subn(pattern, repl, string, count=0, flags=0) | 替换匹配的子串并返回替换次数 | (替换后的字符串, 替换次数) |
re.split(pattern, string, maxsplit=0, flags=0) | 根据模式分割字符串 | 分割后的字符串列表 |
re.compile(pattern, flags=0) | 编译正则表达式模式 | 编译后的正则表达式对象 |
匹配对象
当 re.match()、re.search() 或 re.finditer() 找到匹配时,会返回一个匹配对象。匹配对象具有以下方法:
| 方法 | 描述 |
|---|---|
group() | 返回匹配的整个字符串 |
group(n) | 返回第 n 个捕获组匹配的字符串 |
groups() | 返回所有捕获组匹配的字符串元组 |
groupdict() | 返回命名捕获组匹配的字符串字典 |
start() | 返回匹配的开始位置 |
end() | 返回匹配的结束位置 |
span() | 返回匹配的开始和结束位置元组 |
标志参数
re 模块的函数可以接受标志参数,用于修改正则表达式的行为:
| 标志 | 描述 |
|---|---|
re.IGNORECASE 或 re.I | 忽略大小写 |
re.MULTILINE 或 re.M | 多行模式,^ 和 $ 匹配每行的开头和结尾 |
re.DOTALL 或 re.S | 点号匹配所有字符,包括换行符 |
re.VERBOSE 或 re.X | 详细模式,允许添加注释和空白 |
re.ASCII 或 re.A | 仅 ASCII 模式,\w、\d 等仅匹配 ASCII 字符 |
re.UNICODE 或 re.U | Unicode 模式,\w、\d 等匹配 Unicode 字符 |
re.LOCALE 或 re.L | 本地化模式,根据当前区域设置匹配 |
基本示例
1. 匹配字符串
python
import re
# 匹配邮箱地址
email_pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
emails = ["user@example.com", "invalid-email", "another.user@domain.co.uk"]
for email in emails:
if re.match(email_pattern, email):
print(f"{email} 是有效的邮箱地址")
else:
print(f"{email} 不是有效的邮箱地址")
# 匹配手机号码(中国手机号)
phone_pattern = r"1[3-9]\d{9}"
phones = ["13812345678", "12345678901", "18887654321"]
for phone in phones:
if re.match(phone_pattern, phone):
print(f"{phone} 是有效的手机号")
else:
print(f"{phone} 不是有效的手机号")
# 匹配 URL
url_pattern = r"https?://[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?"
urls = ["https://www.example.com", "http://python.org", "invalid-url"]
for url in urls:
if re.match(url_pattern, url):
print(f"{url} 是有效的 URL")
else:
print(f"{url} 不是有效的 URL")2. 提取字符串
python
import re
# 从文本中提取邮箱地址
text = "联系我们:support@example.com 或 info@company.org"
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(f"提取到的邮箱地址: {emails}")
# 从文本中提取数字
text = "商品价格:199元,折扣:8.5折,库存:100件"
number_pattern = r"\d+(\.\d+)?"
numbers = re.findall(number_pattern, text)
print(f"提取到的数字: {numbers}")
# 从文本中提取日期
text = "今天是2024-01-01,明天是2024/01/02,后天是2024年1月3日"
date_pattern = r"\d{4}[-/年]\d{1,2}[-/月]\d{1,2}[日]?"
dates = re.findall(date_pattern, text)
print(f"提取到的日期: {dates}")
# 使用捕获组提取信息
text = "Alice: 30岁, Bob: 25岁, Charlie: 35岁"
age_pattern = r"(\w+): (\d+)岁"
matches = re.finditer(age_pattern, text)
for match in matches:
name = match.group(1)
age = match.group(2)
print(f"姓名: {name}, 年龄: {age}")3. 替换字符串
python
import re
# 替换文本中的邮箱地址
text = "联系我们:support@example.com 或 info@company.org"
email_pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
# 将邮箱地址替换为 [EMAIL]
result = re.sub(email_pattern, "[EMAIL]", text)
print(f"替换后: {result}")
# 使用函数进行替换
def censor_email(match):
email = match.group(0)
username, domain = email.split('@')
censored_username = username[0] + '*' * (len(username) - 1)
return f"{censored_username}@{domain}"
result = re.sub(email_pattern, censor_email, text)
print(f"部分替换后: {result}")
# 替换文本中的空白字符
text = " 这是 一段 有很多 空白字符 的文本 "
whitespace_pattern = r"\s+"
result = re.sub(whitespace_pattern, " ", text).strip()
print(f"替换空白字符后: '{result}'")
# 替换文本中的 HTML 标签
text = "<p>这是一段 <b>HTML</b> 文本</p>"
html_pattern = r"<[^>]+>"
result = re.sub(html_pattern, "", text)
print(f"移除 HTML 标签后: {result}")4. 分割字符串
python
import re
# 根据空白字符分割字符串
text = "Hello world! This is a test."
whitespace_pattern = r"\s+"
result = re.split(whitespace_pattern, text)
print(f"分割后: {result}")
# 根据多个分隔符分割字符串
text = "apple,banana;cherry|date"
delimiter_pattern = r"[,;|]"
result = re.split(delimiter_pattern, text)
print(f"分割后: {result}")
# 限制分割次数
text = "a,b,c,d,e"
result = re.split(r",", text, maxsplit=2)
print(f"限制分割次数后: {result}")
# 保留分隔符
text = "apple,banana;cherry"
delimiter_pattern = r"([,;])"
result = re.split(delimiter_pattern, text)
print(f"保留分隔符后: {result}")高级示例
1. 编译正则表达式
对于频繁使用的正则表达式,编译后使用可以提高性能:
python
import re
# 编译正则表达式
email_pattern = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
# 使用编译后的正则表达式
text = "联系我们:support@example.com 或 info@company.org"
# 查找所有匹配
emails = email_pattern.findall(text)
print(f"提取到的邮箱地址: {emails}")
# 搜索匹配
match = email_pattern.search(text)
if match:
print(f"找到第一个邮箱地址: {match.group(0)}, 位置: {match.span()}")
# 替换匹配
result = email_pattern.sub("[EMAIL]", text)
print(f"替换后: {result}")2. 命名捕获组
使用命名捕获组可以使代码更加清晰:
python
import re
# 使用命名捕获组
text = "Alice: 30岁, Bob: 25岁"
age_pattern = r"(?P<name>\w+): (?P<age>\d+)岁"
matches = re.finditer(age_pattern, text)
for match in matches:
# 使用组名访问
name = match.group("name")
age = match.group("age")
print(f"姓名: {name}, 年龄: {age}")
# 使用 groupdict() 获取所有命名捕获组
print(f"捕获组字典: {match.groupdict()}")
# 在替换中使用命名捕获组
text = "2024-01-01"
date_pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
# 替换为中文日期格式
result = re.sub(date_pattern, "\g<year>年\g<month>月\g<day>日", text)
print(f"替换后: {result}")3. 零宽断言
零宽断言用于匹配位置而不是字符,包括正向先行断言、负向先行断言、正向后行断言和负向后行断言:
python
import re
# 正向先行断言:匹配后面跟着特定模式的位置
text = "apple123 banana cherry456"
# 匹配后面跟着数字的单词
pattern = r"\w+(?=\d+)"
matches = re.findall(pattern, text)
print(f"后面跟着数字的单词: {matches}")
# 负向先行断言:匹配后面不跟着特定模式的位置
text = "apple123 banana cherry456"
# 匹配后面不跟着数字的单词
pattern = r"\w+(?!\d+)"
matches = re.findall(pattern, text)
print(f"后面不跟着数字的单词: {matches}")
# 正向后行断言:匹配前面是特定模式的位置
text = "123apple 456banana cherry"
# 匹配前面是数字的单词
pattern = r"(?<=\d+)\w+"
matches = re.findall(pattern, text)
print(f"前面是数字的单词: {matches}")
# 负向后行断言:匹配前面不是特定模式的位置
text = "123apple 456banana cherry"
# 匹配前面不是数字的单词
pattern = r"(?<!\d+)\w+"
matches = re.findall(pattern, text)
print(f"前面不是数字的单词: {matches}")4. 复杂正则表达式示例
验证密码强度
python
import re
def validate_password(password):
"""验证密码强度
要求:
- 至少 8 个字符
- 至少包含一个大写字母
- 至少包含一个小写字母
- 至少包含一个数字
- 至少包含一个特殊字符
"""
# 密码长度至少 8 个字符
if len(password) < 8:
return False, "密码长度至少 8 个字符"
# 至少包含一个大写字母
if not re.search(r"[A-Z]", password):
return False, "密码至少包含一个大写字母"
# 至少包含一个小写字母
if not re.search(r"[a-z]", password):
return False, "密码至少包含一个小写字母"
# 至少包含一个数字
if not re.search(r"\d", password):
return False, "密码至少包含一个数字"
# 至少包含一个特殊字符
if not re.search(r"[!@#$%^&*(),.?":{}|<>]", password):
return False, "密码至少包含一个特殊字符"
return True, "密码强度符合要求"
# 测试密码
passwords = [
"Password123!",
"password",
"PASSWORD123",
"Password!",
"pass123!",
"Pass123"
]
for password in passwords:
valid, message = validate_password(password)
print(f"密码: '{password}', 验证结果: {valid}, 消息: {message}")解析日志文件
python
import re
# 日志文件示例
log_content = """
2024-01-01 12:34:56 INFO User 'alice' logged in from 192.168.1.1
2024-01-01 12:35:00 ERROR Database connection failed: Connection refused
2024-01-01 12:36:00 WARNING Disk usage is 85%
2024-01-01 12:37:00 INFO User 'bob' logged in from 192.168.1.2
"""
# 解析日志条目
log_pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (INFO|ERROR|WARNING) (.+)"
matches = re.finditer(log_pattern, log_content)
for match in matches:
timestamp = match.group(1)
level = match.group(2)
message = match.group(3)
print(f"时间: {timestamp}, 级别: {level}, 消息: {message}")
# 提取所有 ERROR 级别的日志
error_pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ERROR (.+)"
error_logs = re.findall(error_pattern, log_content)
print(f"ERROR 级别日志: {error_logs}")
# 提取所有登录信息
login_pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) INFO User '([^']+)' logged in from ([\d.]+)"
login_logs = re.findall(login_pattern, log_content)
print(f"登录信息: {login_logs}")正则表达式的最佳实践
1. 编写清晰的正则表达式
- 使用注释:对于复杂的正则表达式,使用
re.VERBOSE标志添加注释 - 使用命名捕获组:提高代码可读性
- 合理分组:使用括号分组相关的模式
python
import re
# 使用 VERBOSE 标志添加注释
email_pattern = re.compile(r"""
\b # 单词边界
[A-Za-z0-9._%+-]+ # 用户名:字母、数字、点、下划线、百分号、加号、减号
@ # @ 符号
[A-Za-z0-9.-]+ # 域名:字母、数字、点、减号
\. # 点
[A-Z|a-z]{2,} # 顶级域名:至少 2 个字母
\b # 单词边界
""", re.VERBOSE)
# 使用命名捕获组
date_pattern = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})")2. 测试正则表达式
3. 性能优化
- 编译正则表达式:对于频繁使用的正则表达式,编译后使用
- 避免回溯:尽量避免可能导致回溯的模式,如
.* - 使用适当的量词:根据实际情况选择贪婪或非贪婪量词
- 限制匹配范围:使用锚点(如
^、$)和单词边界(如\b)限制匹配范围
python
import re
import time
# 测试编译与未编译正则表达式的性能
text = "联系我们:support@example.com 或 info@company.org" * 1000
# 未编译的正则表达式
start_time = time.time()
for _ in range(1000):
re.findall(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", text)
end_time = time.time()
print(f"未编译正则表达式耗时: {end_time - start_time:.4f} 秒")
# 编译的正则表达式
compiled_pattern = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
start_time = time.time()
for _ in range(1000):
compiled_pattern.findall(text)
end_time = time.time()
print(f"编译正则表达式耗时: {end_time - start_time:.4f} 秒")4. 安全考虑
- 避免使用
.*:可能导致灾难性回溯 - 限制输入长度:防止 ReDoS(正则表达式拒绝服务)攻击
- 验证用户输入:不要直接将用户输入作为正则表达式模式
实际应用示例
示例 1:表单验证
python
import re
def validate_form(data):
"""验证表单数据"""
errors = {}
# 验证姓名
name = data.get("name", "")
if not name:
errors["name"] = "姓名不能为空"
elif not re.match(r"^[\w\s]+$", name):
errors["name"] = "姓名只能包含字母、数字、下划线和空格"
# 验证邮箱
email = data.get("email", "")
if not email:
errors["email"] = "邮箱不能为空"
elif not re.match(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", email):
errors["email"] = "邮箱格式不正确"
# 验证手机号
phone = data.get("phone", "")
if not phone:
errors["phone"] = "手机号不能为空"
elif not re.match(r"1[3-9]\d{9}", phone):
errors["phone"] = "手机号格式不正确"
# 验证密码
password = data.get("password", "")
if not password:
errors["password"] = "密码不能为空"
elif len(password) < 8:
errors["password"] = "密码长度至少 8 个字符"
elif not re.search(r"[A-Z]", password):
errors["password"] = "密码至少包含一个大写字母"
elif not re.search(r"[a-z]", password):
errors["password"] = "密码至少包含一个小写字母"
elif not re.search(r"\d", password):
errors["password"] = "密码至少包含一个数字"
elif not re.search(r"[!@#$%^&*(),.?":{}|<>]", password):
errors["password"] = "密码至少包含一个特殊字符"
# 验证确认密码
confirm_password = data.get("confirm_password", "")
if not confirm_password:
errors["confirm_password"] = "确认密码不能为空"
elif confirm_password != password:
errors["confirm_password"] = "两次输入的密码不一致"
return len(errors) == 0, errors
# 测试表单验证
form_data = {
"name": "Alice Smith",
"email": "alice@example.com",
"phone": "13812345678",
"password": "Password123!",
"confirm_password": "Password123!"
}
valid, errors = validate_form(form_data)
if valid:
print("表单验证通过")
else:
print("表单验证失败:")
for field, error in errors.items():
print(f"{field}: {error}")
# 测试失败的情况
invalid_form_data = {
"name": "",
"email": "invalid-email",
"phone": "1234567890",
"password": "password",
"confirm_password": "different"
}
valid, errors = validate_form(invalid_form_data)
if valid:
print("表单验证通过")
else:
print("表单验证失败:")
for field, error in errors.items():
print(f"{field}: {error}")示例 2:文本处理
python
import re
def process_text(text):
"""处理文本"""
# 移除 HTML 标签
text = re.sub(r"<[^>]+>", "", text)
# 标准化空白字符
text = re.sub(r"\s+", " ", text).strip()
# 移除多余的标点符号
text = re.sub(r"[.!?]{2,}", "", text)
# 首字母大写
sentences = re.split(r"[.!?]+", text)
processed_sentences = []
for sentence in sentences:
if sentence:
sentence = sentence.strip()
if sentence:
sentence = sentence[0].upper() + sentence[1:]
processed_sentences.append(sentence)
return ". ".join(processed_sentences) + "."
# 测试文本处理
text = """
<p> hello world! this is a test. </p>
<p>it's a wonderful day, isn't it???</p>
<p>python is great!!!</p>
"""
processed_text = process_text(text)
print(f"处理前:\n{text}")
print(f"处理后:\n{processed_text}")示例 3:数据提取
python
import re
# 从 HTML 中提取链接
def extract_links(html):
"""从 HTML 中提取链接"""
link_pattern = r"<a\s+href=["']([^"']+)["'][^>]*>([^<]+)</a>"
matches = re.finditer(link_pattern, html)
links = []
for match in matches:
url = match.group(1)
text = match.group(2)
links.append({"url": url, "text": text})
return links
# 测试链接提取
html = """
<a href="https://www.example.com">Example</a>
<a href="https://www.python.org">Python</a>
<a href="https://www.github.com" target="_blank">GitHub</a>
"""
links = extract_links(html)
print("提取的链接:")
for link in links:
print(f"文本: {link['text']}, URL: {link['url']}")
# 从 JSON 字符串中提取特定字段
def extract_json_fields(json_str, fields):
"""从 JSON 字符串中提取特定字段"""
import json
try:
data = json.loads(json_str)
result = {}
for field in fields:
# 使用正则表达式处理嵌套字段
if "." in field:
parts = field.split(".")
value = data
for part in parts:
if isinstance(value, dict) and part in value:
value = value[part]
else:
value = None
break
result[field] = value
else:
result[field] = data.get(field)
return result
except json.JSONDecodeError:
return {}
# 测试 JSON 字段提取
json_str = '{"name": "Alice", "age": 30, "address": {"city": "New York", "zipcode": "10001"}}'
fields = ["name", "age", "address.city", "address.zipcode", "phone"]
result = extract_json_fields(json_str, fields)
print("提取的 JSON 字段:")
print(result)总结
正则表达式是 Python 中强大的字符串处理工具,它可以用于匹配、提取、替换和分割字符串。本章节介绍了:
- 正则表达式基础:基本字符、转义字符、贪婪与非贪婪匹配
- re 模块的使用:常用函数、匹配对象、标志参数
- 基本示例:匹配字符串、提取字符串、替换字符串、分割字符串
- 高级示例:编译正则表达式、命名捕获组、零宽断言、复杂正则表达式
- 最佳实践:编写清晰的正则表达式、测试正则表达式、性能优化、安全考虑
- 实际应用示例:表单验证、文本处理、数据提取
掌握正则表达式的使用,对于 Python 中的文本处理、数据验证和分析非常重要。通过合理地使用正则表达式,可以大大提高代码的效率和可读性。