第1章 正则表达式基础:元字符、字符类、量词

正则表达式,说白了就是一种描述文本模式的工具。你想想看,当你要在一堆日志里找出所有手机号、身份证或者银行卡号时,挨个看肯定不现实。这时候正则就派上用场了。我记得刚开始做数据清洗时,面对几万行杂乱文本,正则就像一把瑞士军刀——简单几行模式,就能把目标数据精准地拎出来。

正则表达式基础知识体系 正则表达式基础知识体系 正则表达式基础 元字符 字符类 量词 . ^ $ 锚点定位 ( ) | 分组选择 \ 转义字符 [...] 字符集合 [^...] 否定 \d \w \s 预定义 * + ? 基础量词 {n} {n,} {n,m} 贪婪 vs 非贪婪 掌握这三要素,就能解决 90% 的敏感数据匹配问题

1.1 正则表达式到底是什么

正则表达式,简称为"正则"或"regex",是一种用来描述字符串模式的"小语言"。说白了,你给它一个规则,它就能在一段文本里找到所有符合规则的部分。举个例子:你要从几百条日志里捞出所有 IP 地址,手写循环又慢又容易漏,用正则几行代码就搞定。

我一开始也觉得正则难记——全是符号,看着像乱码。但后来我发现,核心就三块东西:元字符、字符类、量词。把这三块搞明白,大部分场景你就能自己写出来了。

核心认知:正则的底层逻辑是"匹配位置"和"匹配字符"的组合。元字符控制位置和结构,字符类限定字符范围,量词控制出现次数。三者组合,就能精确描述几乎任何文本模式。

1.2 元字符——正则的"基本字母"

元字符是正则里最基础的符号,它们有特殊含义,不是普通字符。我在项目中见过不少新手一上来就写 \. 结果忘了转义,匹配了半天全是任意字符……

元字符 含义 示例 匹配结果
. 匹配任意单个字符(除换行) a.b "acb"、"a b"、"a1b"
^ 匹配字符串开头 ^Hello 以 "Hello" 开头的行
$ 匹配字符串结尾 end$ 以 "end" 结尾的行
| 逻辑"或" cat|dog "cat" 或 "dog"
\ 转义后面字符的特殊含义 \. 匹配字面量 "."
() 分组,提取子匹配 (ab)+ "ab"、"abab"

小技巧:我习惯在写正则时先把所有元字符用 \ 转义,再去掉不需要转义的。这样不会漏掉。尤其是在匹配文件路径或 URL 时,点号和斜杠特别容易踩坑。

拿个实际场景来说。我在解析服务器日志时,想匹配以 "ERROR" 开头、包含时间戳的行。用 ^ERROR \d{4}-\d{2}-\d{2} 一下子就定位到了。这里的 ^ 锚定了行首,\d 是预定义字符类(后面会细讲),{4} 是量词。你看,三个要素已经凑齐了两个。

1.3 字符类——匹配一组字符

字符类用方括号 [] 表示。说白了就是"匹配这个集合里的任意一个字符"。举个例子:[aeiou] 匹配任意一个英文元音字母。

  • 普通集合:[abc] 匹配 "a"、"b" 或 "c"
  • 范围表示:[a-z] 匹配所有小写字母,[0-9] 匹配所有数字
  • 否定:[^abc] 匹配除了 "a"、"b"、"c" 以外的任意字符(注意 ^ 在方括号里表示取反)
  • 预定义字符类:
    • \d 等价于 [0-9]
    • \w 等价于 [a-zA-Z0-9_]
    • \s 等价于空白字符(空格、制表符、换行)
import re

# 用字符类匹配手机号(简单版)
pattern = r'1[3-9]\d{9}'
text = "我的号码是13812345678,他的是15987654321。"
phones = re.findall(pattern, text)
print(phones)  # ['13812345678', '15987654321']

# 用否定字符类过滤非数字字符
pattern2 = r'[^\d]'
text2 = "订单号:AB-2024-001"
clean = re.sub(pattern2, '', text2)
print(clean)  # '2024001'

注意:我曾经在匹配中文姓名时用了 [a-zA-Z],结果把所有带连字符 "Montgomery-Smith" 这种格式都拆开了。字符类的范围一定要根据实际数据来定。在敏感数据识别中,宁可多写几个分支,也不要一刀切。

1.4 量词——控制重复次数

量词决定了前面的字符或字符类"出现多少次"。这是正则里最容易出性能问题的地方——我见过有人用 .* 一把梭,结果把整个文本都吞了。

量词 含义 示例 匹配
* 零次或多次 ab* "a"、"ab"、"abb"、"abbb"...
+ 一次或多次 ab+ "ab"、"abb"、"abbb"...(不含 "a")
? 零次或一次 ab? "a" 或 "ab"
{n} 精确匹配 n 次 \d{4} 四位数字,如 "2024"
{n,} 至少 n 次 \d{2,} 两位及以上数字
{n,m} n 到 m 次 \d{2,4} "12"、"123"、"1234"

贪婪 vs 非贪婪:默认情况下,量词是"贪婪"的——它会尽量匹配更多的字符。比如 a.*b 去匹配 "a---b---b",它会吞掉整个字符串一直到最后一个 "b"。如果在量词后面加个 ?,就变成"非贪婪"模式,匹配到第一个 "b" 就停。

我建议在敏感数据识别中,尽量用非贪婪匹配。为什么呢?因为敏感数据(如身份证号、银行卡号)长度通常是固定的,用精确量词 {n} 最安全,既不会漏也不会多吞。

import re

# 贪婪 vs 非贪婪 对比
text = "<div>内容1</div><div>内容2</div>"

# 贪婪匹配:吞掉整个字符串直到最后一个 </div>
greedy = re.search(r'<div>.*</div>', text)
print(greedy.group())  # <div>内容1</div><div>内容2</div>

# 非贪婪匹配:遇到第一个 </div> 就停
non_greedy = re.search(r'<div>.*?</div>', text)
print(non_greedy.group())  # <div>内容1</div>

# 量词精确匹配:银行卡号(16-19位数字)
bank_pattern = r'\b\d{16,19}\b'
text2 = "卡号:6222021234567890,请勿泄露。"
print(re.findall(bank_pattern, text2))  # ['6222021234567890']

实战要点:量词的选用直接影响匹配精度和性能。对于敏感数据识别,我总结了一个优先级:精确量词 {n} > 范围量词 {n,m} > + > *。能用定长的绝不用变长,能加边界 \b 的绝不含糊。

1.5 本章小结

到这里,正则的三块基石——元字符、字符类、量词——我们就过了一遍。别看它们单个都很简单,组合起来就能表达非常复杂的文本模式。记得我刚学那会儿,觉得 ^[A-Za-z]+$ 像咒语,后来看懂了,其实就是"从开头到结尾全是字母"的意思。嗯,熟练了就好。

在下一章到来之前,我建议你打开 Python 终端,把今天这几个例子敲一遍。尤其是 re.findall 和 re.sub,这两个函数在敏感数据识别里会用得非常频繁。自己动手跑一遍,比看十遍都管用。

练习建议:找一份你手边的文本(比如 CSV 导出文件、日志片段),试着写三个正则:1. 匹配所有数字;2. 匹配所有手机号(11位数字,以1开头);3. 匹配所有邮箱地址。用 re.findall 验证结果。这三步走完,你就真正上手了。