正则表达式-进阶

正则表达式-进阶

前言

看到这里,意味着你已经掌握了正则表达式的基础知识,能够运用正则解决一些简单的问题了。如果你不熟悉正则的基础知识,请参考前一篇博客学习。正则表达式-基础

这部分我们将继续探究正则的进阶知识。

探囊取物

我们来看一个例子,请尝试用正则表达式匹配出其姓名和年龄。

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);

输出为:
肚子好饿,早知道当初多刷点力扣了

记住,实践出真知,多多练习,熟能生巧。

发布了61 篇原创文章 · 获赞 25 · 访问量 7210

猜你喜欢

转载自blog.csdn.net/qq_42582489/article/details/103976832