首页 / 知识库 / 0基础入门-阅读资料 / 0基础-python入门到精通

第 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 代码分步处理可能更好。