我们来系统地解析 Python 中的正则表达式(re 模块),并提供丰富的用例。

一、核心概念

正则表达式是一种用于匹配字符串模式的强大工具。Python 通过内置的 re 模块提供支持。

常用元字符:

| 元字符 | 描述 |
| :--- | :--- |
. | 匹配任意一个字符(除了换行符 \n) |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
* | 匹配前面的字符0次或多次(贪婪) |
+ | 匹配前面的字符1次或多次(贪婪) |
? | 匹配前面的字符0次或1次(也可用于非贪婪模式) |
{m} | 匹配前面的字符m次 |
{m,n} | 匹配前面的字符m到n次 |
[ ] | 匹配字符集合中的任意一个字符(例如 [abc] 匹配 a, b, 或 c) |
| | 操作,匹配左边或右边的表达式 |
( ) | 1. 分组,将表达式括起来作为一个整体 2. 捕获匹配的内容 |
\ | 转义特殊字符,或表示特殊序列(如 \d\s) |

常用特殊序列:

| 特殊序列 | 描述 | 等价字符集 |
| :--- | :--- | :--- |
\d | 匹配任意数字 | [0-9] |
\D | 匹配任意非数字 | [^0-9] |
\s | 匹配任意空白字符(空格、换行、制表符等) | [ \t\n\r\f\v] |
\S | 匹配任意非空白字符 | [^ \t\n\r\f\v] |
\w | 匹配任意单词字符(字母、数字、下划线) | [a-zA-Z0-9_] |
\W | 匹配任意非单词字符 | [^a-zA-Z0-9_] |
\b | 匹配一个单词的边界(开头或结尾的空位置) | |
\B | 匹配非单词边界 | |


二、re 模块核心方法

1. re.match(pattern, string, flags=0)

从字符串的起始位置开始匹配模式。如果起始位置匹配成功,返回一个匹配对象;否则返回 None

import re

result = re.match(r'Hello', 'Hello, world!')
if result:
    print("Found:", result.group())  # 输出: Found: Hello

result2 = re.match(r'world', 'Hello, world!')
print(result2)  # 输出: None,因为字符串不是以 'world' 开头

2. re.search(pattern, string, flags=0)

扫描整个字符串,找到第一个匹配模式的位置。返回一个匹配对象,如果没有找到则返回 None

import re

text = "我的电话号码是: 123-4567-8901,请惠存。"
pattern = r'\d{3}-\d{4}-\d{4}' # 匹配 xxx-xxxx-xxxx 格式的电话号码

result = re.search(pattern, text)
if result:
    print("找到电话号码:", result.group())  # 输出: 找到电话号码: 123-4567-8901

3. re.findall(pattern, string, flags=0)

找到字符串中所有匹配模式的子串,并以列表的形式返回。如果模式中有分组,则返回分组的元组列表。

import re

text = "苹果的价格是$5,香蕉的价格是$3,橙子的价格是$7。"
pattern = r'\$(\d+)'  # 匹配 $ 符号后的数字,并用分组捕获数字

prices = re.findall(pattern, text)
print(prices)  # 输出: ['5', '3', '7'] (一个字符串列表)

4. re.finditer(pattern, string, flags=0)

与 findall 类似,但返回一个迭代器,其中每个元素都是一个匹配对象。这对于处理大文本或需要匹配对象信息时更高效。

import re

text = "Python is great. I love Python."
pattern = r'Python'

# findall 只能返回字符串
all_matches = re.findall(pattern, text)
print(all_matches)  # 输出: ['Python', 'Python']

# finditer 返回匹配对象,可以获取位置信息
for match in re.finditer(pattern, text):
    print(f"Found '{match.group()}' at position {match.start()}-{match.end()}")
# 输出:
# Found 'Python' at position 0-6
# Found 'Python' at position 21-27

5. re.sub(pattern, repl, string, count=0, flags=0)

将字符串中所有匹配模式的部分替换为另一个字符串 repl,并返回替换后的新字符串。

  • repl:可以是一个字符串,也可以是一个函数。
  • count:最大替换次数,为 0 时表示全部替换。
import re

text = "今天是2023-04-01,天气晴。2024-05-15是明天。"
# 将日期格式从 YYYY-MM-DD 替换为 DD/MM/YYYY
# \1, \2, \3 代表前面第1、2、3个分组捕获的内容
new_text = re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\3/\2/\1', text)
print(new_text)
# 输出: 今天是01/04/2023,天气晴。15/05/2024是明天。

# 使用函数作为 repl
def to_upper(match_obj):
    return match_obj.group().upper()

text = "hello world"
new_text = re.sub(r'\w+', to_upper, text)
print(new_text)  # 输出: HELLO WORLD

6. re.split(pattern, string, maxsplit=0, flags=0)

使用模式作为分隔符来分割字符串,返回一个列表。

import re

text = "苹果,香蕉,,橙子;西瓜"
# 使用逗号、分号或任意连续的空格/逗号作为分隔符
result = re.split(r'[,,\s;]+', text)
print(result)  # 输出: ['苹果', '香蕉', '橙子', '西瓜']

三、匹配对象(Match Object)的方法

当 match() 或 search() 成功时,返回的是一个匹配对象,它有以下常用方法:

  • .group():返回匹配的整个字符串。
  • .group(n):返回第 n 个分组(用 () 括起来的部分)匹配的字符串。
  • .groups():返回一个包含所有分组匹配字符串的元组。
  • .start() / .end():返回匹配开始/结束的位置。
  • .span():返回一个元组 (start, end),包含匹配的起始和结束位置。

分组用例:

import re

text = "林纳斯·托瓦兹(Linus Torvalds)创造了Linux。"
# 使用分组分别捕获中文名和英文名
pattern = r'([\u4e00-\u9fa5·]+)(([A-Za-z ]+))'

match = re.search(pattern, text)
if match:
    print(f"整个匹配: {match.group(0)}")
    print(f"中文名: {match.group(1)}")
    print(f"英文名: {match.group(2)}")
    print(f"所有分组: {match.groups()}")

# 输出:
# 整个匹配: 林纳斯·托瓦兹(Linus Torvalds)
# 中文名: 林纳斯·托瓦兹
# 英文名: Linus Torvalds
# 所有分组: ('林纳斯·托瓦兹', 'Linus Torvalds')

四、编译正则表达式 (re.compile)

如果你需要重复使用同一个模式,可以先将其编译成一个正则表达式对象(Pattern Object),然后使用这个对象的方法,这样可以提高效率。

import re

# 未编译的模式,每次调用 re.method 都会在内部编译一次
result1 = re.search(r'\d+', 'abc123')
result2 = re.search(r'\d+', 'def456')

# 编译后的模式,只需编译一次
pattern_obj = re.compile(r'\d+')
result1 = pattern_obj.search('abc123')
result2 = pattern_obj.search('def456')

# 编译时也可以指定标志,如忽略大小写
pattern_insensitive = re.compile(r'hello', re.IGNORECASE)
result = pattern_insensitive.search('HELLO World')
print(result.group())  # 输出: HELLO

五、综合用例

用例1:验证邮箱地址

import re

def validate_email(email):
    # 一个相对简单的邮箱正则
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    if re.match(pattern, email):
        return True
    else:
        return False

emails = ["user@example.com", "invalid-email", "first.last@company.co.uk"]
for email in emails:
    print(f"{email}: {'Valid' if validate_email(email) else 'Invalid'}")

# 输出:
# user@example.com: Valid
# invalid-email: Invalid
# first.last@company.co.uk: Valid

用例2:提取并处理日志信息

import re

log_data = """
[2023-10-27 14:35:01] INFO - User ‘Alice’ logged in from 192.168.1.101.
[2023-10-27 14:36:45] ERROR - Database connection failed (Timeout).
[2023-10-27 14:38:02] WARNING - Disk usage over 80% on /dev/sda1.
"""

# 模式:提取时间戳、日志级别、消息内容
pattern = r'\[(.*?)\] (\w+) - (.*)'

for line in log_data.strip().split('\n'):
    match = re.search(pattern, line)
    if match:
        timestamp, level, message = match.groups()
        # 只处理 ERROR 级别的日志
        if level == 'ERROR':
            print(f"发现错误!时间:{timestamp}, 信息:{message}")
# 输出:
# 发现错误!时间:2023-10-27 14:36:45, 信息:Database connection failed (Timeout).

用例3:强大的字符串清理和格式化

import re

dirty_text = "   This is    a messy    string,  with  extra spaces AND Mixed CASE.   "
# 1. 去除首尾空格
clean_text = dirty_text.strip()
# 2. 将多个连续空格替换为一个空格
clean_text = re.sub(r'\s+', ' ', clean_text)
# 3. 将句子首字母大写,其余小写
clean_text = clean_text.capitalize()

print(f"Before: '{dirty_text}'")
print(f"After:  '{clean_text}'")
# 输出:
# Before: '   This is    a messy    string,  with  extra spaces AND Mixed CASE.   '
# After:  'This is a messy string, with extra spaces and mixed case.'

总结

| 任务 | 推荐方法 |
| :--- | :--- |
| 检查字符串是否以某模式开头 | re.match() |
查找第一个匹配项 | re.search() |
查找所有匹配项(只需字符串) | re.findall() |
查找所有匹配项(需要位置等信息) | re.finditer() |
替换所有匹配项 | re.sub() |
使用复杂分隔符分割字符串 | re.split() |
模式需要多次使用 | re.compile() |

掌握这些核心概念和方法,你就能应对绝大多数需要使用正则表达式的场景了。正则表达式需要多加练习,推荐使用 regex101.com 这类在线工具来测试和调试你的表达式。