正则表达式

什么是正则表达式

正则表达式就是用特殊符号来描述文本模式的”搜索语法”。比如你想找所有手机号,总不能一个个翻吧?写个正则 1[3-9]\d{9} 就能一网打尽

Java里用 PatternMatcher 两个类来处理正则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testBasicRegex() {
// 最基本的用法
String text = "我的手机号是13812345678,备用号是15998765432";

// 1. 编译正则表达式
Pattern pattern = Pattern.compile("1[3-9]\\d{9}");

// 2. 创建匹配器
Matcher matcher = pattern.matcher(text);

// 3. 查找所有匹配
while (matcher.find()) {
System.out.println("找到手机号: " + matcher.group());
}
// 输出: 找到手机号: 13812345678
// 输出: 找到手机号: 15998765432
}

常用语法表格

字符类

语法 含义 示例
. 任意一个字符(除换行) a.c 匹配 abc、a1c、a_c
\d 一个数字,等价于 [0-9] \d{3} 匹配 123
\D 非数字 \D+ 匹配 abc
\w 字母/数字/下划线,等价于 [a-zA-Z0-9_] \w+ 匹配 hello_123
\W 非单词字符 \W 匹配 @、# 等
\s 空白字符(空格、Tab、换行) \s+ 匹配空白区域
\S 非空白字符
[abc] a或b或c中的任意一个 [aeiou] 匹配元音字母
[^abc] 除了a、b、c的任意字符 [^0-9] 匹配非数字
[a-z] a到z范围内的字符 [A-Za-z] 匹配字母

量词

语法 含义 示例
* 0次或多次 ab*c 匹配 ac、abc、abbc
+ 1次或多次 ab+c 匹配 abc、abbc,不匹配 ac
? 0次或1次 colou?r 匹配 color 和 colour
{n} 恰好n次 \d{4} 匹配4位数字
{n,} 至少n次 \d{2,} 匹配2位及以上数字
{n,m} n到m次 \d{6,8} 匹配6-8位数字

锚点

语法 含义 示例
^ 字符串开头 ^Hello 匹配以Hello开头
$ 字符串结尾 world$ 匹配以world结尾
\b 单词边界 \bjava\b 匹配独立的java,不匹配javascript

分组与引用

语法 含义 示例
(abc) 分组,捕获匹配内容 (\d{4})-(\d{2})-(\d{2}) 捕获年月日
\1 反向引用第1个分组 (\w+)\s+\1 匹配重复单词 the the
(?:abc) 非捕获分组 只分组不捕获
a|b 或(a或b) cat|dog 匹配cat或dog

常用正则模式

开发中经常要校验的格式,直接抄就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testCommonPatterns() {
// 手机号(简单版:1开头,第二位3-9,共11位)
String phoneRegex = "^1[3-9]\\d{9}$";
System.out.println("13812345678".matches(phoneRegex)); // true
System.out.println("12345678901".matches(phoneRegex)); // false(第二位是2)

// 邮箱(简化版)
String emailRegex = "^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$";
System.out.println("test@gmail.com".matches(emailRegex)); // true

// IPv4地址(简单版)
String ipRegex = "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$";
System.out.println("192.168.1.1".matches(ipRegex)); // true
System.out.println("999.999.999.999".matches(ipRegex)); // false

// 身份证号(18位)
String idCardRegex = "^\\d{17}[\\dXx]$";
System.out.println("110101199001011234".matches(idCardRegex)); // true
}
场景 正则 说明
手机号 1[3-9]\d{9} 简单校验,够用
邮箱 [\w.-]+@[\w.-]+\.[a-zA-Z]{2,} 不完美但够用
IPv4 太长了看上面代码 要严格校验0-255
身份证 \d{17}[\dXx] 只校验格式,不校验校验码
中文 [\u4e00-\u9fa5]+ Unicode范围
密码强度 (?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,} 至少8位含大小写和数字

Java中的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testJavaRegexApi() {
// 方式1:String.matches() - 整个字符串是否匹配
boolean isPhone = "13812345678".matches("1[3-9]\\d{9}");
System.out.println("是手机号: " + isPhone); // true

// 方式2:String.replaceAll() - 替换所有匹配
String text = "价格是100元,优惠后50元";
String result = text.replaceAll("\\d+", "***");
System.out.println(result); // 价格是***元,优惠后***元

// 方式3:String.split() - 按正则分割
String csv = "apple,,banana,,,cherry";
String[] fruits = csv.split(",+"); // 一个或多个逗号
System.out.println(Arrays.toString(fruits)); // [apple, banana, cherry]

// 方式4:Pattern + Matcher - 最灵活,可以提取分组
String log = "2024-01-15 ERROR 用户登录失败 2024-01-16 WARN 磁盘空间不足";
Pattern p = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})\\s+(ERROR|WARN)\\s+(.+?)(?=\\d{4}|$)");
Matcher m = p.matcher(log);
while (m.find()) {
System.out.println("日期: " + m.group(1) + ", 级别: " + m.group(2) + ", 内容: " + m.group(3).trim());
}
}

性能提示:如果一个正则要用很多次,别每次都 Pattern.compile(),提前编译好复用

1
2
3
4
5
6
7
8
9
10
// ❌ 每次调用都重新编译,浪费性能
public boolean isPhone(String s) {
return s.matches("1[3-9]\\d{9}"); // matches() 内部每次都 compile
}

// ✅ 编译一次,多次使用
private static final Pattern PHONE_PATTERN = Pattern.compile("1[3-9]\\d{9}");
public boolean isPhone(String s) {
return PHONE_PATTERN.matcher(s).matches();
}

⭐ 安全角度:ReDoS(正则拒绝服务)

什么是灾难性回溯

某些写得不好的正则遇到特定输入时,匹配时间会指数级增长,CPU直接飙到100%,相当于把服务器搞瘫了

原理:正则引擎用回溯算法匹配,嵌套量词会导致组合爆炸

恶意正则示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testReDoS() {
// ⚠️ 危险正则:(a+)+$ 里有嵌套量词
// 当输入 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaab" 时
// 正则引擎会疯狂回溯,尝试所有可能的分组方式

Pattern evil = Pattern.compile("(a+)+$");
String malicious = "aaaaaaaaaaaaaaaaaaaaaaaaaab"; // a重复很多次后跟一个b

long start = System.currentTimeMillis();
evil.matcher(malicious).matches(); // ⚠️ 这一行可能跑几十秒甚至更久!
System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");

// 真实案例:
// 2019年 Cloudflare 全球宕机27分钟,原因就是一条恶意正则
// Stack Overflow 也因为 ReDoS 出过故障
}

容易触发ReDoS的正则特征

危险模式 示例 问题
嵌套量词 (a+)+ 内外都是量词,组合爆炸
重叠选择 (a|a)+ 两个分支匹配同样的东西
量词+回溯 (a+b)+c 配合无法匹配的尾部

防御方法

避免嵌套量词:(a+)+ 改成 a+

用占有量词(不回溯):(a++)+$ 或原子组 (?>a+)+$

设置超时:

1
2
3
4
5
6
7
8
9
// Java没有内置正则超时,可以在单独线程里跑,超时就kill
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Boolean> future = executor.submit(() -> pattern.matcher(input).matches());
try {
boolean result = future.get(1, TimeUnit.SECONDS); // 最多等1秒
} catch (TimeoutException e) {
future.cancel(true);
System.out.println("正则匹配超时,可能有ReDoS风险!");
}

用户输入的正则要做安全检查,或者干脆不让用户自定义正则

常见坑

坑1:Java字符串里反斜杠要转义,正则 \d 在Java里要写成 "\\d"。经常写少一个反斜杠

坑2matches()全量匹配(相当于自动加了 ^...$),find()部分匹配

1
2
"abc123def".matches("\\d+");        // false!整个字符串不全是数字
Pattern.compile("\\d+").matcher("abc123def").find(); // true!找到了123

坑3:忘记正则里的特殊字符需要转义。. 在正则里是”任意字符”,想匹配实际的点要写 \\.

坑4:贪婪匹配 vs 懒惰匹配

1
2
3
4
5
String html = "<b>hello</b> and <b>world</b>";
// 贪婪(默认):尽可能多匹配
html.replaceAll("<b>.*</b>", "X"); // X(整个都被替换了)
// 懒惰(加?):尽可能少匹配
html.replaceAll("<b>.*?</b>", "X"); // X and X(分别替换)

坑5:用 split(".") 分割字符串,结果是空数组!因为 . 匹配了所有字符。正确写法:split("\\.")

练习题

题1:写一个正则,从文本中提取所有URL(http或https开头)

题2:写一个正则,校验密码强度(至少8位,包含大写、小写、数字、特殊字符中的至少3种)

题3(⭐ 安全题):以下正则用来校验邮箱格式,分析是否存在ReDoS风险:

1
2
String emailRegex = "^([a-zA-Z0-9]+\\.?)+@([a-zA-Z0-9]+\\.)+[a-zA-Z]{2,}$";
// 提示:试试输入 "aaaaaaaaaaaaaaaaaaaaa@" 会怎样

上一章 目录 下一章
网络编程 java基础 日志