什么是正则表达式

正则表达式(Regular Expression,简称 regex)是一种用于匹配字符串中字符组合的模式。它提供了一种强大、灵活且高效的方式来处理文本——搜索、匹配、替换和提取数据。

Python 的 re 模块提供了完整的正则表达式支持。在本章中,我们将从基础语法开始,逐步深入到实战应用。

第一个正则表达式

import re

# 检查字符串中是否包含 "python"
text = "我喜欢 Python 编程语言"
pattern = r"Python"

result = re.search(pattern, text)
if result:
    print(f"找到了: {result.group()}")  # 输出: 找到了: Python

这里的 r"Python" 中的 r 表示原始字符串(raw string),它会告诉 Python 不要处理字符串中的反斜杠转义,这对于正则表达式至关重要。

re 模块的核心函数

re.match() — 从开头匹配

re.match() 从字符串的起始位置开始匹配,如果开头不匹配则返回 None

import re

text = "Python 是一门优秀的编程语言"

# 从开头匹配
match = re.match(r"Python", text)
if match:
    print(match.group())  # Python

# 不从开头匹配则失败
match = re.match(r"优秀", text)
print(match)  # None

re.search() — 搜索整个字符串

re.search() 扫描整个字符串,找到第一个匹配的位置

text = "我喜欢 Python,也喜欢 Java"

# 搜索整个字符串
match = re.search(r"Java", text)
if match:
    print(match.group())      # Java
    print(match.start())      # 匹配起始位置
    print(match.end())        # 匹配结束位置
    print(match.span())       # (起始, 结束) 元组

re.findall() — 查找所有匹配

re.findall() 返回所有匹配的字符串列表:

text = "我的邮箱是 alice@example.com,Bob 的邮箱是 bob@test.com"

emails = re.findall(r"\w+@\w+\.\w+", text)
print(emails)
# 输出: ['alice@example.com', 'bob@test.com']

如果有分组,findall() 返回元组列表:

text = "2026-05-21, 2026-06-15"

dates = re.findall(r"(\d{4})-(\d{2})-(\d{2})", text)
print(dates)
# 输出: [('2026', '05', '21'), ('2026', '06', '15')]

re.finditer() — 迭代匹配对象

re.finditer() 返回一个迭代器,生成 Match 对象,适合处理大量匹配:

text = "Python 1, Java 2, Go 3"

for match in re.finditer(r"\d+", text):
    print(f"找到数字: {match.group()} 在位置 {match.span()}")
# 输出:
# 找到数字: 1 在位置 (7, 8)
# 找到数字: 2 在位置 (14, 15)
# 找到数字: 3 在位置 (19, 20)

re.sub() — 替换

re.sub() 替换所有匹配的文本:

text = "我的电话是 138-0000-1111,请拨打"

# 替换手机号为 ***
masked = re.sub(r"\d{3}-\d{4}-\d{4}", "***-****-****", text)
print(masked)  # 我的电话是 ***-****-****,请拨打

# 使用函数进行动态替换
def replace_func(match):
    phone = match.group()
    return phone[:3] + "-****-" + phone[-4:]

masked = re.sub(r"\d{3}-\d{4}-\d{4}", replace_func, text)
print(masked)  # 我的电话是 138-****-1111,请拨打

re.split() — 分割

re.split() 按模式分割字符串:

text = "苹果, 香蕉; 橙子 | 葡萄"

# 按逗号、分号或竖线分割
fruits = re.split(r"[,;|]\s*", text)
print(fruits)  # ['苹果', '香蕉', '橙子', '葡萄']

元字符与字符类

常用元字符

元字符 说明 示例
. 匹配任意单个字符(除换行符) a.b 匹配 acba_b
^ 匹配字符串开头 ^Hello 匹配以 Hello 开头的字符串
$ 匹配字符串结尾 end$ 匹配以 end 结尾的字符串
* 匹配前一个字符 0 次或多次 ab*c 匹配 acabcabbc
+ 匹配前一个字符 1 次或多次 ab+c 匹配 abcabbc,不匹配 ac
? 匹配前一个字符 0 次或 1 次 ab?c 匹配 acabc
{n} 精确匹配 n 次 a{3} 匹配 aaa
{n,} 至少匹配 n 次 a{2,} 匹配 aaaaa
{n,m} 匹配 n 到 m 次 a{2,4} 匹配 aaaaaaaaa

字符类

字符类用 [] 定义,匹配括号内的任意一个字符

import re

# [abc] 匹配 a、b 或 c
print(re.findall(r"[aeiou]", "hello world"))  # ['e', 'o', 'o']

# [a-z] 匹配所有小写字母
print(re.findall(r"[a-z]", "Hello 123"))  # ['e', 'l', 'l', 'o']

# [^abc] 匹配除 a、b、c 外的任意字符
print(re.findall(r"[^0-9]", "abc123def"))  # ['a', 'b', 'c', 'd', 'e', 'f']

预定义字符类

字符类 等价于 说明
\d [0-9] 匹配数字
\D [^0-9] 匹配非数字
\w [a-zA-Z0-9_] 匹配单词字符
\W [^a-zA-Z0-9_] 匹配非单词字符
\s [ \t\n\r\f\v] 匹配空白字符
\S [^ \t\n\r\f\v] 匹配非空白字符
text = "用户: 小明, 年龄: 25, 邮箱: xiao@test.com"

# 提取所有数字
print(re.findall(r"\d+", text))       # ['25']

# 提取所有单词
print(re.findall(r"\w+", text))       # ['用户', '小明', '年龄', '25', '邮箱', 'xiao', 'test', 'com']

分组与捕获

普通分组 ()

圆括号用于创建分组,可以提取匹配中的特定部分:

import re

text = "2026-05-21"

# 使用分组分别提取年、月、日
match = re.search(r"(\d{4})-(\d{2})-(\d{2})", text)
if match:
    print(match.group(0))   # 完整匹配: 2026-05-21
    print(match.group(1))   # 年: 2026
    print(match.group(2))   # 月: 05
    print(match.group(3))   # 日: 21
    print(match.groups())   # ('2026', '05', '21')

命名分组 (?P<name>)

命名分组让代码更可读:

text = "2026-05-21"

match = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", text)
if match:
    print(match.group("year"))    # 2026
    print(match.group("month"))   # 05
    print(match.group("day"))     # 21

# 在 re.sub 中使用命名分组
def format_date(match):
    return f"{match.group('month')}/{match.group('day')}/{match.group('year')}"

formatted = re.sub(
    r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})",
    format_date,
    "今天是 2026-05-21"
)
print(formatted)  # 今天是 05/21/2026

非捕获分组 (?:...)

有时只需要分组来应用量词,而不需要捕获内容:

text = "hello world hello python"

# 使用捕获分组(不必要的捕获)
matches = re.findall(r"(hello)\s+\w+", text)
print(matches)  # ['hello', 'hello'] — 只捕获了 hello

# 使用非捕获分组
matches = re.findall(r"(?:hello)\s+(\w+)", text)
print(matches)  # ['world', 'python'] — 只捕获了目标内容

反向引用

可以在同一个正则表达式中引用之前捕获的分组:

# 匹配重叠的单词(如 "hello hello")
text = "hello hello world world python"

duplicates = re.findall(r"(\w+)\s+\1", text)
print(duplicates)  # ['hello', 'world']

# 在 re.sub 中使用反向引用
text = "2026-05-21"
print(re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\2/\3/\1", text))
# 输出: 05/21/2026

前瞻断言与后顾断言

前瞻断言 (Lookahead)

前瞻断言检查后面是否跟着某个模式,但不消耗字符:

text = "5px, 10px, 20em, 30px"

# 正向前瞻: 匹配后面跟 "px" 的数字
print(re.findall(r"\d+(?=px)", text))
# 输出: ['5', '10', '30']

# 负向前瞻: 匹配后面不跟 "px" 的数字
print(re.findall(r"\d+(?!px)", text))
# 输出: ['5', '1', '20', '30']

后顾断言 (Lookbehind)

后顾断言检查前面是否有某个模式:

text = "$100, $200, €50, $300"

# 正向后顾: 匹配前面有 "$" 的数字
print(re.findall(r"(?<=\$)\d+", text))
# 输出: ['100', '200', '300']

# 负向后顾: 匹配前面没有 "$" 的数字
print(re.findall(r"(?<!\$)\d+", text))
# 输出: ['50']

综合使用

text = "apple: 5元, banana: 3元, cherry: 8元"

# 提取价格数字(前面有 ": ",后面有 "元")
prices = re.findall(r"(?<=: )\d+(?=元)", text)
print(prices)  # ['5', '3', '8']

贪婪与非贪婪

正则表达式默认是贪婪的,会尽可能多地匹配:

import re

text = "<div><p>你好</p></div>"

# 贪婪匹配: 尽可能长
greedy = re.search(r"<.+>", text)
print(greedy.group())  # <div><p>你好</p></div>

# 非贪婪匹配: 尽可能短(加 ?)
non_greedy = re.search(r"<.+?>", text)
print(non_greedy.group())  # <div>

关键区别:

text = "a123b456c"

# 贪婪
print(re.findall(r"a(.+)b", text))  # ['123b456']

# 非贪婪
print(re.findall(r"a(.+?)b", text))  # ['123']
贪婪版本 非贪婪版本
* *?
+ +?
? ??
{n,m} {n,m}?

编译正则表达式

当同一个正则表达式被多次使用时,编译它可以提高性能:

import re

# 编译正则表达式
email_pattern = re.compile(r"\b\w+@\w+\.\w+\b")

# 使用编译后的对象(方法相同)
text = "联系我们: support@example.com 或 info@test.org"

emails = email_pattern.findall(text)
print(emails)  # ['support@example.com', 'info@test.org']

for match in email_pattern.finditer(text):
    print(f"邮箱: {match.group()} (位置: {match.span()})")

编译的好处:

  1. 性能提升——正则表达式只解析一次
  2. 代码复用——在多个地方使用同一个模式
  3. 可读性——给模式一个有意义的变量名

标志 (Flags)

标志可以改变正则表达式的行为:

标志 缩写 说明
re.IGNORECASE re.I 忽略大小写
re.MULTILINE re.M 多行模式,^$ 匹配每行
re.DOTALL re.S . 匹配换行符
re.VERBOSE re.X 允许注释和空白,提高可读性
re.ASCII re.A \w\d 等只匹配 ASCII 字符

re.IGNORECASE

text = "Python python PYTHON"

print(re.findall(r"python", text, re.IGNORECASE))
# 输出: ['Python', 'python', 'PYTHON']

re.MULTILINE

text = """第一行
第二行
第三行"""

# 默认: ^ 只匹配整个字符串开头
print(re.findall(r"^\d+", text, re.MULTILINE))  # []

# 多行模式: ^ 匹配每行开头
text = """1. 第一项
2. 第二项
3. 第三项"""

print(re.findall(r"^\d+", text, re.MULTILINE))
# 输出: ['1', '2', '3']

re.DOTALL

text = "你好\n世界"

# 默认: . 不匹配换行符
print(re.findall(r"你.界", text))        # []

# DOTALL: . 匹配换行符
print(re.findall(r"你.界", text, re.DOTALL))  # ['你好\n世界']

re.VERBOSE

# 复杂正则表达式变得可读
pattern = re.compile(r"""
    \b                 # 单词边界
    \d{3,4}            # 区号: 3-4位数字
    [- ]?              # 可选的分隔符
    \d{7,8}            # 号码: 7-8位数字
    \b                 # 单词边界
""", re.VERBOSE)

text = "联系电话: 010-12345678 或 0755 87654321"
print(pattern.findall(text))
# 输出: ['010-12345678', '0755 87654321']

实战案例

1. 邮箱验证

import re

def is_valid_email(email: str) -> bool:
    """验证邮箱地址是否合法"""
    pattern = re.compile(r"""
        ^                    # 开头
        [a-zA-Z0-9._%+-]+    # 用户名: 字母数字._%+-
        @                    # @ 符号
        [a-zA-Z0-9.-]+       # 域名
        \.                   # 点
        [a-zA-Z]{2,}         # 顶级域名: 至少2个字母
        $                    # 结尾
    """, re.VERBOSE | re.IGNORECASE)
    return bool(pattern.match(email))

# 测试
emails = ["user@example.com", "invalid-email", "user@.com", "user.name@domain.co.uk"]
for email in emails:
    print(f"{email}: {'有效' if is_valid_email(email) else '无效'}")

2. 手机号提取与脱敏

import re

def extract_phones(text: str) -> list[str]:
    """提取中国手机号(11位数字,以1开头)"""
    return re.findall(r"\b1[3-9]\d{9}\b", text)

def mask_phones(text: str) -> str:
    """手机号脱敏:138****1111"""
    return re.sub(r"\b(1[3-9]\d)(\d{4})(\d{4})\b", r"\1****\3", text)

text = "请联系张先生 13812345678 或李女士 15987654321"
print(extract_phones(text))  # ['13812345678', '15987654321']
print(mask_phones(text))
# 请联系张先生 138****5678 或李女士 159****4321

3. URL 解析

import re

def parse_url(url: str) -> dict:
    """解析 URL 的各个组成部分"""
    pattern = re.compile(r"""
        (?P<protocol>https?)://      # 协议
        (?P<domain>[^/:]+)           # 域名
        (?::(?P<port>\d+))?          # 可选端口
        (?P<path>/[^?#]*)            # 路径
        (?:\?(?P<query>[^#]*))?      # 可选查询参数
        (?:\#(?P<fragment>.*))?      # 可选锚点
    """, re.VERBOSE | re.IGNORECASE)

    match = pattern.match(url)
    if match:
        return match.groupdict()
    return {}

url = "https://example.com:8080/path/to/page?name=python&version=3.13#section1"
print(parse_url(url))
# {
#     'protocol': 'https',
#     'domain': 'example.com',
#     'port': '8080',
#     'path': '/path/to/page',
#     'query': 'name=python&version=3.13',
#     'fragment': 'section1'
# }

4. 日志文件解析

import re

log_pattern = re.compile(r"""
    (?P<ip>\d+\.\d+\.\d+\.\d+)      # IP 地址
    \s-\s-\s
    \[(?P<time>[^\]]+)\]             # 时间戳
    \s"(?P<method>\w+)               # HTTP 方法
    \s(?P<path>[^\s]+)               # 请求路径
    \s[^"]*"
    \s(?P<status>\d{3})              # 状态码
""", re.VERBOSE)

log_line = '192.168.1.1 - - [21/May/2026:10:30:25 +0800] "GET /api/users HTTP/1.1" 200 1234'

match = log_pattern.match(log_line)
if match:
    data = match.groupdict()
    print(f"IP: {data['ip']}")
    print(f"时间: {data['time']}")
    print(f"方法: {data['method']}")
    print(f"路径: {data['path']}")
    print(f"状态: {data['status']}")

5. 数据清洗

import re

def clean_text(text: str) -> str:
    """清理文本中的多余空白和特殊字符"""
    # 移除 HTML 标签
    text = re.sub(r"<[^>]+>", "", text)
    # 将多个空白合并为一个
    text = re.sub(r"\s+", " ", text)
    # 移除开头和结尾的空白
    text = text.strip()
    return text

def extract_keywords(text: str) -> list[str]:
    """提取中文和英文关键词"""
    # 匹配中文字符或英文单词
    return re.findall(r"[\u4e00-\u9fff]+|[a-zA-Z]\w*", text)

html = "<p>欢迎学习  Python  编程!</p><br>这是 第二  段。</p>"
clean = clean_text(html)
print(clean)                         # 欢迎学习 Python 编程!这是 第二 段。

keywords = extract_keywords("Python编程 数据分析 Machine Learning")
print(keywords)  # ['Python', '编程', '数据分析', 'Machine', 'Learning']

常用正则表达式速查表

用途 正则表达式
邮箱 ^[\w._%+-]+@[\w.-]+\.[a-zA-Z]{2,}$
中国手机号 ^1[3-9]\d{9}$
固定电话 ^0\d{2,3}-?\d{7,8}$
URL ^https?://[\w./?=&%-]+$
IP 地址 (IPv4) ^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$
日期 (YYYY-MM-DD) ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
时间 (HH:MM:SS) ^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$
正整数 ^[1-9]\d*$
中文字符 [\u4e00-\u9fff]
HTTP 状态码 2xx ^2\d{2}$
强密码(8位以上,含大小写和数字) ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
连续重复单词 (\w+)\s+\1
16 进制颜色 ^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$

性能与最佳实践

1. 避免灾难性回溯

某些正则表达式在特定输入下会导致指数级回溯:

import re
import time

# BAD: 灾难性回溯
bad_pattern = re.compile(r"(a+)+b")
# 对 "aaaaaaaaac" 这样的输入会运行极长时间

# GOOD: 避免嵌套量词
good_pattern = re.compile(r"a+b")

# 测试
text = "a" * 30 + "c"
start = time.perf_counter()
bad_pattern.match(text)
print(f"BAD: {time.perf_counter() - start:.4f}秒")
# 指数级时间

2. 尽量使用原始字符串

# BAD: 普通字符串,需要双重转义
pattern = "\\d+\\.\\d+"

# GOOD: 原始字符串
pattern = r"\d+\.\d+"

3. 预编译频繁使用的模式

# BAD: 每次调用都解析
def has_email(text):
    return bool(re.search(r"\w+@\w+\.\w+", text))

# GOOD: 预编译
EMAIL_PATTERN = re.compile(r"\w+@\w+\.\w+")
def has_email(text):
    return bool(EMAIL_PATTERN.search(text))

4. 精确匹配时使用锚点

# BAD: 部分匹配
re.search(r"\d{11}", "abc13812345678def")  # 匹配成功

# GOOD: 精确匹配
re.fullmatch(r"1[3-9]\d{9}", "13812345678")  # 完全匹配

5. 使用非捕获分组提高性能

# BAD: 不必要的捕获
re.findall(r"(https?)://(\w+\.\w+)", url)

# GOOD: 不需要捕获时用非捕获分组
re.findall(r"(?:https?)://(\w+\.\w+)", url)

小结

正则表达式是文本处理的瑞士军刀。本章我们系统学习了 re 模块的核心函数(matchsearchfindallfinditersubsplit),元字符与字符类的基础用法,分组与捕获(包括命名分组和非捕获分组),前瞻/后顾断言,贪婪与非贪婪匹配,以及编译优化和标志使用。通过邮箱验证、手机号脱敏、URL 解析、日志解析和数据清洗等实战案例,你应该已经掌握了正则表达式在真实项目中的应用方式。最后,记得始终使用原始字符串(r"...")、预编译频繁使用的模式、避免灾难性回溯——这些最佳实践将帮助你写出高效且可靠的正则表达式。

Summary: re模块函数、元字符、分组断言、实战案例与速查表。