返回博客

40. 正则表达式:从入门到实战

40. 正则表达式:从入门到实战

第四十篇:正则表达式——从入门到实战

前言

**正则表达式(Regular Expression)**是处理文本的强大工具。无论是验证邮箱格式、提取 URL、还是批量替换文本,正则都能优雅解决。Python 的 re 模块提供了完整的正则支持。本文将系统讲解核心语法和实战应用。

基础语法速览

常用字符类

import re

# . 匹配任意字符(除换行符)
print(re.match("a.c", "abc"))    # 匹配 abc
print(re.match("a.c", "a1c"))    # 匹配 a1c
print(re.match("a.c", "ac"))     # None

# \d 匹配数字 [0-9]
print(re.findall(r"\d+", "我有 3 个苹果,5 元"))
# ['3', '5']

# \w 匹配字母数字下划线 [a-zA-Z0-9_]
print(re.findall(r"\w+", "Hello_World_123"))
# ['Hello_World_123']

# \s 匹配空白字符 [ \\t\\n\\r\\f\\v]
print(re.split(r"\s+", "hello   world\\ttest"))
# ['hello', 'world', 'test']

# 反向字符类
# \D 非数字, \W 非单词字符, \S 非空白
print(re.findall(r"\D+", "abc123def456"))
# ['abc', 'def']

量词

import re

text = "foo fooo foom"

# * 匹配 0 次或多次
print(re.findall(r"f.*m", text))
# ['foo', 'fooo', 'foom']

# + 匹配 1 次或多次
print(re.findall(r"f.+m", text))
# ['foo', 'fooo', 'foom']

# ? 匹配 0 次或 1 次
print(re.findall(r"colou?r", "color colour colouur"))
# ['color', 'colour']

# {n} 精确匹配 n 次
print(re.findall(r"a{3}", "aaa aa aaaa aaaaa"))
# ['aaa', 'aaa']

# {n,} 至少 n 次
print(re.findall(r"a{2,}", "aa aaa a aaaaa"))
# ['aa', 'aaa', 'aaaaa']

# {n,m} n 到 m 次
print(re.findall(r"a{2,4}", "aa aaa aaaaa"))
# ['aa', 'aaa', 'aaa'] (贪婪匹配最多 4)

分组与捕获

import re

# () 创建分组
match = re.search(r"(\d{4})-(\d{2})-(\d{2})", "2026-09-09")
print(match.group(0))  # 完整匹配: 2026-09-09
print(match.group(1))  # 第一组: 2026
print(match.group(2))  # 第二组: 09
print(match.group(3))  # 第三组: 09
print(match.groups())  # ('2026', '09', '09')

# re.sub 中引用分组
phone = "张三 13800138000, 李四 13900139000"
masked = re.sub(r"(\d{3})\d{4}(\d{4})", r"\1****\2", phone)
print(masked)
# 张三 138****8000, 李四 139****9000

锚点

import re

# ^ 字符串开头, $ 字符串结尾
print(re.match(r"^Hello", "Hello World"))      # 匹配
print(re.match(r"World$", "Hello World"))       # 匹配
print(re.match(r"^Hello$", "Hello World"))      # None (中间有内容)
print(re.match(r"^Hello$", "Hello"))            # 匹配

# \b 单词边界
print(re.findall(r"\bword\b", "a word sentence"))    # ['word']
print(re.findall(r"\bword\b", "aword sentence"))     # []

# \A 字符串开头(仅), \Z 字符串结尾(仅)
print(re.match(r"\A\d+", "123abc"))  # 匹配
print(re.search(r"\d+\Z", "abc123")) # 匹配

re 模块核心函数

search vs findall vs finditer

import re

html = """
<h1>Title</h1>
<p class="intro">Introduction</p>
<p class="main">Main content here</p>
"""

# search — 找第一个匹配
match = re.search(r"<title>(.*?)</title>", html)
if match:
    print(f"找到标题: {match.group(1)}")
else:
    print("未找到标题")
# 未找到标题 (HTML 中没有 <title>)

# findall — 返回所有匹配的列表
print(re.findall(r'<p class="(.*?)">(.*?)</p>', html))
# [('intro', 'Introduction'), ('main', 'Main content here')]

# finditer — 返回迭代器(内存友好)
for match in re.finditer(r'<h[1-6]>(.*?)</h[1-6]>', html):
    print(f"找到标题: '{match.group(1)}' at position {match.start()}")

re.sub — 替换

import re

# 基本替换
text = "apple banana cherry apple date"
result = re.sub(r"apple", "orange", text)
print(result)
# orange banana cherry orange date

# 替换指定次数(count 参数)
result = re.sub(r"apple", "orange", text, count=1)
print(result)
# orange banana cherry apple date

# 使用函数动态生成替换内容
def multiply_match(m):
    return str(int(m.group()) * 2)

print(re.sub(r"\d+", multiply_match, "scores are 85, 92, 78"))
# scores are 170, 184, 156

# re.subn 返回替换结果和新计数
result, count = re.subn(r"[aeiou]", "_", "beautiful")
print(f"结果: {result}, 替换了 {count} 次")
# 结果: b__ut__ful, 替换了 5 次

re.compile — 预编译模式

import re
import time

# 一次性编译,重复使用时提升性能
pattern = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})")

# 命名分组更清晰
date_str = "今天是 2026-09-09"
match = pattern.search(date_str)
if match:
    print(f"年: {match.group('year')}")   # 2026
    print(f"月: {match.group('month')}")   # 09
    print(f"日: {match.group('day')}")     # 09

# 性能对比
text = "a" * 10000 + "b"
bad_pattern = re.compile(r"^a*b$")

start = time.perf_counter()
for _ in range(10000):
    bad_pattern.search(text)
print(f"预编译: {(time.perf_counter() - start)*1000:.1f}ms")

start = time.perf_counter()
for _ in range(10000):
    re.search(r"^a*b$", text)
print(f"每次编译: {(time.perf_counter() - start)*1000:.1f}ms")

高级特性

分支与惰性匹配

import re

text = "<div>content</div><span>other</span>"

# 惰性匹配(非贪婪)— ? 跟在量词后
print(re.findall(r"<.*?>", text))  # 惰性: [<div>, </div>, <span>, </span>]
print(re.findall(r"<.*>", text))   # 贪婪: [<div>content</div><span>other</span>]

# 分支 | — 左边优先匹配
print(re.sub(r"(foo|bar)", lambda m: m.group().upper(), "foo bar foo"))
# FOO BAR FOO

# 分支注意:只尝试第一个成功的分支
pattern = re.compile(r"(car|cart)")
print(pattern.match("cartoon").group())  # cart (先匹配成功)

零宽断言

import re

# (?=...) 正向先行断言 — 后面必须是...
# 匹配带单位的数字
numbers = re.findall(r"\d+(?=px|%|em)", "font-size: 14px; margin: 20%; padding: 1em")
print(numbers)  # ['14', '20', '1']

# (?!...) 负向先行断言 — 后面不能是...
print(re.findall(r"\w+(?!@)", "user@example.com admin@test.org"))
# ['example', 'com', 'test', 'org']

# (?<=...) 正向后行断言 — 前面必须是...
print(re.findall(r"(?<=\$)\d+", "I have $100 and $200"))
# ['100', '200']

# (?<!...) 负向后行断言 — 前面不能是...
print(re.findall(r"(?<!http://)\w+\.\w+", "http://google.com https://yahoo.com plain.org"))
# ['plain.org']  (排除 http:// 开头的 URL)

条件式与命名分组

import re

# 条件式: (?(name)yes|no)
# 如果有 name 组,则匹配 yes;否则匹配 no
pattern = re.compile(r"(?P<quote>[\"']).*?(?P=quote)|(?:[^\s*])+")

# 引用命名分组中的值
html = '<a href="https://example.com" title="Example Site">Click Here</a>'
pattern = re.compile(r'<a\s+href="(?P<url>[^"]+)".*?>(?P<text>[^<]+)</a>')
match = pattern.search(html)
if match:
    print(f"URL: {match.group('url')}")
    print(f"文本: {match.group('text')}")
    # URL: https://example.com
    # 文本: Click Here

标志选项

import re

text = """Line 1
LINE 2
line three
LINE THREE"""

# re.IGNORECASE / re.I — 不区分大小写
print(re.findall(r"line\d", text, re.I))
# ['Line', 'LINE']

# re.MULTILINE / re.M — 多行模式 (^/$ 匹配每行首尾)
lines = ["hello world", "goodbye world", "HELLO WORLD"]
for line in lines:
    if re.match(r"^hello\b", line, re.I | re.M):
        print(f"以 hello 开头: {line}")

# re.DOTALL / re.S — . 匹配换行符
html_content = "<div>Hello\nWorld</div>"
print(re.findall(r"<div>(.*?)</div>", html_content, re.S))
# ['Hello\nWorld']

# re.VERBOSE / re.X — 允许在模式中使用空格和注释
pattern = re.compile(r"""
    (?P<area_code> \(?\d{3}\)? )  # 区号,如 (123) 或 123
    [\s.\-]?                      # 可选的分隔符
    (\d{3})                       # 前三位
    [\s.\-]?                      # 可选的分隔符
    (\d{4})                       # 最后四位
""", re.VERBOSE)

phone = "(123) 456-7890"
match = pattern.search(phone)
if match:
    print(f"区号: {match.group('area_code')}")
    print(f"号码: {match.group(1)}-{match.group(2)}")

实战场景集

数据验证

import re

class Validators:
    """常用数据验证"""
    
    @staticmethod
    def is_email(email):
        """邮箱验证"""
        pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
        return bool(pattern.match(email))
    
    @staticmethod
    def is_phone_cn(phone):
        """中国大陆手机号验证"""
        pattern = re.compile(r"^1[3-9]\d{9}$")
        return bool(pattern.match(phone))
    
    @staticmethod
    def is_url(url):
        """URL 验证"""
        pattern = re.compile(
            r"^https?://"  # 协议
            r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,63}\.?|"  # 域名
            r"localhost|"  # localhost
            r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"  # IP
            r":?\d+"  # 端口
            r"(?:/[-a-zA-Z0-9_./~:&%+#=%!]*)?$", re.I
        )
        return bool(pattern.match(url))
    
    @staticmethod
    def has_html_tags(text):
        """检测是否包含 HTML 标签"""
        return bool(re.search(r"<[^>]+>", text))

tests = [
    ("xiao@example.com", Validators.is_email),
    ("13800138000", Validators.is_phone_cn),
    ("https://www.example.com/path?q=1", Validators.is_url),
    ("<b>bold</b>", Validators.has_html_tags),
]

for value, validator in tests:
    print(f"{value!r:30s} → {'✓' if validator(value) else '✗'}")

日志解析

import re

log_lines = """
2026-09-09 12:00:01 ERROR [main] Database connection timeout after 30s
2026-09-09 12:00:02 WARN [worker] Retry attempt 3 of 5 for request /api/data
2026-09-09 12:00:03 INFO [server] Request completed: GET /health 200 OK (12ms)
2026-09-09 12:00:04 DEBUG [cache] Cache hit ratio: 0.85 (hits: 850, misses: 150)
2026-09-09 12:00:05 ERROR [auth] Invalid token: expired at 2026-09-08T23:59:59Z
"""

# 定义解析模式
log_pattern = re.compile(
    r"(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+"
    r"(?P<level>DEBUG|INFO|WARN|ERROR|CRITICAL)\s+"
    r"\[(?P<component>\w+)\]\s+"
    r"(?P<message>.*)"
)

from collections import defaultdict

stats = defaultdict(int)
errors = []

for line in log_lines.strip().split("\n"):
    match = log_pattern.match(line.strip())
    if match:
        stats[match.group("level")] += 1
        if match.group("level") in ("ERROR", "CRITICAL"):
            errors.append(match.group("message"))
        print(f"[{match.group('level'):8s}] {match.group('component'):8s} | {match.group('message')[:50]}...")

print(f"\n统计:")
for level, count in sorted(stats.items()):
    print(f"  {level}: {count}")
print(f"\n错误消息 ({len(errors)} 条):")
for err in errors:
    print(f"  - {err}")

爬虫数据提取

import re

html = """
<article class="post">
    <h2 class="title">Python 教程之正则表达式详解</h2>
    <div class="meta">
        <span class="author">作者: 小明</span>
        <span class="date">发布于 2026-09-09</span>
        <span class="views">阅读: 1234</span>
    </div>
    <div class="content">
        <p>正则表达式是一种强大的文本处理工具...</p>
        <img src="https://image.wordflow.cn/regex-example.png" alt="示例图片"/>
    </div>
</article>
"""

# 提取文章信息
title = re.search(r'<h2[^>]*class="title"[^>]*>(.*?)</h2>', html, re.S)
author = re.search(r'class="author"[^>]*>(.*?)</span>', html, re.S)
date = re.search(r'class="date"[^>]*>.*?(\d{4}-\d{2}-\d{2})', html, re.S)
views = re.search(r'class="views"[^>]*>.*?阅读:\s*(\d+)', html, re.S)
image_urls = re.findall(r'<img[^>]*src="([^"]+)"', html, re.I)

print(f"标题: {title.group(1).strip()}")
print(f"作者: {author.group(1).strip().replace('作者: ', '')}")
print(f"日期: {date.group(1)}")
print(f"阅读量: {views.group(1)}")
print(f"图片: {', '.join(image_urls)}")

HTML 清理与清洗

import re

raw_html = '''
<html>
<body>
    <h1>Welcome!</h1>
    <script>alert('xss')</script>
    <p>This is <b>important</b> content with <a href="http://example.com" target="_blank">a link</a>.</p>
    <!-- This is a comment -->
    <style>body { background: red; }</style>
    <p>More text &amp; special chars &lt;here&gt;.</p>
</body>
</html>
'''

# 移除脚本和样式标签
cleaned = re.sub(r'<script[^>]*>.*?</script>', '', raw_html, flags=re.S)
cleaned = re.sub(r'<style[^>]*>.*?</style>', '', cleaned, flags=re.S)

# 移除 HTML 注释
cleaned = re.sub(r'<!--.*?-->', '', cleaned, flags=re.S)

# 去除多余空白
cleaned = re.sub(r'\s+', ' ', cleaned).strip()

print(cleaned[:200])

格式化文本转换

import re

def camel_to_snake(name):
    """驼峰转蛇形命名"""
    result = re.sub(r'([A-Z])', r'_\1', name).lower()
    return result.lstrip('_')

def snake_to_camel(name):
    """蛇形转驼峰命名"""
    return ''.join(word.capitalize() for word in name.split('_'))

print(camel_to_snake("XMLParser"))          # xml_parser
print(camel_to_snake("simpleHTTPServer"))   # simple_http_server
print(snake_to_camel("my_database_connection"))  # MyDatabaseConnection

def markdown_links(text):
    """将 [text](url) 转换为 HTML 链接"""
    return re.sub(
        r'\[([^\]]+)\]\(([^)]+)\)',
        r'<a href="\2">\1</a>',
        text
    )

md_text = "Visit [Python](https://python.org) and [TRAE](https://trae.ai)."
print(markdown_links(md_text))
# Visit <a href="https://python.org">Python</a> and <a href="https://trae.ai">TRAE</a>.

常见陷阱与避坑指南

import re

# ⚠️ 陷阱 1: 忘记使用原始字符串 r""
# re.compile("\\d") → \d 被 Python 转义成普通字符
# re.compile(r"\d") → \d 才是正则的数字匹配
print(re.findall(r"\d+", "123"))   # ✓ 正确: ['123']
# print(re.findall("\\d+", "123")) # ✗ 可能出问题

# ⚠️ 陷阱 2: 贪婪匹配导致意外结果
text = "a<b>a</b>b<b>c</b>"
print(re.findall(r"<.*>", text))
# ['<b>a</b>b<b>c</b>'] ← 贪吃掉了一切!
print(re.findall(r"<.*?>", text))
# ['<b>', '</b>', '<b>', '</b>'] ← 用惰性修正

# ⚠️ 陷阱 3: re.search 只找第一次匹配
text = "one two three"
print(re.search(r"\w+", text).group())
# one  (只找到了第一个)
print(re.findall(r"\w+", text))
# ['one', 'two', 'three'] (全部找到)

# ⚠️ 陷阱 4: 忽略特殊字符
special_chars = re.escape("price: $99.99 (including tax)")
print(special_chars)
# price: \$99\.99 \(including tax\)
# 可用于安全嵌入用户输入

最佳实践总结

✅ 推荐做法

  1. 始终使用原始字符串 r"" — 避免转义冲突
  2. 预编译频繁使用的模式 — re.compile() 提升性能
  3. 需要多个匹配时用 finditer — 比 findall 节省内存
  4. 命名分组增强可读性 — (?P<name>...) 比数字索引更清晰
  5. 使用 VERBOSE 编写复杂模式 — 可加注释、分行的模式更易维护
  6. 验证而非提取 — 严格匹配 — 用 ^...$ 限定范围防止误匹配

❌ 避免做法

import re

# ❌ 用正则解析 HTML/XML(请用 BeautifulSoup 等专用库)
# re.findall(r'<a href="(.*?)">(.*?)</a>', html)  ← 脆弱且容易出错

# ❌ 过度复杂的正则(超过 100 字符难以维护)
# 拆分成多个简单步骤或用函数逐步处理

# ❌ 忽视 re.DOTALL 导致的换行问题
# re.search(r'.*', 'multi\nline')  ← 默认不会跨行
# re.search(r'.*', 'multi\nline', re.DOTALL)  ← 这才跨行

常用正则模式收集

用途 模式 说明
邮箱 ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ 基本邮箱验证
手机号(CN) ^1[3-9]\d{9}$ 中国大陆手机号
IP 地址 \b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b IPv4 地址
网址 ^https?://[^\s/$.?#].[^\s]*$ HTTP/HTTPS URL
UUID ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ 标准 UUID
日期 \d{4}-\d{2}-\d{2} YYYY-MM-DD
时间戳 \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} YYYY-MM-DD HH:MM:SS

总结

本文系统讲解了 Python 正则表达式的核心知识:

  1. 基础语法 — 字符类(\d/\w/\s)、量词(*+/?)、分组(()),锚点(^/$/\b)
  2. re 核心函数 — search/findall/finditer/sub/compile 的使用场景
  3. 高级特性 — 惰性匹配(?/)、零宽断言(?=/)(?<=/)、命名分组、VERBOSE 模式
  4. 实战案例 — 数据验证、日志解析、网页数据提取、HTML 清洗、文本转换
  5. 常见陷阱 — 原始字符串、贪婪匹配、只匹配一次、特殊字符转义
  6. 最佳实践 — 预编译、finditer 替代 findall、命名分组增强可读性

下一篇文章将进入进阶专题:多进程与多线程。


本文是 Python 教程系列的第四十篇。