什么是正则表达式
正则表达式就是用特殊符号来描述文本模式的”搜索语法”。比如你想找所有手机号,总不能一个个翻吧?写个正则 1[3-9]\d{9} 就能一网打尽
Java里用 Pattern 和 Matcher 两个类来处理正则
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";
Pattern pattern = Pattern.compile("1[3-9]\\d{9}");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) { System.out.println("找到手机号: " + matcher.group()); }
}
|
常用语法表格
字符类
| 语法 |
含义 |
示例 |
. |
任意一个字符(除换行) |
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() {
String phoneRegex = "^1[3-9]\\d{9}$"; System.out.println("13812345678".matches(phoneRegex)); System.out.println("12345678901".matches(phoneRegex));
String emailRegex = "^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"; System.out.println("test@gmail.com".matches(emailRegex));
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)); System.out.println("999.999.999.999".matches(ipRegex));
String idCardRegex = "^\\d{17}[\\dXx]$"; System.out.println("110101199001011234".matches(idCardRegex)); }
|
| 场景 |
正则 |
说明 |
| 手机号 |
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() {
boolean isPhone = "13812345678".matches("1[3-9]\\d{9}"); System.out.println("是手机号: " + isPhone);
String text = "价格是100元,优惠后50元"; String result = text.replaceAll("\\d+", "***"); System.out.println(result);
String csv = "apple,,banana,,,cherry"; String[] fruits = csv.split(",+"); System.out.println(Arrays.toString(fruits));
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}"); }
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() {
Pattern evil = Pattern.compile("(a+)+$"); String malicious = "aaaaaaaaaaaaaaaaaaaaaaaaaab";
long start = System.currentTimeMillis(); evil.matcher(malicious).matches(); System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
}
|
容易触发ReDoS的正则特征
| 危险模式 |
示例 |
问题 |
| 嵌套量词 |
(a+)+ |
内外都是量词,组合爆炸 |
| 重叠选择 |
(a|a)+ |
两个分支匹配同样的东西 |
| 量词+回溯 |
(a+b)+c |
配合无法匹配的尾部 |
防御方法
避免嵌套量词:(a+)+ 改成 a+
用占有量词(不回溯):(a++)+$ 或原子组 (?>a+)+$
设置超时:
1 2 3 4 5 6 7 8 9
| ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Boolean> future = executor.submit(() -> pattern.matcher(input).matches()); try { boolean result = future.get(1, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); System.out.println("正则匹配超时,可能有ReDoS风险!"); }
|
用户输入的正则要做安全检查,或者干脆不让用户自定义正则
常见坑
坑1:Java字符串里反斜杠要转义,正则 \d 在Java里要写成 "\\d"。经常写少一个反斜杠
坑2:matches() 是全量匹配(相当于自动加了 ^...$),find() 是部分匹配
1 2
| "abc123def".matches("\\d+"); Pattern.compile("\\d+").matcher("abc123def").find();
|
坑3:忘记正则里的特殊字符需要转义。. 在正则里是”任意字符”,想匹配实际的点要写 \\.
坑4:贪婪匹配 vs 懒惰匹配
1 2 3 4 5
| String html = "<b>hello</b> and <b>world</b>";
html.replaceAll("<b>.*</b>", "X");
html.replaceAll("<b>.*?</b>", "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,}$";
|