正则表达式-进阶
前言
看到这里,意味着你已经掌握了正则表达式的基础知识,能够运用正则解决一些简单的问题了。如果你不熟悉正则的基础知识,请参考前一篇博客学习。正则表达式-基础
这部分我们将继续探究正则的进阶知识。
探囊取物
我们来看一个例子,请尝试用正则表达式匹配出其姓名和年龄。
Name:Aurora Age:18
里面夹杂着一些无关紧要的数据
Name:Bob Age:20
数据有很多种错误的格式
Name:Cassin Age:22
我们用正则的基础知识来尝试匹配,\w 匹配名字,\s 匹配空白,\d 匹配年龄。匹配的表达式为:Name:\w+\s*Age:\d{1,3}
System.out.println("Name:Aurora Age:18".matches("Name:\\w+\\s*Age:\\d{1,3}")); //输出为true
System.out.println("里面有一些无关紧要的数据".matches("Name:\\w+\\s*Age:\\d{1,3}")); //输出为false
System.out.println("Name:Bob Age:20".matches("Name:\\w+\\s*Age:\\d{1,3}")); //输出为true
System.out.println("数据有很多种错误的格式".matches("Name:\\w+\\s*Age:\\d{1,3}")); //输出为false
System.out.println("Name:Cassin Age:22".matches("Name:\\w+\\s*Age:\\d{1,3}")); //输出为true
既然已经匹配到了姓名和年龄的数据,那么我们就要将它们的内容取出来。这里我们使用 Java 语言的 indexof 和 subString 方法可以做到。
String str="Name:Aurora Age:18";
int begin=str.indexOf("Name");
int end=str.indexOf(' ');
int begin2=str.indexOf("Age");
String s1=str.substring(begin+5, end);
String s2=str.substring(begin2+4);
System.out.println(s1); //输出 Aurora
System.out.println(s2); //输出 18
实际上,正则拥有着更为便捷的取值方式。
我们只要用 ( ) 将需要取值的地方括起来,传给 Pattern 对象,再用 Pattern 对象匹配后获得的 Matcher 对象来取值就行了。每个匹配的值将会按照顺序保存在 Matcher 对象的 group 中。
判断 Pattern 对象与字符串是否匹配的方法是 Matcher.matches(),如果匹配成功,这个函数将返回 true,如果匹配失败,则返回 false。
Pattern pattern = Pattern.compile("Name:(\\w+)\\s*Age:(\\d{1,3})");
Matcher matcher = pattern.matcher("Name:Aurora Age:18");
if(matcher.matches()){
String s1 = matcher.group(1);
String s2 = matcher.group(2);
System.out.println(s1); //输出 Aurora
System.out.println(s2); //输出 18
}
至于说 group 的下标为什么不是从0,而是从1开始,是因为 group(0) 被用来保存整个字符串了。
System.out.println(matcher.group(0)); //输出 Name:Aurora Age:18
到这里你可能会犯迷糊,我们之前一直使用的是 String.matches 方法来匹配正则表达式,这里的 Pattern 又是什么呢?别着急,我们一步一步往下说。
我们首先来看下 String.matches 方法的源代码。
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
我们看到,该源代码中调用了 Pattern.matches 方法,我们继续跟进查看源代码。
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
我们可以看到,String.matches 方法的内部就是调用 Pattern 。而且,每次调用 String.matches 函数,都会新建出一个 Pattern 对象。
如果我们要使用同一个正则表达式多次匹配字符串的话,最佳做法是先新建一个 Pattern 对象,这样可以反复使用,提高程序运行效率。
Pattern pattern = Pattern.compile("Name:(\\w+)\\s*Age:(\\d{1,3})");
Matcher matcher1 = pattern.matcher("Name:Aurora Age:18");
Matcher matcher2 = pattern.matcher("Name:Bob Age:20");
Matcher matcher3 = pattern.matcher("Name:Cassin Age:22");
移花接木
考虑一个实际场景:你有一个让用户输入便签的输入框,用户可以输入多个标签,但是你并没有提示用户,标签之间应该用什么间隔符号隔开。
这种情况下,用户的输入是五花八门的,会用空格,逗号,分号等一系列分隔符。例如:
- 二分,回溯,递归,分治
- 搜索;查找;旋转;遍历
- 数论 图论 逻辑 概率
一般的做法是使用 String.split 方法,依次尝试各种分割符号来解决这个问题。
public static String[] splitTabs(String tabs) {
if(tabs.split(",").length==4) return tabs.split(",");
if(tabs.split(";").length==4) return tabs.split(";");
if(tabs.split(" ").length==4) return tabs.split(" ");
return new String[0];
}
public static void main(String[] args) {
System.out.println(Arrays.toString(splitTabs("二分,回溯,递归,分治")));
System.out.println(Arrays.toString(splitTabs("搜索;查找;旋转;遍历")));
System.out.println(Arrays.toString(splitTabs("数论 图论 逻辑 概率")));
}
输出为:
[二分, 回溯, 递归, 分治]
[搜索, 查找, 旋转, 遍历]
[数论, 图论, 逻辑, 概率]
这种方法简单粗暴,我们可以用正则表达式做到更好。
实际上,split 函数传入的参数就是一个正则表达式。如果直接使用某字符串,就属于精确匹配了,只能匹配那一个字符串。我们应该使用正则表达式的模糊匹配,只要能匹配成功,就将其分割。
System.out.println(Arrays.toString("二分,回溯,递归,分治".split("[,;\\s+]")));
System.out.println(Arrays.toString("搜索;查找;旋转;遍历".split("[,;\\s+]")));
System.out.println(Arrays.toString("数论 图论 逻辑 概率".split("[,;\\s+]")));
输出为:
[二分, 回溯, 递归, 分治]
[搜索, 查找, 旋转, 遍历]
[数论, 图论, 逻辑, 概率]
字符串中,不仅 split 函数,replaceAll 函数也是传的正则表达式。我们可以用正则表达式进行模糊匹配,将符合规则的字符串全部替换掉。
上面的例子中,我们可以把用户输入的所有数据统一规范为使用 ; 分割。
System.out.println("二分,回溯,递归,分治".replaceAll("[,;\\s+]",";"));
System.out.println("搜索;查找;旋转;遍历".replaceAll("[,;\\s+]",";"));
System.out.println("数论 图论 逻辑 概率".replaceAll("[,;\\s+]",";"));
输出为:
二分;回溯;递归;分治
搜索;查找;旋转;遍历
数论;图论;逻辑;概率
在 replaceAll 的第二个参数中,我们还可以通过 $1,$2,…来反向引用匹配到的子串。只要将需要引用的部分用 ( ) 括起来就可以了。
System.out.println("二分,回溯,递归,分治".replaceAll("([,;\\s+])","---$1"));
System.out.println("搜索;查找;旋转;遍历".replaceAll("([,;\\s+])","$1+++"));
System.out.println("数论 图论 逻辑 概率".replaceAll("([,;\\s+])","***$1***"));
输出为:
二分---,回溯---,递归---,分治
搜索;+++查找;+++旋转;+++遍历
数论*** ***图论*** ***逻辑*** ***概率
有时候我们不需要替换,只需要将正则匹配出来的部分添加一些前缀或后缀,就可以用这种方式!
蓦然回首
给你一串字符串,统计尾数 e 的个数:
- LeetCode
- LeetCodeeeee
- LeetCodeee
这看起来并不难,结合我们所学的知识,使用 (\w+)(e*) 匹配,再用 group(2) 判断即可。
Pattern pattern = Pattern.compile("(\\w+)(e*)");
Matcher matcher = pattern.matcher("LeetCode");
if(matcher.matches()) {
String s1 = matcher.group(1);
String s2 = matcher.group(2);
System.out.println("s1= "+s1+", s1.length= "+s1.length());
System.out.println("s2= "+s2+", s2.length= "+s2.length());
}
输出为:
s1= LeetCode, s1.length= 8
s2= , s2.length= 0
我们原本期望的是 s1=LeetCod,s2=e,但是结果好像并不和我们想的一样。
还记得我们在基础部分提到的贪婪模式和非贪婪模式吗。这个例子就是因为默认使用的贪婪匹配, e 仍然属于 \w 能匹配的范畴,正则表达式默认会尽可能多地向后匹配,所以 LeetCode 全部被 s1匹配完了。
非贪婪匹配的定义是在能匹配目标字符串的前提下,尽可能少的向后匹配。
贪婪匹配要更改为非贪婪匹配也很简单,只需要非贪婪匹配的正则表达式后面加个 ? 即可表示非贪婪匹配。
Pattern pattern = Pattern.compile("(\\w+?)(e*)");
Matcher matcher = pattern.matcher("LeetCode");
if(matcher.matches()) {
String s1 = matcher.group(1);
String s2 = matcher.group(2);
System.out.println("s1= "+s1+", s1.length= "+s1.length());
System.out.println("s2= "+s2+", s2.length= "+s2.length());
}
输出为:
s1= LeetCod, s1.length= 7
s2= e, s2.length= 1
我们第一次介绍 ?时,? 表示的是匹配 0 次或者 1 次,而非贪婪匹配要使用 ?,会不会出现符号混淆的问题呢?不用担心,这个问题不会出现。
- 如果只有一个字符,那就不存在贪婪不贪婪的问题
- 如果匹配多次,那么表示非贪婪匹配的 ? 前面必有一个标志匹配次数的符号
上面的这个例子中,不会出现 s1=L,s2=ee 的情况。如果这样匹配的话,字符串 LeetCode 就无法和正则表达式匹配起来了。
最终考验
最后出一道题来测试一下,如果通过了它,相信你应该学会了正则的知识。
有一个人说话不利索,经常口吃,请你帮忙纠正他。
肚…子。。好饿…,…早知道…当…初…。。。多…刷…点。。。力…扣了…!
String str="肚...子。。好饿........,....早知道.....当.....初...。。。多.....刷.....点。。。力.....扣了.........";
String str2=str.replaceAll("[\\.。]","");
System.out.println(str2);
输出为:
肚子好饿,早知道当初多刷点力扣了
记住,实践出真知,多多练习,熟能生巧。