第 23 章 正则表达式
23.1 正则表达式语法速览
| 元字符 | 含义 | 示例 |
|---|---|---|
. | 任意字符(除换行) | a.c → “abc”, “a1c” |
^ | 行首 | ^Hello |
$ | 行尾 | world$ |
* | 0 次或多次 | ab*c → “ac”, “abc”, “abbc” |
+ | 1 次或多次 | ab+c → “abc”, “abbc” |
? | 0 次或 1 次 | colou?r → “color”, “colour” |
{n} | 恰好 n 次 | a{3} → “aaa” |
{n,m} | n 到 m 次 | a{2,4} → “aa”, “aaa”, “aaaa” |
[] | 字符集 | [abc] → “a” 或 “b” 或 “c” |
[^] | 取反字符集 | [^0-9] → 非数字 |
| | 或 | cat|dog |
() | 分组 | (ab)+ → “ab”, “abab” |
\ | 转义 | \. 匹配字面的 ”.” |
预定义字符类:
| 缩写 | 含义 | 等价 |
|---|---|---|
\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] |
\b | 单词边界 |
23.2 re 模块核心函数
import re
text = "Hello, my phone is 138-1234-5678, and email is alice@example.com"
# match — 从字符串开头匹配
m = re.match(r"Hello", text)
print(m.group()) # "Hello"
print(m.span()) # (0, 5)
# search — 搜索第一个匹配
m = re.search(r"\d{3}-\d{4}-\d{4}", text)
print(m.group()) # "138-1234-5678"
# findall — 找所有匹配,返回列表
numbers = re.findall(r"\d+", text)
print(numbers) # ['138', '1234', '5678']
# finditer — 找所有匹配,返回迭代器
for m in re.finditer(r"\d+", text):
print(f"找到 '{m.group()}' 在位置 {m.span()}")
# sub — 替换
cleaned = re.sub(r"\d", "*", text)
print(cleaned)
# "Hello, my phone is ***-****-****, and email is alice@example.com"
# 用函数替换
def censor_phone(match):
phone = match.group()
return phone[:3] + "-****-" + phone[-4:]
result = re.sub(r"\d{3}-\d{4}-\d{4}", censor_phone, text)
print(result) # "Hello, my phone is 138-****-5678, ..."
# split — 按模式分割
parts = re.split(r"[,;]\s*", "apple, banana; cherry, date")
print(parts) # ['apple', 'banana', 'cherry', 'date']
# subn — 替换并返回替换次数
result, count = re.subn(r"\d", "#", "a1b2c3")
print(result, count) # "a#b#c#" 3
23.3 编译正则与 flags
频繁使用的正则应该预编译:
import re
# 编译正则
pattern = re.compile(r"\d{3}-\d{4}-\d{4}")
# 编译后的对象有同样的方法
m = pattern.search(text)
matches = pattern.findall(text)
常用 flags
# re.IGNORECASE (re.I) — 忽略大小写
re.findall(r"hello", "Hello HELLO hello", re.I)
# ['Hello', 'HELLO', 'hello']
# re.MULTILINE (re.M) — 多行模式,^ 和 $ 匹配每行
text = """line 1
line 2
line 3"""
re.findall(r"^line \d", text, re.M)
# ['line 1', 'line 2', 'line 3']
# re.DOTALL (re.S) — . 也匹配换行符
re.search(r"start.*end", "start\nmiddle\nend", re.S).group()
# 'start\nmiddle\nend'
# re.VERBOSE (re.X) — 允许注释和空白,提高可读性
pattern = re.compile(r"""
(\d{3}) # 区号
[-.\s]? # 可选分隔符
(\d{4}) # 前四位
[-.\s]? # 可选分隔符
(\d{4}) # 后四位
""", re.VERBOSE)
m = pattern.search("13812345678")
print(m.groups()) # ('138', '1234', '5678')
# 组合多个 flag
re.findall(r"^hello.*world$", text, re.I | re.M | re.S)
23.4 分组与命名分组
基本分组
import re
# () 分组
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "日期: 2026-03-31")
print(m.group(0)) # "2026-03-31" — 整个匹配
print(m.group(1)) # "2026" — 第 1 组
print(m.group(2)) # "03" — 第 2 组
print(m.group(3)) # "31" — 第 3 组
print(m.groups()) # ('2026', '03', '31')
# findall 有分组时返回分组内容
dates = re.findall(r"(\d{4})-(\d{2})-(\d{2})", "2026-03-31 and 2025-12-25")
print(dates) # [('2026', '03', '31'), ('2025', '12', '25')]
命名分组
# (?P<name>...) 命名分组
m = re.search(
r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})",
"日期: 2026-03-31"
)
print(m.group("year")) # "2026"
print(m.group("month")) # "03"
print(m.group("day")) # "31"
print(m.groupdict()) # {'year': '2026', 'month': '03', 'day': '31'}
非捕获分组
# (?:...) 非捕获分组 — 只分组不捕获
m = re.search(r"(?:https?://)?(\w+\.\w+)", "https://example.com")
print(m.group(1)) # "example.com" — 协议部分不被捕获
反向引用
# \1 引用第一个分组
# 匹配重复单词
re.search(r"\b(\w+)\s+\1\b", "the the quick brown fox")
# 匹配到 "the the"
# 在替换中使用反向引用
re.sub(r"(\w+)\s+\1", r"\1", "the the quick brown fox fox")
# "the quick brown fox"
# 命名反向引用
re.sub(r"(?P<word>\w+)\s+(?P=word)", r"\g<word>", "the the")
23.5 贪婪 vs 非贪婪匹配
默认情况下,量词 *、+、?、{n,m} 是贪婪的——尽可能多匹配:
import re
html = "<b>bold</b> and <i>italic</i>"
# 贪婪(默认)— 匹配尽可能多
m = re.search(r"<.*>", html)
print(m.group()) # "<b>bold</b> and <i>italic</i>" — 匹配了整个字符串!
# 非贪婪(加 ?)— 匹配尽可能少
m = re.search(r"<.*?>", html)
print(m.group()) # "<b>" — 只匹配第一个标签
# 所有标签
tags = re.findall(r"<.*?>", html)
print(tags) # ['<b>', '</b>', '<i>', '</i>']
# 各量词的非贪婪版本
# *? — 0 次或多次(非贪婪)
# +? — 1 次或多次(非贪婪)
# ?? — 0 次或 1 次(非贪婪)
# {n,m}? — n 到 m 次(非贪婪)
23.6 零宽断言(前瞻与后顾)
零宽断言不消耗字符,只判断位置:
import re
# (?=...) 正向前瞻(lookahead)— 后面必须跟着...
re.findall(r"\w+(?=@)", "alice@example.com bob@test.com")
# ['alice', 'bob'] — 匹配 @ 前面的单词
# (?!...) 负向前瞻 — 后面不能跟着...
re.findall(r"\d+(?!%)", "50% off, only $30, save 20%")
# ['5', '30', '2'] — 排除百分号前的完整数字需要更精确的模式
# (?<=...) 正向后顾(lookbehind)— 前面必须是...
re.findall(r"(?<=\$)\d+", "Price: $100, discount $20")
# ['100', '20'] — 匹配 $ 后面的数字
# (?<!...) 负向后顾 — 前面不能是...
re.findall(r"(?<!\$)\b\d+\b", "100 items at $50 each")
# ['100'] — 排除 $ 后面的数字
实用例子:
# 密码强度检查(同时满足多个条件)
def check_password(password):
patterns = [
r"(?=.*[a-z])", # 至少一个小写字母
r"(?=.*[A-Z])", # 至少一个大写字母
r"(?=.*\d)", # 至少一个数字
r"(?=.*[!@#$%^&*])", # 至少一个特殊字符
r".{8,}", # 至少 8 个字符
]
return all(re.search(p, password) for p in patterns)
print(check_password("Abc12345!")) # True
print(check_password("abc12345")) # False
# 千分位分隔
def add_commas(n):
return re.sub(r"(?<=\d)(?=(\d{3})+$)", ",", str(n))
print(add_commas(1234567890)) # "1,234,567,890"
23.7 常见实战模式
邮箱
email_pattern = re.compile(r"""
[a-zA-Z0-9._%+-]+ # 用户名
@ # @
[a-zA-Z0-9.-]+ # 域名
\.[a-zA-Z]{2,} # 顶级域名
""", re.VERBOSE)
text = "联系 alice@example.com 或 bob.smith@company.co.uk"
print(email_pattern.findall(text))
# ['alice@example.com', 'bob.smith@company.co.uk']
URL
url_pattern = re.compile(r"""
https?:// # 协议
(?:www\.)? # 可选 www
[a-zA-Z0-9.-]+ # 域名
\.[a-zA-Z]{2,} # 顶级域名
(?:/[^\s]*)? # 可选路径
""", re.VERBOSE)
text = "访问 https://www.example.com/path?q=1 或 http://test.org"
print(url_pattern.findall(text))
日期提取与转换
date_pattern = re.compile(r"""
(?P<year>\d{4})
[-/.]
(?P<month>\d{1,2})
[-/.]
(?P<day>\d{1,2})
""", re.VERBOSE)
text = "事件发生在 2026-03-31 和 2025/12/25"
for m in date_pattern.finditer(text):
d = m.groupdict()
print(f"{d['year']}年{d['month']}月{d['day']}日")
# 2026年03月31日
# 2025年12月25日
清理 HTML 标签
def strip_html(html):
return re.sub(r"<[^>]+>", "", html)
html = "<p>Hello <b>World</b>!</p>"
print(strip_html(html)) # "Hello World!"
IP 地址
ip_pattern = re.compile(r"""
\b
(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3} # 前三段
(?:25[0-5]|2[0-4]\d|[01]?\d\d?) # 最后一段
\b
""", re.VERBOSE)
text = "服务器 IP: 192.168.1.100,网关: 10.0.0.1,无效: 999.999.999.999"
print(ip_pattern.findall(text))
# ['192.168.1.100', '10.0.0.1']
中文匹配
# 匹配中文字符
chinese = re.findall(r"[\u4e00-\u9fff]+", "Hello 你好世界 Python 编程")
print(chinese) # ['你好世界', '编程']
# 匹配中文姓名(2-4个汉字)
names = re.findall(r"[\u4e00-\u9fff]{2,4}", "张三和李四是好朋友,王五也是")
print(names) # ['张三和李', '四是好朋', '王五也是'] — 需要更精确的模式
本章小结:正则表达式是文本处理的利器。核心技能:掌握基本语法和预定义字符类、理解贪婪与非贪婪、善用分组和命名分组、灵活运用零宽断言。编写复杂正则时推荐用
re.VERBOSE添加注释,提高可维护性。记住:如果正则过于复杂,考虑用 Python 代码分步处理可能更好。