Skip to content

Python 正则表达式

正则表达式(Regular Expression,简称 regex 或 regexp)是一种用于匹配字符串中字符组合的模式。在 Python 中,正则表达式通过 re 模块实现。本章节将详细介绍 Python 中正则表达式的概念、语法和使用方法。

什么是正则表达式?

正则表达式是一种用于描述字符串模式的工具,它可以:

  1. 匹配字符串:检查字符串是否符合特定模式
  2. 提取字符串:从字符串中提取符合特定模式的部分
  3. 替换字符串:替换字符串中符合特定模式的部分
  4. 分割字符串:根据特定模式分割字符串

正则表达式在文本处理、数据验证、日志分析等场景中非常有用。

正则表达式基础

基本字符

字符描述示例
普通字符匹配自身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.IGNORECASEre.I忽略大小写
re.MULTILINEre.M多行模式,^$ 匹配每行的开头和结尾
re.DOTALLre.S点号匹配所有字符,包括换行符
re.VERBOSEre.X详细模式,允许添加注释和空白
re.ASCIIre.A仅 ASCII 模式,\w\d 等仅匹配 ASCII 字符
re.UNICODEre.UUnicode 模式,\w\d 等匹配 Unicode 字符
re.LOCALEre.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. 测试正则表达式

  • 使用在线工具测试:如 Regex101RegExr
  • 编写单元测试:确保正则表达式在各种情况下都能正确工作
  • 处理边缘情况:考虑各种可能的输入格式

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 中强大的字符串处理工具,它可以用于匹配、提取、替换和分割字符串。本章节介绍了:

  1. 正则表达式基础:基本字符、转义字符、贪婪与非贪婪匹配
  2. re 模块的使用:常用函数、匹配对象、标志参数
  3. 基本示例:匹配字符串、提取字符串、替换字符串、分割字符串
  4. 高级示例:编译正则表达式、命名捕获组、零宽断言、复杂正则表达式
  5. 最佳实践:编写清晰的正则表达式、测试正则表达式、性能优化、安全考虑
  6. 实际应用示例:表单验证、文本处理、数据提取

掌握正则表达式的使用,对于 Python 中的文本处理、数据验证和分析非常重要。通过合理地使用正则表达式,可以大大提高代码的效率和可读性。