前言:正则表达式的价值与应用场景
正则表达式(Regular Expression,简称 Regex)是一种用于描述字符串模式的语法规则,本质是 "匹配字符串的数学公式",广泛应用于:
- 文本处理:数据提取、格式校验、内容替换、字符串分割;
- 开发场景:日志分析、代码语法检查、用户输入验证(手机号、邮箱、密码);
- 运维 / 爬虫:日志过滤、网页数据抓取、配置文件解析;
- 办公自动化:Excel 数据清洗、Word 文档批量替换。
只要涉及文本的匹配、提取、验证、替换,正则表达式就能用极简的语法,替代数十行甚至上百行的原生代码逻辑 。
一:正则表达式基础
1.1 正则表达式的本质与核心思想
正则表达式的核心是 "模式匹配"------ 用预先定义的 "规则字符串" 去匹配目标字符串中的内容。例如:
- 规则 \d{11} 可匹配 11 位数字(手机号);
- 规则 [a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-z]{2,4} 可匹配邮箱格式。
核心思想:将复杂的字符串规则抽象为简洁的正则语法,通过 "元字符""量词""边界" 等组合,实现精准匹配。
1.2 正则表达式的两种定义方式(多语言对比)
所有语言的正则都支持两种定义方式,核心区别在于是否支持动态拼接:
|--------|-------------------------------------------------------|---------------|------------------------|
| 定义方式 | 语法格式(以匹配数字为例) | 适用场景 | 主流语言支持情况 |
| 字面量写法 | Python: r'\d' / JS: /\d/ | 规则固定不变时(推荐) | Python、JS、Perl、PHP 等支持 |
| 构造函数写法 | Python: re.compile('\\d') / JS: new RegExp('\\d') | 规则需动态拼接(如含变量) | 所有语言均支持 |
注意:Python 中字符串的 \ 是转义符,因此正则中的 \d 需写成 \\d,或用原始字符串 r'\d'(推荐,避免双转义)。
1.3 基础元字符:匹配单个字符
元字符是正则的 "基本 building block",用于匹配单个字符或字符类别,以下是所有基础元字符的详细说明:
|------------------|--------------------------------------|---------------------------|--------------------------|
| 元字符 | 作用描述 | 示例 | 匹配结果 |
| 普通字符(a-z、0-9、等) | 匹配字符本身 | abc 匹配 abc123 | abc |
| . | 匹配任意单个字符(默认不匹配换行符 \n) | a.c 匹配 abc a1c a#c | abc、a1c、a#c |
| \d | 匹配任意数字(等价于 [0-9]) | \d 匹配 a1b2c3 | 1、2、3 |
| \D | 匹配非数字(等价于 [^0-9]) | \D 匹配 1a2b3c | a、b、c |
| \w | 匹配单词字符(字母、数字、下划线,等价于 [a-zA-Z0-9]) | \w 匹配 a_1#b | a、、1、b |
| \W | 匹配非单词字符(等价于 [^a-zA-Z0-9]) | \W 匹配 a_1#b | #、 |
| \s | 匹配空白符(空格、制表符 \t、换行符 \n、回车符 \r) | a\sb 匹配 a b a\tb | a b、a\tb |
| \S | 匹配非空白符 | a\Sb 匹配 a1b a#b | a1b、a#b |
| \b | 单词边界(匹配单词开头 / 结尾,不占字符位置) | \bcat\b 匹配 cat a cat b | cat(不匹配 category 中的 cat) |
| \B | 非单词边界 | \Bcat\B 匹配 category | cat(仅匹配单词中间的 cat) |
| ^ | 字符串开头(多行模式下匹配行首) | ^abc 匹配 abc123 abc\n456 | abc(第一行开头) |
| | 字符串结尾(多行模式下匹配行尾) | 123 匹配 abc123 456\n123 | 123(最后一行结尾) |
| \ | 转义符(将特殊字符转为普通字符) | \. 匹配 a.b | .(避免 . 被当作元字符) |
示例 1:基础元字符实战(Python)
python
import re
str1 = "Hello_World 123! 456\n789"
# 匹配所有单词字符
print(re.findall(r'\w', str1)) # ['H','e','l','l','o','_','W','o','r','l','d','1','2','3','4','5','6','7','8','9']
# 匹配所有空白符
print(re.findall(r'\s', str1)) # [' ', ' ', '\n']
# 匹配字符串开头的Hello
print(re.findall(r'^Hello', str1)) # ['Hello']
# 匹配字符串结尾的789
print(re.findall(r'789$', str1)) # ['789']
# 匹配点号(需转义)
str2 = "a.b.c"
print(re.findall(r'\.', str2)) # ['.', '.']
1.4 字符集:匹配多个候选字符中的一个
字符集用 [] 表示,用于匹配 "集合内任意一个字符",支持范围表示和排除逻辑:
1.4.1 基础字符集
语法:[字符1字符2...] 或 [字符范围]
示例:
-
abc\]:匹配 a、b、c 中的任意一个;
-
A-Z\]:匹配大写字母;
-
a-zA-Z0-9\]:匹配字母和数字(等价于 \\w 去掉下划线)。
语法:[^字符1字符2...](^ 在 [] 内表示 "排除")
示例:
-
\^abc\]:匹配除 a、b、c 之外的任意字符;
1.4.3 字符集的特殊规则
-
\] 内的元字符会失效,无需转义(除 \^、-、\] 外):
-
-\] 或 \[a-z-\] 匹配减号(将 - 放在开头 / 结尾或转义 \\-)。
示例 2:字符集实战(Python)
python
import re
str = "Google Runoob Taobao 123! 456"
# 匹配所有元音字母(a/e/i/o/u,忽略大小写)
patt1 = r'[aeiouAEIOU]'
print(re.findall(patt1, str)) # ['o','o','e','u','o','o','a','o','a','o']
# 匹配所有非字母字符
patt2 = r'[^a-zA-Z]'
print(re.findall(patt2, str)) # [' ',' ',' ',' ','1','2','3','!',' ','4','5','6']
# 匹配数字或下划线
patt3 = r'[0-9_]'
print(re.findall(patt3, str)) # ['1','2','3','4','5','6']
1.5 量词:匹配字符出现的次数
量词用于指定 "前面的元素(字符、字符集、分组)出现的次数",是正则简化重复规则的核心,以下是所有量词的详细说明:
|--------|--------------------------|----------|-----------------------|-------------------------|
| 量词 | 作用描述 | 贪婪 / 非贪婪 | 示例 | 匹配结果 |
| * | 匹配前面的元素 0 次或多次(无限次) | 贪婪 | ab* 匹配 a ab abb abbb | a、ab、abb、abbb |
| + | 匹配前面的元素 1 次或多次(至少 1 次) | 贪婪 | ab+ 匹配 ab abb abbb | ab、abb、abbb |
| ? | 匹配前面的元素 0 次或 1 次(可选) | 贪婪 | ab? 匹配 a ab | a、ab |
| {n} | 匹配前面的元素恰好 n 次 | 固定 | a{3} 匹配 aaa aaaa | aaa(aaaa 中匹配前 3 个 a) |
| {n,} | 匹配前面的元素至少 n 次(n 到无限次) | 贪婪 | a{2,} 匹配 aa aaa aaaa | aa、aaa、aaaa |
| {n,m} | 匹配前面的元素 n 到 m 次(含 n 和 m) | 贪婪 | a{2,3} 匹配 aa aaa aaaa | aa、aaa(aaaa 中匹配前 3 个) |
| *? | 0 次或多次(非贪婪,尽可能少匹配) | 非贪婪 | ab*? 匹配 a ab abb | a、ab、ab(abb 中仅匹配 1 个 b) |
| +? | 1 次或多次(非贪婪) | 非贪婪 | ab+? 匹配 ab abb | ab、ab(abb 中仅匹配 1 个 b) |
| ?? | 0 次或 1 次(非贪婪) | 非贪婪 | ab?? 匹配 a ab | a、a(ab 中不匹配 b) |
| {n,m}? | n 到 m 次(非贪婪) | 非贪婪 | a{2,3}? 匹配 aaa aaaa | aa、aa(aaaa 中匹配前 2 个) |
关键概念:贪婪匹配 vs 非贪婪匹配
- 贪婪匹配(默认):量词会尽可能多地匹配字符,直到无法匹配为止;
- 非贪婪匹配:在量词后加 ?,量词会尽可能少地匹配字符,满足条件即可。
示例 3:贪婪与非贪婪对比(Python)
python
import re
str1 = "aaaaa"
# 贪婪匹配:{2,3} 尽可能匹配3个
print(re.findall(r'a{2,3}', str1)) # ['aaa', 'aa'](3个+2个)
# 非贪婪匹配:{2,3}? 尽可能匹配2个
print(re.findall(r'a{2,3}?', str1)) # ['aa', 'aa', 'a'](2个+2个+1个不满足,舍弃)
str2 = "<div>内容1</div></div>"
# 贪婪匹配:.* 会匹配到最后一个 >
print(re.findall(r'<div>.*</div>', str2)) # ['1</div>内容2(整体匹配)
# 非贪婪匹配:.*? 匹配到第一个 </div>
print(re.findall(r'? # ['内容1</div>', '<div>内容2>'](分开匹配)
1.6 分组与选择:复杂模式的组合
1.6.1 分组(()):将多个元素视为一个整体
作用:1. 把多个字符 / 元字符组合成一个 "单元",配合量词使用;2. 捕获匹配结果(后续可引用)。
示例:
- (ab)+:匹配 ab、abab、ababab 等(ab 作为整体重复);
- (a|b)c:匹配 ac 或 bc(| 表示 "或",配合分组使用)。
1.6.2 选择(|):匹配多个模式中的一个
- 语法:模式1|模式2|模式3(匹配任意一个模式);
- 注意:| 的优先级最低,需用分组明确范围,否则会匹配 "模式 1" 或 "模式 2 + 后续内容"。
反例:ab|cd 匹配 ab 或 cd(正确);a|bc 匹配 a 或 bc(而非 ab 或 bc)。
1.6.3 非捕获分组((?:)):仅分组,不捕获结果
- 作用:当只需将元素组合成单元,无需捕获匹配内容时使用,可提升性能;
- 区别于普通分组:普通分组 () 会将匹配结果存入内存,非捕获分组 (?:) 不存储。
示例 4:分组与选择实战(Python)
python
import re
str = "abab 123 456 abcd"
# 1. 普通分组:匹配 (ab) 重复1次以上
patt1 = re.compile("(ab)+")
m1 = patt1.finditer(str)
for match in m1:
print("普通分组匹配:" + match.group()) # 输出:abab
# 2. 选择匹配:匹配 123 或 456
patt2 = re.compile("123|456")
m2 = patt2.finditer(str)
for match in m2:
print("选择匹配:" + match.group()) # 输出:123、456
# 3. 非捕获分组:匹配 (ab) 或 (cd),但不捕获分组内容
patt3 = re.compile("(?:ab)|(?:cd)")
m3 = patt3.finditer(str)
for match in m3:
print("非捕获分组匹配:" + match.group()) # 输出:ab、ab、cd
1.7 修饰符(标志):调整匹配规则
修饰符用于修改正则的匹配行为,不同语言的表示方式不同,但功能一致,以下是常用修饰符的对比:
|-----|-------------|----------------------------------|--------------------|---------------------|--------------------------|
| 修饰符 | 全称 | 功能描述 | Python 写法 | JavaScript 写法 | Java 写法 |
| g | Global | 全局匹配,匹配所有符合规则的结果(而非仅第一个) | 无(通过 findall() 实现) | /pattern/g | 无(通过循环 find() 实现) |
| i | Ignore Case | 忽略大小写匹配 | re.I/re.IGNORECASE | /pattern/i | Pattern.CASE_INSENSITIVE |
| m | Multiline | 多行模式:^ 匹配行首,$ 匹配行尾(默认匹配整个字符串首尾) | re.M/re.MULTILINE | /pattern/m | Pattern.MULTILINE |
| s | Dotall | 点号模式:让 . 匹配所有字符(包括换行符 \n,默认不匹配) | re.S/re.DOTALL | /pattern/s(ES2018+) | Pattern.DOTALL |
| x | Verbose | 注释模式:允许正则中写注释(# 后面的内容为注释),提升可读性 | re.X/re.VERBOSE | 不支持 | Pattern.COMMENTS |
示例 5:修饰符实战(Python)
python
# Python:re.I(忽略大小写)+ re.M(多行模式)
import re
str1 = "Hello\nhello\nHELLO"
# 忽略大小写,匹配所有 hello(无论大小写)
patt1 = re.compile(r'hello', re.I)
print(patt1.findall(str1)) # ['Hello', 'hello', 'HELLO']
# 多行模式:匹配每一行开头的 H/h
patt2 = re.compile(r'^h', re.I | re.M)
print(patt2.findall(str1)) # ['H', 'h', 'H']
# 点号匹配换行:匹配从 Hello 到 HELLO 的所有内容(re.S 修饰符)
patt3 = re.compile(r'Hello.*HELLO', re.S)
match = patt3.search(str1)
print(match.group()) # 输出整个字符串(包括换行)
二:正则表达式进阶
2.1 反向引用:复用之前的分组匹配结果
反向引用用于 "引用正则中已捕获的分组内容",语法为 \n(n 是分组编号,从 1 开始),核心应用场景:匹配重复出现的内容。
2.1.1 基础反向引用(数字引用)
语法:(模式)\1(\1 引用第一个分组的匹配结果);
示例:
- (ab)\1:匹配 abab(第一个分组匹配 ab,\1 复用 ab);
- (\d)\1:匹配 22、33 等重复数字;
- ([a-zA-Z])(\d)\2\1:匹配 a22a、b55b 等(字母 + 数字 + 重复数字 + 重复字母)。
2.1.2 命名分组与反向引用
数字引用在分组较多时容易混淆,命名分组允许给分组起名字,语法为 (?P>模式)(Python),反向引用为 (?P=name)(Python)。
示例 6:反向引用实战(Python)
python
# Python:数字反向引用 + 命名分组反向引用
import re
# 1. 匹配重复的两位数字(如 22、33、55)
str1 = "11 222 33 4444 55"
patt1 = r'(\d)\1'
print(re.findall(patt1, str1)) # ['1','2','3','4','5'](返回分组内容,需调整为匹配整体)
patt1_fix = r'(\d)\1+' # 匹配1个以上重复数字
print(re.findall(patt1_fix, str1)) # ['1','2','3','4','5'](仍返回分组,需用 group())
# 用 match 对象获取整体匹配
for match in re.finditer(patt1_fix, str1):
print(match.group()) # 输出:11、22、33、4444、55
# 2. 命名分组:匹配 abab 格式(命名分组为 ab_group)
patt2 = r'(?P)(?P=ab_group)'
str2 = "ab abab ababab"
print(re.findall(patt2, str2)) # ['ab', 'ab'](分组内容)
for match in re.finditer(patt2, str2):
print(match.group()) # 输出:abab、abab
2.2 断言(零宽断言):匹配位置而非字符
断言(Assertion)用于 "判断某个位置前后是否满足特定条件",本身不匹配任何字符(零宽度),仅用于定位。断言分为四大类:正向先行断言、负向先行断言、正向后行断言、负向后行断言。
2.2.1 先行断言(Lookahead):判断位置后面的内容
- 正向先行断言:(?=模式) ------ 位置后面必须匹配模式;
- 负向先行断言:(?!模式) ------ 位置后面必须不匹配模式。
示例:
- \d+(?=元):匹配 "元" 前面的数字(如 100元 中的 100);
- \d+(?!元):匹配后面不是 "元" 的数字(如 100美元 中的 100)。
2.2.2 后行断言(Lookbehind):判断位置前面的内容
- 正向后行断言:(?<=模式) ------ 位置前面必须匹配模式;
- 负向后行断言:(?) ------ 位置前面必须不匹配模式。
示例:
- (?)\d+:匹配 "¥" 后面的数字(如 ¥100 中的 100);
- (?)\d+:匹配前面不是 "¥" 的数字(如 $100 中的 100)。
2.2.3 断言的核心特点
- 零宽度:断言匹配的是 "位置",而非字符,因此匹配结果中不会包含断言的内容;
- 不消耗字符:匹配后指针不移动,后续匹配从当前位置继续;
- 支持嵌套:断言中可以嵌套其他断言(如 (?{3})¥)\d+)。
示例 7:断言实战(Python)
python
# Python:断言提取价格(支持后行断言,Python 3.7+)
import re
str1 = "苹果:¥5.99 香蕉:10元 橙子:$12.5 葡萄:8.8元"
# 1. 提取 ¥ 后面的价格(正向后行断言)
patt1 = r'(?.?\d*'
print(re.findall(patt1, str1)) # ['5.99']
# 2. 提取 元 前面的价格(正向先行断言)
patt2 = r'\d+\.?\d*(?=元)'
print(re.findall(patt2, str1)) # ['10', '8.8']
# 3. 提取非 ¥ 开头的价格(负向后行断言)
patt3 = r'(?d+\.?\d*(?=元)'
print(re.findall(patt3, str1)) # ['10', '8.8']
# 4. 提取不是 元 结尾的价格(负向先行断言)
patt4 = r'\d+\.?\d*(?!元)'
print(re.findall(patt4, str1)) # ['5.99', '12.5'](注意:10 和 8.8 后面是元,被排除)
2.3 特殊构造:条件匹配与 Unicode 支持
2.3.1 条件匹配((?(条件)模式1|模式2))
条件匹配根据 "分组是否匹配" 或 "断言是否成立" 选择匹配模式,语法:
- (?(n)模式1|模式2):如果第 n 个分组匹配成功,匹配模式 1,否则匹配模式 2;
- (?(name)模式1|模式2):如果命名分组 name 匹配成功,匹配模式 1,否则匹配模式 2;
- (?(断言)模式1|模式2):如果断言成立,匹配模式 1,否则匹配模式 2。
示例 8:条件匹配实战(Python)
python
import re
# 匹配:如果前面是 http,则匹配 ://,否则匹配 (空)
str1 = "http://runoob.com https://baidu.com ftp://qq.com"
patt1 = r'(http)?(?(1)://|)' # 1是第一个分组(http),如果匹配则匹配 ://,否则匹配空
print(re.findall(patt1, str1)) # [('http', '://'), ('https', ''), ('', '')](注意:https 不匹配 http,因此第二个分组为空)
# 修正:匹配 http 或 https 后面的 ://
patt1_fix = r'(https?)(?(1)://|)'
print(re.findall(patt1_fix, str1)) # [('http', '://'), ('https', '://'), ('', '')]
2.3.2 Unicode 匹配(\p{})
Unicode 匹配用于匹配特定语言、脚本或符号,语法为 \p{属性},需配合 re.UNICODE 修饰符(Python)。
常用 Unicode 属性:
- \p{L}:匹配任意字母(包括中文、英文、日文等);
- \p{N}:匹配任意数字(包括中文数字、阿拉伯数字等);
- \p{Z}:匹配任意空白符;
- \p{Han}:匹配任意中文字符;
- \p{P}:匹配任意标点符号。
示例 9:Unicode 匹配实战(Python)
python
# Python:匹配中文字符和中文数字
import re
str1 = "你好,世界!123 四五六年级 ⅣⅤⅥ"
# 匹配中文字符(\p{Han})
patt1 = re.compile(r'\p{Han}+', re.UNICODE)
print(patt1.findall(str1)) # ['你好', '世界', '四五六年级']
# 匹配所有数字(阿拉伯数字+中文数字+罗马数字)
patt2 = re.compile(r'\p{N}+', re.UNICODE)
print(patt2.findall(str1)) # ['123', '四五六年级', 'ⅣⅤⅥ'](注意:中文数字"四五六年级"被识别为数字)
2.4 正则优化:性能与可读性提升技巧
2.4.1 性能优化原则
- 避免过度回溯:回溯是 NFA 引擎(主流引擎)的核心机制,但过度回溯会导致性能暴跌,需注意:
- 用非贪婪量词替代贪婪量词(如 .*? 替代 .*);
- 避免嵌套量词(如 (a+)* 会导致大量回溯);
- 用断言替代复杂分组(如 (?=\d{11}) 提前判断长度,避免无效匹配)。
- 编译正则:多次使用的正则模式,先编译(re.compile()),提升执行效率。
- 精准匹配:避免使用过于宽泛的模式(如 .*),尽量用具体的字符集(如 [a-zA-Z0-9])。
2.4.2 可读性优化技巧
- 使用原始字符串:避免双转义,提升可读性(如 r'\d' 比 '\\d' 清晰);
- 非捕获分组:无需捕获的分组用 (?:),明确意图;
- 注释模式(re.X):复杂正则用注释说明每个部分的作用。
示例 10:优化后的复杂正则(Python,注释模式)
python
import re
# 匹配邮箱:支持字母、数字、下划线、中划线,域名支持2-4位后缀
email_pattern = re.compile(r'''
^[a-zA-Z0-9_-]+ # 用户名:字母、数字、下划线、中划线
@ # @符号
[a-zA-Z0-9_-]+ # 域名主体:字母、数字、下划线、中划线
(\.[a-zA-Z0-9_-]+)* # 二级域名:.com.cn 等
\.[a-z]{2,4} # 顶级域名:2-4位字母
$
''', re.VERBOSE | re.IGNORECASE) # 注释模式 + 忽略大小写
print(email_pattern.match("test-123@runoob.com")) # 匹配成功(返回Match对象)
print(email_pattern.match("test@.com")) # 匹配失败(返回None)
三:正则表达式实战(多场景应用)
3.1 数据提取:从文本中提取目标信息
数据提取是正则最常用的场景,包括提取数字、邮箱、手机号、URL、日期等。
3.1.1 提取手机号(中国大陆)
- 规则:11 位数字,以 13/14/15/16/17/18/19 开头;
- 正则:1[3-9]\d{9}。
示例 11:提取文本中的所有手机号(Python)
python
import re
text = "我的手机号是13812345678,备用号19987654321,还有一个12345678901(无效)"
phone_pattern = r'1[3-9]\d{9}'
phones = re.findall(phone_pattern, text)
print(phones) # ['13812345678', '19987654321'](12345678901 以12开头,被排除)
3.1.2 提取邮箱地址
- 规则:用户名(字母、数字、下划线、中划线、点号)@ 域名(字母、数字、下划线、中划线、点号). 顶级域名(2-4 位字母);
- 正则:[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*@[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*\.[a-z]{2,4}。
示例 12:提取文本中的所有邮箱(Python)
python
import re
text = "我的邮箱是test123@runoob.com,工作邮箱是user-name.456@company.cn,还有invalid-email@.com(无效)"
email_pattern = r'[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*@[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*\.[a-z]{2,4}'
emails = re.findall(email_pattern, text, re.IGNORECASE)
print(emails) # ['test123@runoob.com', 'user-name.456@company.cn']
3.1.3 提取 URL(网页链接)
- 规则:以 http/https/ftp 开头,包含域名和路径;
- 正则:(https?:\/\/|ftp:\/\/)?([a-zA-Z0-9_-]+\.)+[a-zA-Z0-9_-]+(:\d+)?(\/[^\s]*)?。
示例 13:提取文本中的所有 URL(Python)
python
import re
text = "访问 https://www.runoob.com 学习正则,或 http://baidu.com:8080/search?kw=regex,也可以看 ftp://ftp.example.com/file.txt"
url_pattern = r'(https?:\/\/|ftp:\/\/)?([a-zA-Z0-9_-]+\.)+[a-zA-Z0-9_-]+(:\d+)?(\/[^\s]*)?'
urls = re.findall(url_pattern, text)
# 提取完整URL(因为分组原因,需拼接结果)
complete_urls = [''.join(url_parts) for url_parts in urls if ''.join(url_parts)]
print(complete_urls) # ['https://www.runoob.com', 'http://baidu.com:8080/search?kw=regex', 'ftp://ftp.example.com/file.txt']
3.2 格式校验:验证用户输入合法性
格式校验用于判断用户输入是否符合要求(如密码强度、身份证号、日期格式等)。
3.2.1 验证密码强度(强密码)
- 规则:8-20 位,包含大小写字母、数字、特殊字符(!@#$%^&*)中的至少三种;
- 正则:^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#%\^\&\*\]).{8,20}(四种都包含)或 ^(?![a-zA-Z]+)(?!\[A-Z0-9\]+)(?![A-Z!@#%\^\&\*\]+)(?![a-z0-9]+)(?!\[a-z!@#%^&*]+)(?!\[0-9!@#%^&*]+).{8,20}(至少三种)。
示例 14:验证密码强度(Python)
python
import re
def check_password(password):
# 强密码规则:8-20位,至少包含大小写字母、数字、特殊字符中的三种
pattern = r'^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z!@#$%^&*]+$)(?![a-z0-9]+$)(?![a-z!@#$%^&*]+$)(?![0-9!@#$%^&*]+$).{8,20}$'
return re.match(pattern, password) is not None
print(check_password("Pass123!")) # True(包含大小写、数字、特殊字符)
print(check_password("pass1234")) # False(仅小写字母+数字,两种)
print(check_password("PASSWORD!")) # False(仅大写字母+特殊字符,两种)
3.2.2 验证身份证号(中国大陆 18 位)
- 规则:18 位,前 17 位为数字,第 18 位为数字或 X(大小写均可);
- 正则:^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$。
示例 15:验证身份证号(Python)
python
import re
def check_id_card(id):
pattern = r'^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$'
return re.match(pattern, id) is not None
print(check_id_card("110101199001011234")) # True
print(check_id_card("11010119900101123X")) # True
print(check_id_card("1101011990010112345")) # False(19位)
3.3 文本替换:批量修改字符串内容
文本替换用于批量修改符合规则的内容(如敏感词过滤、格式标准化、冗余内容删除)。
3.3.1 敏感词过滤(替换为 *)
示例 16:过滤敏感词(Python)
python
import re
def filter_sensitive_words(text, sensitive_words):
# 敏感词拼接为正则(用|分隔,re.escape处理特殊字符)
pattern = r'(' + '|'.join(re.escape(word) for word in sensitive_words) + ')'
# 替换为同等长度的*
return re.sub(pattern, lambda m: '*' * len(m.group()), text)
text = "这个混蛋竟然在公共场合说脏话,太无耻了!"
sensitive_words = ["混蛋", "脏话", "无耻"]
filtered_text = filter_sensitive_words(text, sensitive_words)
print(filtered_text) # 输出:这个**竟然在公共场合说**,太**了!
3.3.2 格式化日期(将 2024-05-20 转为 2024 年 05 月 20 日)
示例 17:日期格式转换(Python)
python
import re
text = "今天是2024-05-20,明天是2024-05-21"
pattern = r'(\d{4})-(\d{2})-(\d{2})'
# 替换为 \1年\2月\3日(\1 引用第一个分组,即年份)
formatted_text = re.sub(pattern, r'\1年\2月\3日', text)
print(formatted_text) # 输出:今天是2024年05月20日,明天是2024年05月21日
3.4 字符串分割:按复杂规则分割字符串
字符串分割用于按正则规则分割字符串(比普通 split() 支持更复杂的分隔符)。
3.4.1 按任意空白符分割(空格、制表符、换行符)
示例 18:按空白符分割(Python)
python
import re
text = "Hello World\tPython\nRegex" # 包含多个空格、制表符、换行符
result = re.split(r'\s+', text)
print(result) # ['Hello', 'World', 'Python', 'Regex']
3.4.2 按数字或逗号分割
示例 19:按数字或逗号分割(Python)
python
import re
text = "a1b22c333d,444e555f"
result = re.split(r'\d+|,', text)
print(result) # ['a', 'b', 'c', 'd', 'e', 'f']
四:正则表达式常见问题
4.1 常见错误及解决方案
4.1.1 转义符使用错误
- 问题:Python 中直接写 '\d',导致 \d 被当作转义符(实际匹配 d);
- 解决方案:使用原始字符串 r'\d' 或双转义 '\\d'。
4.1.2 贪婪匹配导致的过度匹配
- 问题:用 .* 匹配时,匹配范围超出预期(如 `>.* 匹配整个字符串);
- 解决方案:使用非贪婪量词 .*? 或更精准的字符集(如 [^<]*)。
4.1.3 分组捕获与非捕获混淆
- 问题:使用 findall() 时,正则中有分组,导致返回分组内容而非整体匹配;
- 解决方案:无需捕获时用非捕获分组 (?:),或调整正则结构(如 r'(\d)\1+' 改为 r'\d\1+',但需注意反向引用的有效性)。
4.1.4 断言的位置错误
- 问题:将断言写在匹配内容后面,导致匹配失败(如 \d+(?=元) 写成 (?=元)\d+,想匹配 "元" 后面的数字,实际匹配 "元" 前面的数字);
- 解决方案:明确断言的作用(先行断言在后面,后行断言在前面)。
4.2 跨语言兼容性注意事项
- 转义规则:
- Python/Java:字符串中需双转义 \\d,或用原始字符串 / 正则字面量;
- JS/Perl:正则字面量中直接写 \d,无需转义。
- 修饰符差异:
- Python 无 g 修饰符,通过 findall()/finditer() 实现全局匹配;
- Java 无 g 修饰符,通过循环 Matcher.find() 实现全局匹配。
- 扩展特性支持:
- 后行断言:Python 3.7+、JS ES2018+、Java 8+ 支持;
- 命名分组:Python 支持 (?P<name>),JS/Java 支持 (?<name>);
- Unicode 匹配:Python 需加 re.UNICODE,Java 默认支持,JS 需开启 u 修饰符(/pattern/u)。
4.3 正则调试工具推荐
- 在线调试工具:
- Regex101:支持多语言(Python/JS/Java 等),实时显示匹配过程和结果;
- Regexr:可视化正则匹配,支持语法提示和示例库;
- 菜鸟教程正则测试工具:适合新手,简洁易用。
- 本地工具:
- Sublime Text/VS Code:内置正则匹配功能,支持实时预览;
- Python IDLE:通过 re.findall()/re.search() 快速测试正则。
4.4正则表达式的优先级
下表清晰列出正则核心语法的优先级顺序,优先级越高的元素越先被解析:
| 优先级 | 语法元素 | 核心说明 |
|---|---|---|
| 1(最高) | 转义字符(\) | 将特殊字符转为普通字符(如。匹配点、* 匹配星号) |
| 2 | 分组 / 捕获(()) | 改变执行顺序,支持捕获组、非捕获组、命名组等 |
| 3 | 量词(重复限定符) | *、+、?、{n}、{n,}、{n,m}(含非贪婪修饰符?) |
| 4 | 锚点 / 边界 | ^(行首)、$(行尾)、\b(单词边界)、\A/\Z(字符串首尾) |
| 5 | 字符类 | [](字符集合)、[^](否定集合)、\d/\w/\s(预定义字符类) |
| 6 | 普通字符 | 无特殊含义的字母、数字、符号(如 a、5、-) |
| 7(最低) | 管道符(|) | 逻辑 "或",匹配任意一个分支 |
第五部分:正则表达式总结与进阶学习资源
5.1 核心知识点总结
正则表达式的学习路径可概括为:
- 基础层:元字符 → 字符集 → 量词 → 分组与选择 → 修饰符;
- 进阶层:反向引用 → 断言 → 条件匹配 → Unicode 匹配;
- 实战层:数据提取 → 格式校验 → 文本替换 → 字符串分割。
核心原则:精准匹配 (用最具体的规则匹配目标内容)、性能优先 (避免过度回溯和宽泛模式)、可读性优先(复杂正则加注释或拆分)。
5.2 进阶学习资源
- 在线文档:
- Python re 模块官方文档:https://docs.python.org/zh-cn/3/library/re.html;
- JavaScript 正则表达式 MDN 文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions;
- Java Pattern 类官方文档:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/regex/Pattern.html。
2.实战练习:
结语
正则表达式是一门 "看似复杂,实则简洁" 的语言,掌握它的核心是 "理解模式匹配的逻辑" 而非死记硬背语法。初期可以从简单场景入手(如提取数字、验证手机号),逐步过渡到复杂场景(如断言、条件匹配),配合调试工具和实战练习,很快就能熟练运用。