正则表达式 re 模块实战

正则表达式(regex)是文本处理的瑞士军刀。你能用它一行代码解决”从日志里提取所有 IP”、”验证邮箱格式”、”把驼峰命名改成蛇形”、”从 HTML 里抽出所有链接”(虽然 HTML 更该用 BeautifulSoup)。Python 的正则由 re 模块提供。这一篇不做完整语法参考(那要一整本书),而是把日常最常用的模式和 API 讲清楚。

一、re 模块的四个核心函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re

# 1. 匹配:在字符串开头找
re.match(r"\d+", "123abc") # <Match object>
re.match(r"\d+", "abc123") # None,不是开头

# 2. 搜索:在字符串任意位置找
re.search(r"\d+", "abc123def") # <Match object>

# 3. 找全部
re.findall(r"\d+", "a1b22c333") # ['1', '22', '333']

# 4. 替换
re.sub(r"\d+", "N", "a1b22c333") # 'aNbNcN'
  • match:从头匹配;
  • search:任意位置匹配(一次);
  • findall:找全部;
  • sub:替换。

再加两个:

1
2
3
4
5
6
# 拆分
re.split(r"\s+", "hello world python") # ['hello', 'world', 'python']

# 迭代匹配
for m in re.finditer(r"\d+", "a1b22c333"):
print(m.start(), m.group())

二、正则的基础语法

字符类

  • . 任意字符(除换行)
  • \d 数字,等于 [0-9]
  • \D 非数字
  • \w 字母/数字/下划线,等于 [a-zA-Z0-9_](Unicode 模式下也含中文)
  • \W 非上述
  • \s 空白(空格、tab、换行)
  • \S 非空白
  • [abc] a 或 b 或 c
  • [^abc] 非 a b c
  • [a-z] a 到 z

量词

  • * 0 次或多次
  • + 1 次或多次
  • ? 0 或 1 次
  • {n} 恰好 n 次
  • {n,} 至少 n 次
  • {n,m} n 到 m 次

贪婪 vs 非贪婪:默认贪婪(尽量多匹配),加 ? 变非贪婪(尽量少)。

1
2
re.findall(r"<.+>", "<a><b>")     # ['<a><b>']   贪婪
re.findall(r"<.+?>", "<a><b>") # ['<a>', '<b>'] 非贪婪

锚点

  • ^ 字符串开头
  • $ 字符串结尾
  • \b 单词边界

分组与捕获

  • (...) 捕获组
  • (?:...) 非捕获组(只分组不捕获)
  • (?P<name>...) 命名组
1
2
3
4
5
6
7
8
9
10
m = re.match(r"(\d{4})-(\d{2})-(\d{2})", "2025-09-25")
m.group() # '2025-09-25'(整体)
m.group(1) # '2025'
m.group(2) # '09'
m.groups() # ('2025', '09', '25')

# 命名组
m = re.match(r"(?P<year>\d{4})-(?P<mon>\d{2})-(?P<day>\d{2})", "2025-09-25")
m.group("year") # '2025'
m.groupdict() # {'year': '2025', 'mon': '09', 'day': '25'}

  • a|b 匹配 a 或 b

三、raw string 与转义

永远用 r"..."(raw string)写正则。否则 \d 里的 \d 会被 Python 解释成”转义”,需要写 "\\d"

1
2
re.findall("\\d+", s)     # 能用,但难读
re.findall(r"\d+", s) # ✅ 推荐

四、编译正则

如果一个正则要用很多次,先 编译 一下:

1
2
3
pattern = re.compile(r"\d+")
pattern.findall("a1b22")
pattern.sub("N", "a1b22")

好处:

  • 只解析一次;
  • 少写一个参数;
  • 有个”专属对象”,可读性高。

五、常用模式实战

1. 验证格式

1
2
3
4
5
6
7
8
9
10
11
# 邮箱(简化版)
re.fullmatch(r"[\w.+-]+@[\w-]+\.[\w.-]+", email)

# 手机号(中国大陆 11 位)
re.fullmatch(r"1[3-9]\d{9}", phone)

# 身份证(18 位)
re.fullmatch(r"\d{17}[\dX]", id_card)

# IPv4
re.fullmatch(r"(\d{1,3}\.){3}\d{1,3}", ip)

fullmatch vs match:fullmatch 要求整个字符串匹配(避免”1234567890abc”被 match 通过)。

2. 抽取字段

1
2
3
4
5
# 从日志里抽 IP + 状态码 + 路径
log = '192.168.1.1 - - [2025-09-25:10:11:12] "GET /login HTTP/1.1" 200'
m = re.search(r'(\S+) .+? "(\S+) (\S+) .+?" (\d+)', log)
print(m.groups())
# ('192.168.1.1', 'GET', '/login', '200')

3. 拆分

1
2
3
# 拆分句子(多种标点)
re.split(r"[,.!?、;。!?]\s*", "hi, 咖飞. how? 学 Python!")
# ['hi', '咖飞', 'how', '学 Python', '']

4. 替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 简单替换
re.sub(r"\d+", "N", "abc 123 def 456") # 'abc N def N'

# 引用捕获组:\1 或 \g<name>
re.sub(r"(\w+)@\w+", r"\1@匿名", "user@example.com")
# 'user@匿名'

# 用函数做动态替换
def to_upper(m):
return m.group().upper()

re.sub(r"[a-z]+", to_upper, "hi 咖飞 abc") # 'HI 咖飞 ABC'

# 只替换前 N 次
re.sub(r"\d", "N", "1234", count=2) # 'NN34'

5. 命名组 + sub

1
2
3
# 交换年月
re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})", r"\g<m>/\g<y>", "2025-09")
# '09/2025'

六、re 的 flags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
re.IGNORECASE / re.I     # 忽略大小写
re.MULTILINE / re.M # ^ 和 $ 匹配每行的开头结尾
re.DOTALL / re.S # . 匹配换行
re.VERBOSE / re.X # 允许写多行 + 注释

# 多个 flag 用 | 组合
re.findall(r"hello", "Hello", re.I) # ['Hello']

# 写多行正则
pattern = re.compile(r"""
(\d{4}) # 年
-
(\d{2}) # 月
-
(\d{2}) # 日
""", re.VERBOSE)

VERBOSE 特别适合复杂正则,别忘了实际字符要转义或用字符类包起来(否则空格会被忽略)。

七、常见误区

1. 贪婪陷阱

1
2
re.findall(r"<.+>", "<h1>hello</h1>")   # ['<h1>hello</h1>']  整个吃掉
re.findall(r"<.+?>", "<h1>hello</h1>") # ['<h1>', '</h1>'] 非贪婪

2. \d 只匹配 ASCII 数字

Python 的 \d 默认也匹配 Unicode 数字(如中文数字”零”),如果只想 ASCII:

1
2
3
re.findall(r"\d", "1 二 3")            # ['1', '3']
re.findall(r"\d", "1 二 3", re.ASCII) # ['1', '3']
re.findall(r"[0-9]", "1 二 3") # ['1', '3'] 更明确

3. 中文匹配

Unicode 中文范围:[一-龥](常用汉字):

1
2
re.findall(r"[一-龥]+", "hi 咖飞 abc 学 Python")
# ['咖飞', '学']

4. HTML 不该用正则

HTML 嵌套结构复杂,用正则容易翻车。用 BeautifulSoup / lxml。

八、re 的性能

  • 编译一次的正则比字符串正则快(避免重复解析);
  • 大字符串上跑贪婪匹配可能会指数级慢(catastrophic backtracking);
  • Python 的 re 是回溯引擎;性能极端场景考虑 regex 第三方库(支持 possessive quantifier)。

注意re 内部有 LRU 缓存,即使不 compile 也会缓存约 100 条,但显式 compile 更明确。

九、Walrus + regex:简洁写法

1
2
3
4
if m := re.match(r"(\d+)-(\d+)", s):
print(m.group(1), m.group(2))
else:
print("不匹配")

海象运算符让”判断 + 取值”合并,比传统写法短。

十、几个日常必备正则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 去除多余空白
re.sub(r"\s+", " ", text).strip()

# 只保留字母数字
re.sub(r"[^a-zA-Z0-9]", "", text)

# 只保留中文
re.sub(r"[^一-龥]", "", text)

# 驼峰转下划线
re.sub(r"([A-Z])", r"_\1", "UserProfileID").lower().lstrip("_")
# 'user_profile_i_d' 最后一个 D 也被分开,业务上一般够用

# 更精细的驼峰
re.sub(r"(?<!^)(?=[A-Z])", "_", "UserProfileID").lower()
# 'user_profile_i_d'

十一、一个完整例子:日志敏感信息脱敏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# masker.py
"""对日志行做敏感信息脱敏。"""

import re

PATTERNS = [
# 手机号
(re.compile(r"1[3-9]\d{9}"),
lambda m: m.group()[:3] + "****" + m.group()[-4:]),

# 邮箱:xxx@yyy → x***@yyy
(re.compile(r"([\w.+-])[\w.+-]*(@[\w.-]+)"),
r"\1***\2"),

# 身份证 18 位
(re.compile(r"\d{6}(\d{8})\d{3}[\dX]"),
lambda m: m.group()[:6] + "********" + m.group()[-4:]),

# 银行卡(简化:连续 16-19 位数字)
(re.compile(r"\b\d{16,19}\b"),
lambda m: m.group()[:4] + " **** **** " + m.group()[-4:]),
]


def mask(line: str) -> str:
for pattern, repl in PATTERNS:
line = pattern.sub(repl, line)
return line


if __name__ == "__main__":
tests = [
"user phone 13812345678 login",
"email kafei.gk@example.com",
"id: 410102199001011234",
"card 6222 0212 3456 7890",
]
for t in tests:
print(mask(t))

十二、常见陷阱

  1. 忘 raw string"\d" 会有告警,写 r"\d"
  2. matchsearch 混淆:match 只匹配开头。
  3. findall 有捕获组时返回不同
    • 无捕获组:返回整体匹配字符串列表;
    • 一个捕获组:返回捕获内容列表;
    • 多个捕获组:返回元组列表。
    1
    2
    3
    re.findall(r"\d+", "a1b2")            # ['1', '2']
    re.findall(r"(\d+)", "a1b2") # ['1', '2']
    re.findall(r"(\d+)(\w)", "a1b2c") # [('1', 'b'), ('2', 'c')]
  4. 贪婪吃太多:加 ? 变非贪婪。
  5. catastrophic backtracking:类似 (a+)+ 遇到长字符串会几秒钟卡死。
  6. 在替换里用 \1 但字符串没加 r"\1" 会被解释成 \x01。用 raw string。

十三、小结与延伸阅读

  • 五大 API:matchsearchfindallsubsplit
  • 永远用 r"..."
  • 分组用 (...),命名组 (?P<n>...)
  • flags:IMSX
  • 贪婪 +、非贪婪 +?
  • HTML 别用正则,用专门的解析库;
  • 复杂正则用 re.compile + VERBOSE
  • 海象运算符 + re.match 是简洁的匹配-取值合体。

延伸阅读:

下一篇 日期时间 datetime 模块 我们讲怎么优雅地处理时间。