第四十篇:正则表达式——从入门到实战
前言
**正则表达式(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 & special chars <here>.</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\)
# 可用于安全嵌入用户输入
最佳实践总结
✅ 推荐做法
- 始终使用原始字符串
r""— 避免转义冲突 - 预编译频繁使用的模式 —
re.compile()提升性能 - 需要多个匹配时用 finditer — 比 findall 节省内存
- 命名分组增强可读性 —
(?P<name>...)比数字索引更清晰 - 使用 VERBOSE 编写复杂模式 — 可加注释、分行的模式更易维护
- 验证而非提取 — 严格匹配 — 用
^...$限定范围防止误匹配
❌ 避免做法
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 正则表达式的核心知识:
- 基础语法 — 字符类(\d/\w/\s)、量词(*+/?)、分组(()),锚点(^/$/\b)
- re 核心函数 — search/findall/finditer/sub/compile 的使用场景
- 高级特性 — 惰性匹配(?/)、零宽断言(?=/)(?<=/)、命名分组、VERBOSE 模式
- 实战案例 — 数据验证、日志解析、网页数据提取、HTML 清洗、文本转换
- 常见陷阱 — 原始字符串、贪婪匹配、只匹配一次、特殊字符转义
- 最佳实践 — 预编译、finditer 替代 findall、命名分组增强可读性
下一篇文章将进入进阶专题:多进程与多线程。
本文是 Python 教程系列的第四十篇。
