Python 基础教程:正则表达式实战

前言

正则表达式是文本处理的瑞士军刀。无论是验证用户输入、提取网页数据,还是清洗日志文本,正则都能游刃有余。然而很多开发者对正则望而生畏,要么写出一堆难以维护的”神秘符号”,要么遇到问题就百度复制。

本文将帮助您:

  • 系统掌握正则的核心语法
  • 熟练运用Python re 模块的各种 API
  • 积累可直接复用的实战模板
  • 避坑常见匹配失败原因

正则基础语法速查

元字符一览

元字符说明示例
.任意单个字符(换行除外)a.c 匹配 “abc”
\d任意数字 [0-9]\d+ 匹配 “123”
\D任意非数字 [^0-9]\D+ 匹配 “abc”
\w字母数字下划线 [a-zA-Z0-9_]\w+ 匹配 “hello_1”
\W非字母数字 [^a-zA-Z0-9_]\W+ 匹配 “@#”
\s空白字符(空格、Tab、换行)\s+ 匹配 ” “
\S非空白字符\S+ 匹配 “hello”
\b单词边界\bword\b 精确匹配 “word”
\B非单词边界\Bword 匹配 “sword”
^字符串开始^hello 匹配 “hello world”
$字符串结束world$ 匹配 “hello world”

字符类

# 基本字符类
[abc]      # 匹配 a、b 或 c 中的任意一个
[^abc]     # 匹配除了 a、b、c 以外任意字符
[a-z]      # 匹配小写字母
[A-Z]      # 匹配大写字母
[0-9]      # 匹配数字
[a-zA-Z0-9]  # 匹配字母或数字
​
# 预定义字符类(更简洁)
\d  = [0-9]
\w  = [a-zA-Z0-9_]
\s  = [ \t\n\r\f\v]

量词

量词说明示例
*零个或多个(贪婪)ab* 匹配 “a”、”ab”、”abbb”
+一个或多个(贪婪)ab+ 匹配 “ab”、”abbb”(不匹配 “a”)
?零个或一个colou?r 匹配 “color”、”colour”
{n}精确 n 个\d{4} 匹配 “2026”
{n,}至少 n 个\d{2,} 匹配 “12”、”123″
{n,m}n 到 m 个\d{2,4} 匹配 “12” 到 “1234”

贪婪 vs 非贪婪

import re
​
text = "<div>content</div>"
​
# 贪婪:尽可能多匹配
re.search(r'<.+>', text).group()  # 匹配 "<div>content</div>"
​
# 非贪婪:尽可能少匹配
re.search(r'<.+?>', text).group()  # 匹配 "<div>"

捕获组与特殊语法

# () 捕获组:提取特定部分
match = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2026-05-15')
print(match.group(1))  # "2026" — 第一个捕获组
print(match.group(2))  # "05" — 第二个捕获组
print(match.group(3))  # "15" — 第三个捕获组
print(match.groups())  # ('2026', '05', '15') — 全部捕获组
​
# 命名捕获组(Python 3.6+)
match = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-05-15')
print(match.group('year'))   # "2026"
print(match.groupdict())     # {'year': '2026', 'month': '05', 'day': '15'}
​
# (?:...) 非捕获组:不创建捕获组(性能更好)
re.findall(r'(?:\d+\.)+\d+', '版本 1.2.3')  # ['1.2.3']
​
# (?=...) 正向先行断言:后面跟着...
re.findall(r'\w+(?=\s+USD)', '100 USD, 200 EUR')  # ['100']
​
# (?!...) 负向先行断言:后面不跟着...
re.findall(r'\d+(?!\s+USD)', '100 USD, 200')  # ['200']
​
# (?<=...) 正向后行断言:前面是...
re.findall(r'(?<=\$)\d+', '$100, €200')  # ['100']
​
# (?<!...) 负向后行断言:前面不是...
re.findall(r'(?<!\$)\d+', '$100, 200')  # ['100', '20'] ⚠️ 匹配所有数字

转义字符

# 需要转义的字符:. ^ $ * + ? { } [ ] \ | ( )
# 在字符类 [] 中只需转义:] \ ^ -
​
# 匹配字面点号
re.search(r'\.', 'abc.def')  # 匹配 "."
​
# 匹配 IP 地址格式(转义点号)
re.search(r'\d+\.\d+\.\d+\.\d+', '192.168.1.1')
​
# re.escape() 自动转义
pattern = re.escape('[test]')  # r'\[test\]'
re.findall(pattern, '[test] string')

Python re 模块详解

六大核心函数

函数说明返回值
re.match()从字符串开头匹配匹配对象/None
re.search()扫描整个字符串,首个匹配匹配对象/None
re.findall()扫描整个字符串,所有匹配字符串列表
re.finditer()扫描整个字符串,所有匹配迭代器
re.sub()替换匹配项新字符串
re.split()按匹配项分割字符串列表
import re
​
text = "我的邮箱是 test@example.com,备用邮箱 backup@test.org"
​
# 1. match:从开头匹配
result = re.match(r'我的', text)
print(result.group())  # "我的"
​
result = re.match(r'邮箱', text)  # None — 不从开头
​
# 2. search:扫描首个匹配
result = re.search(r'\w+@\w+\.\w+', text)
print(result.group())  # "test@example.com"
​
# 3. findall:返回所有匹配
emails = re.findall(r'\w+@\w+\.\w+', text)
print(emails)  # ['test@example.com', 'backup@test.org']
​
# 4. finditer:迭代器,适合大文本
for match in re.finditer(r'\w+@\w+\.\w+', text):
    print(f"位置 {match.start()}-{match.end()}: {match.group()}")
​
# 5. sub:替换
new_text = re.sub(r'\w+@\w+\.\w+', '[已隐藏]', text)
print(new_text)  # "我的邮箱是 [已隐藏],备用邮箱 [已隐藏]"
​
# 6. split:分割
parts = re.split(r'[,,]', "苹果,香蕉,橙子")
print(parts)  # ['苹果', '香蕉', '橙子']

match 对象详解

text = "2026-05-15 晴朗"
​
# search 返回 match 对象
match = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', text)
​
# 常用方法
print(match.group(0))        # "2026-05-15" — 整个匹配
print(match.group(1))        # "2026" — 第一个捕获组
print(match.group('year'))   # "2026" — 命名捕获组
print(match.groups())        # ('2026', '05', '15') — 所有捕获组
print(match.groupdict())     # {'year': '2026', 'month': '05', 'day': '15'}
​
# 位置信息
print(match.start())         # 0 — 匹配开始位置
print(match.end())           # 10 — 匹配结束位置
print(match.span())          # (0, 10) — (开始, 结束)
​
# 搜索对象
print(match.pos)             # 搜索起始位置
print(match.lastindex)       # 最后一个捕获组的索引

编译与 flags

# 编译正则表达式(重复使用时更高效)
pattern = re.compile(r'\d{4}-\d{2}-\d{2}')
​
# 编译后可调用
pattern.findall('2026-05-15 天气晴')
pattern.search('今天 2026-05-15')
pattern.match('2026-05-15')  # 从头匹配
​
# flags 参数
text = "Hello\nWorld"
​
# re.IGNORECASE / re.I — 忽略大小写
re.findall(r'hello', text, re.I)  # ['Hello']
​
# re.MULTILINE / re.M — ^ 和 $ 匹配行首行尾
text = "第一行\n第二行"
re.findall(r'^\w+', text, re.M)  # ['第一行', '第二行']
​
# re.DOTALL / re.S — . 匹配换行
re.findall(r'.+', text, re.S)  # ['第一行\n第二行']
​
# re.VERBOSE / re.X — 允许注释
pattern = re.compile(r'''
    \d{4}  # 年份
    -      # 分隔符
    \d{2}  # 月份
    -      # 分隔符
    \d{2}  # 日期
''', re.VERBOSE)
​
# 组合使用
re.search(r'hello', text, re.I | re.M)
​
# 内联 flags(只影响当前组)
re.findall(r'(?i)hello', text)  # 忽略大小写
re.findall(r'(?x)\d+ \w+', text)  # 允许空格和注释

常用模式实战

1. 邮箱验证

import re
​
def validate_email(email):
    """验证邮箱格式"""
    pattern = r'''
        ^                      # 字符串开始
        [a-zA-Z0-9._%+-]+      # 用户名部分
        @                      # @ 符号
        [a-zA-Z0-9.-]+         # 域名部分
        \.[a-zA-Z]{2,}$        # 顶级域名
    '''
    return bool(re.fullmatch(pattern, email, re.VERBOSE))
​
# 测试
emails = [
    "test@example.com",
    "user.name+tag@domain.co.uk",
    "invalid.email",
    "@nodomain.com",
    "spaces in@email.com"
]
​
for email in emails:
    result = validate_email(email)
    print(f"{email:30} -> {'✅ 有效' if result else '❌ 无效'}")

2. 手机号验证

import re
​
def validate_phone(phone):
    """验证中国手机号"""
    pattern = r'^1[3-9]\d{9}$'
    return bool(re.fullmatch(pattern, phone))
​
def format_phone(phone):
    """格式化手机号:13812345678 -> 138-1234-5678"""
    match = re.match(r'1[3-9](\d{3})(\d{4})(\d{4})', phone)
    if match:
        return f"{match.group(1)}-{match.group(2)}-{match.group(3)}"
    return phone
​
# 测试
phones = ["13812345678", "19912345678", "12345678901", "138123456789"]
for phone in phones:
    print(f"{phone:15} 验证: {validate_phone(phone)} | 格式化: {format_phone(phone)}")

3. URL 提取与解析

import re
from urllib.parse import urlparse
​
def extract_urls(text):
    """提取文本中所有 URL"""
    pattern = r'https?://[^\s<>"{}|\\^`\[\]]+'
    return re.findall(pattern, text)
​
def parse_url(url):
    """解析 URL 各部分"""
    match = re.match(
        r'(?P<scheme>https?)://'
        r'(?:(?P<user>\w+):(?P<password>\w+)@)?'
        r'(?P<host>[^/:]+)'
        r'(?::(?P<port>\d+))?'
        r'(?P<path>/[^\s]*)?',
        url
    )
    if match:
        return match.groupdict()
    return {}
​
# 使用
text = "访问 https://user:pass@example.com:8080/api/v1/users?id=123"
urls = extract_urls(text)
print(f"提取到 URL: {urls}")
​
parsed = parse_url(urls[0])
print(f"解析结果: {parsed}")

4. HTML 标签内容提取

import re
​
def strip_html(html):
    """移除所有 HTML 标签"""
    return re.sub(r'<[^>]+>', '', html)
​
def extract_links(html):
    """提取所有链接和文本"""
    pattern = r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>([^<]*)</a>'
    return re.findall(pattern, html)
​
def extract_img_srcs(html):
    """提取所有图片 src"""
    pattern = r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>'
    return re.findall(pattern, html)
​
# 示例
html = '''
<a href="https://example.com">示例链接</a>
<img src="https://example.com/img.png" alt="图片" />
<a href="https://example.org">另一个链接</a>
'''
​
print(f"链接: {extract_links(html)}")
print(f"图片: {extract_img_srcs(html)}")
print(f"纯文本: {strip_html(html)}")

5. 日志解析

import re
from datetime import datetime
from collections import Counter
​
LOG_PATTERN = re.compile(
    r'(?P<timestamp>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+'
    r'\[(?P<level>\w+)\]\s+'
    r'(?P<logger>[\w.]+)\s+-\s+'
    r'(?P<message>.+)'
)
​
def parse_log_line(line):
    """解析单条日志"""
    match = LOG_PATTERN.match(line)
    if match:
        data = match.groupdict()
        # 转换时间戳
        data['time'] = datetime.strptime(data['timestamp'], '%Y-%m-%d %H:%M:%S')
        return data
    return None
​
def analyze_logs(log_content):
    """分析日志统计"""
    logs = []
    for line in log_content.strip().split('\n'):
        parsed = parse_log_line(line)
        if parsed:
            logs.append(parsed)
    
    # 统计各级别日志数量
    levels = Counter(log['level'] for log in logs)
    
    # 统计各 logger 日志数量
    loggers = Counter(log['logger'] for log in logs)
    
    return {
        'total': len(logs),
        'levels': dict(levels),
        'loggers': dict(loggers.most_common(5))
    }
​
# 示例日志
sample_log = """
2026-05-15 10:30:00 [INFO] app.server - 服务器启动
2026-05-15 10:30:01 [DEBUG] app.db - 连接数据库
2026-05-15 10:30:05 [ERROR] app.api - 请求处理失败
2026-05-15 10:30:06 [INFO] app.server - 收到新请求
"""
​
result = analyze_logs(sample_log)
print(f"日志统计: {result}")

6. 数据清洗

import re
​
def clean_text(text):
    """文本清洗"""
    # 移除多余空格
    text = re.sub(r'\s+', ' ', text)
    
    # 移除特殊控制字符
    text = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', text)
    
    # 规范化引号
    text = re.sub(r'[""]', '"', text)
    text = re.sub(r'['']', "'", text)
    
    # 移除多余标点
    text = re.sub(r'\.{2,}', '.', text)
    
    return text.strip()
​
def extract_numbers(text):
    """提取所有数字(整数或浮点数)"""
    pattern = r'-?\d+\.?\d*'
    return [float(x) for x in re.findall(pattern, text)]
​
def remove_emoji(text):
    """移除 Emoji"""
    emoji_pattern = re.compile(
        "["
        "\U0001F600-\U0001F64F"  # emoticons
        "\U0001F300-\U0001F5FF"  # symbols & pictographs
        "\U0001F680-\U0001F6FF"  # transport & map symbols
        "\U0001F1E0-\U0001F1FF"  # flags
        "\U00002702-\U000027B0"
        "\U000024C2-\U0001F251"
        "]+", flags=re.UNICODE
    )
    return emoji_pattern.sub(r'', text)
​
# 测试
dirty_text = '  文本   中间  有   多余 空格  '
print(f"清洗后: '{clean_text(dirty_text)}'")  # '文本中间有多余空格'
​
print(f"提取数字: {extract_numbers('价格: 19.9元, 数量: 5件')}")  # [19.9, 5]
​
print(f"移除Emoji: {remove_emoji('Hello 👋 World 🌍!')}")  # 'Hello  World !'

高级技巧

1. 前瞻后顾(环视)

import re
​
text = "apple apple banana apple"
​
# 正向后顾 (?<=...):前面必须是...
pattern = r'(?<=\bapple\s)banana'  # apple 后面跟着的 banana
print(re.findall(pattern, text))  # ['banana']
​
# 负向后顾 (?<!...):前面不能是...
pattern = r'(?<!red\s)apple'  # red 后面跟着的 apple
print(re.findall(pattern, text))  # ['apple', 'apple'](第一个 red apple 被排除)
​
# 正向前瞻 (?=...)
pattern = r'\w+(?=\s+banana)'  # banana 前面的单词
print(re.findall(pattern, text))  # ['apple']
​
# 负向前瞻 (?!...)
pattern = r'\w+(?!\s+banana)'  # 不是 banana 前面的单词
print(re.findall(pattern, text))  # ['apple', 'apple', 'apple']

2. 条件匹配

import re
​
# (?(id)yes|no) 条件匹配:如果指定组匹配则用 yes,否则用 no
​
# 示例:匹配带引号的字符串
text = '"hello" and "world" and unquoted'
​
# 捕获引号
pattern = r'("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\')'
print(re.findall(pattern, text))  # ['"hello"', '"world"']

3. 递归与平衡组

import re
​
# 匹配配对符号(如 HTML 标签)
# 注意:正则不适合真正解析 HTML,这里仅作演示
​
def match_balanced(text, open_char='(', close_char=')'):
    """匹配配对括号内容"""
    pattern = rf'''
        {re.escape(open_char)}   # 开始符号
        (?:                      # 非捕获组
            [^(){open_char}{close_char}]  # 普通字符
            |                    # 或
            (?P<recursion>{re.escape(open_char)}...{re.escape(close_char)})  # 递归
        )*
        {re.escape(close_char)}  # 结束符号
    '''
    return re.findall(pattern, text, re.VERBOSE)
​
# 示例
print(match_balanced('(a + (b - c))'))  # ['(a + (b - c))']

4. 替换的高级用法

import re

text = "用户: 张三, 年龄: 25, 城市: 北京"

# 替换函数
def normalize_age(match):
    age = int(match.group(1))
    return f"{age}岁" if age < 100 else f"{age}岁(异常)"

result = re.sub(r'年龄:\s*(\d+)', normalize_age, text)
print(result)  # "用户: 张三, 年龄: 25岁, 城市: 北京"

# 使用命名组引用
def swap_name(match):
    return f"{match.group('last')}{match.group('first')}"

pattern = r'名字:(?P<first>\w+)(?P<last>\w+)'
text = "名字: 大明"
print(re.sub(pattern, swap_name, text))  # "大明"

# 反向引用(在替换中使用捕获组)
text = "<b>加粗</b> 和 <i>斜体</i>"

# 将 HTML 标签转换为 Markdown
result = re.sub(r'<(\w+)>(.*?)</\1>', r'**\2**', text)
print(result)  # "**加粗** 和 **斜体**"

5. split 高级用法

import re

# 按多个分隔符分割
text = "苹果,香蕉;橙子|葡萄-草莓"
result = re.split(r'[,;|\-]', text)
print(result)  # ['苹果', '香蕉', '橙子', '葡萄', '草莓']

# 保留分隔符
result = re.split(r'(?<=[,;|])(?=\S)', text)
print(result)

# 按数字分组分割
text = "用户1年龄30用户2年龄25用户3年龄40"
result = re.split(r'(?<=\D)(?=\d)|(?<=\d)(?=\D)', text)
print(result)  # ['用户', '1', '年龄', '30', '用户', '2', '年龄', '25', ...]

# 使用捕获组保留分隔符
text = "a,b,c"
result = re.split(r'(,)', text)
print(result)  # ['a', ',', 'b', ',', 'c']

常见场景模板

模板1:密码强度验证

import re
​
def validate_password(password):
    """验证密码强度"""
    errors = []
    
    if len(password) < 8:
        errors.append("密码长度至少8位")
    if not re.search(r'[A-Z]', password):
        errors.append("必须包含大写字母")
    if not re.search(r'[a-z]', password):
        errors.append("必须包含小写字母")
    if not re.search(r'\d', password):
        errors.append("必须包含数字")
    if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        errors.append("必须包含特殊字符")
    
    return {
        'valid': len(errors) == 0,
        'errors': errors
    }
​
# 测试
test_passwords = ["Abc123!", "weak", "NoSpecial1", "Valid@Pass123"]
for pwd in test_passwords:
    result = validate_password(pwd)
    status = "✅" if result['valid'] else "❌"
    print(f"{status} {pwd:20} -> {result['errors'] if result['errors'] else '通过'}")

模板2:身份证号验证

import re
​
def validate_chinese_id(id_number):
    """验证中国身份证号"""
    # 基本格式检查
    if not re.fullmatch(r'[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]', id_number):
        return False
    
    # 校验位验证
    weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    check_codes = '10X98765432'
    
    total = sum(int(id_number[i]) * weights[i] for i in range(17))
    check_code = check_codes[total % 11]
    
    return check_code == id_number[17].upper()
​
# 测试
ids = ["11010119900101123X", "11010119900101123x", "invalid", "000000000000000000"]
for id_num in ids:
    print(f"{id_num} -> {validate_chinese_id(id_num)}")

模板3:IP 地址验证与排序

import re

def validate_ip(ip):
    """验证 IPv4 地址"""
    pattern = r'^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$'
    return bool(re.fullmatch(pattern, ip))

def sort_ip_addresses(ips):
    """按 IP 地址数值排序"""
    def ip_to_int(ip):
        parts = re.findall(r'\d+', ip)
        return sum(int(p) * (256 ** (3 - i)) for i, p in enumerate(parts))
    
    return sorted(ips, key=ip_to_int)

# 测试
test_ips = ["192.168.1.1", "10.0.0.1", "192.168.1.10", "172.16.0.1", "0.0.0.0"]
valid_ips = [ip for ip in test_ips if validate_ip(ip)]
print(f"有效 IP: {valid_ips}")
print(f"排序后: {sort_ip_addresses(valid_ips)}")

模板4:Markdown 链接提取

import re

def extract_markdown_links(text):
    """提取 Markdown 链接"""
    pattern = r'\[([^\]]+)\]\(([^\)]+)\)'
    matches = re.findall(pattern, text)
    return [{'text': m[0], 'url': m[1]} for m in matches]

def markdown_to_html(text):
    """Markdown 链接转 HTML"""
    def replace_link(match):
        text, url = match.groups()
        return f'<a href="{url}" target="_blank">{text}</a>'
    
    return re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', replace_link, text)

# 示例
md_text = '''
欢迎访问 [Python官网](https://python.org)!
另一个 [链接](https://example.com) 在这里。
'''

print("链接列表:")
for link in extract_markdown_links(md_text):
    print(f"  - {link}")

print("\n转换为 HTML:")
print(markdown_to_html(md_text))

性能优化

1. 预编译正则

import re
import time

# ❌ 循环中重复编译(慢)
start = time.time()
for _ in range(10000):
    re.search(r'\d{4}-\d{2}-\d{2}', '2026-05-15')
print(f"重复编译: {time.time() - start:.4f}s")

# ✅ 预编译(快)
pattern = re.compile(r'\d{4}-\d{2}-\d{2}')
start = time.time()
for _ in range(10000):
    pattern.search('2026-05-15')
print(f"预编译: {time.time() - start:.4f}s")

2. 避免回溯灾难

import re
import time
​
# ❌ 回溯灾难:可能导致灾难性回溯
bad_pattern = re.compile(r'(a+)+b')  # 对 "aaa...aa" 无 'b' 时极慢
test_str = 'a' * 30
​
start = time.time()
try:
    bad_pattern.match(test_str)
except:
    pass
print(f"有回溯问题的正则: {time.time() - start:.4f}s")
​
# ✅ 优化:使用原子组或更精确的模式
good_pattern = re.compile(r'a+b')  # 简单直接
start = time.time()
good_pattern.match(test_str)
print(f"优化后: {time.time() - start:.4f}s")

3. 选择更高效的匹配方式

import re
​
text = "a" * 10000 + "b"
​
# ❌ findall 扫描整个字符串
start = time.time()
re.findall(r'a*b', text)
print(f"findall: {time.time() - start:.6f}s")
​
# ✅ search 找到即停
start = time.time()
re.search(r'a*b', text)
print(f"search: {time.time() - start:.6f}s")
​
# ✅ match 只检查开头(更快)
start = time.time()
re.match(r'a+b', text)  # 不需要,检查开头是 'a' 开头不匹配
print(f"match: {time.time() - start:.6f}s")

4. 使用恰当的字符类

# ❌ 慢
re.findall(r'(?:a|b|c|d|e|f|g)', text)
​
# ✅ 快
re.findall(r'[a-g]', text)
​
# ❌ 慢:点号通配
re.findall(r'.+', text)
​
# ✅ 快:排除更具体
re.findall(r'[^x]+', text)

排错指南

常见错误与解决方案

错误1:匹配结果为空

import re
​
text = "Hello World"
​
# ❌ 问题1:大小写不匹配
result = re.findall(r'hello', text)  # []
​
# ✅ 解决:添加 re.I 或 (?i)
result = re.findall(r'(?i)hello', text)  # ['Hello']
​
# ❌ 问题2:没有使用原始字符串
result = re.findall('\n', text)  # \n 被解释为换行符
​
# ✅ 解决:使用 r'' 或双反斜杠
result = re.findall(r'\n', text)  # []
result = re.findall('\\n', text)  # []

错误2:只匹配部分内容

text = "价格: 100元, 重量: 200克"
​
# ❌ 只获取到第一个匹配
result = re.search(r'\d+', text).group()  # "100"
​
# ✅ 使用 findall 获取所有
result = re.findall(r'\d+', text)  # ['100', '200']
​
# ❌ search 只返回第一个
# ✅ 需要全部匹配时用 findall

错误3:非贪婪匹配问题

text = "<div>content1</div><div>content2</div>"
​
# ❌ 非贪婪 .+? 可能匹配错误
result = re.search(r'<div>.+?</div>', text).group()  # "<div>content1</div>"
# ✅ 正确
​
text = "<div><span>content</span></div>"
​
# ❌ .+? 会停在第一个 </div>
result = re.search(r'<div>.+?</div>', text).group()  # "<div><span>content</span></div>"
# ✅ 正确,因为非贪婪会匹配最短
​
# 如果 HTML 结构复杂,考虑:
# 1. 使用 BeautifulSoup 等专用解析器
# 2. 使用更精确的模式
pattern = r'<div>(?:(?!<div>|</div>).)*</div>'

错误4:转义字符问题

# ❌ 在字符类 [] 中错误转义
result = re.findall(r'[abc\]d]', 'abcde')  # [] 中 ] 需要转义
# 结果: ['a', 'b', 'c', 'd'] — 可能不是预期
​
# ✅ 正确转义
result = re.findall(r'[abc\]d]', 'abcde')  # 匹配 a, b, c, ], d
result = re.findall(r'[a-c]d', 'ade bde cde')  # 匹配 ad, bd, cd
​
# ❌ ^ 在字符类中位置错误
result = re.findall(r'[^abc]', 'abc123')  # ^ 在开头表示否定
​
# ✅ ^ 不在开头则字面匹配
result = re.findall(r'[a^bc]', 'abc^')  # 匹配 a, b, c, ^

错误5:正则性能问题

import re
import time
​
# 模拟长文本
text = "abc" * 10000 + "abcx"
​
# ❌ 低效模式
start = time.time()
pattern = re.compile(r'(abc)+x')
pattern.match(text)
print(f"低效模式: {time.time() - start:.6f}s")
​
# ✅ 高效模式
start = time.time()
pattern = re.compile(r'abc+x')
pattern.match(text)
print(f"高效模式: {time.time() - start:.6f}s")

调试技巧

import re
​
# 1. 打印匹配详情
def debug_regex(pattern, text):
    """调试正则表达式"""
    match = re.search(pattern, text)
    if match:
        print(f"✅ 匹配成功: '{match.group()}'")
        print(f"   位置: {match.span()}")
        if match.groups():
            print(f"   捕获组: {match.groups()}")
        if match.groupdict():
            print(f"   命名组: {match.groupdict()}")
    else:
        print(f"❌ 匹配失败")
        print(f"   尝试模式: {pattern}")
        print(f"   输入文本: '{text}'")
​
# 使用
debug_regex(r'(\w+)@(\w+)\.(\w+)', 'test@example.com')
​
# 2. 分步构建正则
parts = {
    'username': r'[a-zA-Z0-9._%+-]+',
    'domain': r'[a-zA-Z0-9.-]+',
    'tld': r'\.[a-zA-Z]{2,}'
}
email_pattern = rf"^{parts['username']}@{parts['domain']}{parts['tld']}$"
print(f"构建的正则: {email_pattern}")
​
# 3. 使用 regex101.com 验证
# 在浏览器中可视化调试正则表达式

总结

核心要点

  1. 优先使用原始字符串 r'':避免转义地狱
  2. 预编译 re.compile():重复匹配时性能提升显著
  3. 选择合适的函数match/search/findall 各有用途
  4. 注意贪婪 vs 非贪婪.+? vs .+
  5. 字符类替代点号[abc]+ 优于 .+
  6. 环视不断言(?=...) 不消耗字符

速查表

┌─────────────────────────────────────────────────────────────┐
│                    正则表达式速查表                          │
├──────────────────┬──────────────────────────────────────────┤
│     场景         │                   模式                    │
├──────────────────┼──────────────────────────────────────────┤
│ 邮箱             │ r'^[\w.-]+@[\w.-]+\.\w{2,}$'            │
│ 手机号           │ r'^1[3-9]\d{9}$'                         │
│ URL              │ r'https?://\S+'                          │
│ IP 地址          │ r'\d{1,3}(?:\.\d{1,3}){3}'              │
│ 日期 YYYY-MM-DD  │ r'\d{4}-\d{2}-\d{2}'                   │
│ 时间 HH:MM:SS    │ r'\d{2}:\d{2}:\d{2}'                   │
│ HTML 标签        │ r'<[^>]+>'                              │
│ 中文字符         │ r'[\u4e00-\u9fa5]+'                     │
│ 纯数字           │ r'^\d+$'                                │
│ 空白行           │ r'^\s*$'                                │
│ 捕获组引用       │ r'(\w+)=\1' — 重复单词                  │
└──────────────────┴──────────────────────────────────────────┘

推荐工具

工具用途
regex101.com可视化正则测试,支持 Python
regexr.com正则学习与测试
PyrexpChrome 正则调试插件
The Regex Coach桌面正则工具

💡 提示: 正则虽强大,但并非所有场景都适用。解析复杂格式(如 HTML、JSON)建议使用专用解析库。 📚 扩展阅读:


原创内容,版权所有。未经授权,禁止转载。

© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容