前言
上一篇文章我们学习了正则表达式原理,这次我们学习下怎么写正则表达式。这里,我们不会学习正则表达式的各种符号,如果不经常写,那么肯定是会忘的。到时候如果要想写那么还需我们再去查资料。这里我们主要学习下在Java中如何使用正则,以及怎么写出高效的正则表达式。
在Java中使用正则
先来干的,说下在Java中如何使用正则表达式。
通常情况下,我们做一个字符串的分割匹配之类的可能就直接使用String方法自带的api来完成了。这里我们学习下正则表达式的实际使用流程,以及实际应用场景的使用。
在正则表达式原理中,我们知道对于一个正则表达式,计算机需要把这个正则表达式翻译成一个自动机。在Java中这个自动机就是:Pattern。所以第一步就是将正则编译成Pattern。
Pattern p = Pattern.compile("regex");
然后我们用我们的目标字符串去匹配。得到一个匹配结果。Java的正则原理是基于NFA的,支持的能力很全面。因此匹配结果中会有很多的信息。而这个结果在Java中就是Matcher,匹配器。这个匹配结果包含了我们想要的所有匹配信息。
String target = "targetString";
Matcher m = p.matcher(target);
现在我们拿到了匹配结果,这时候就可以从匹配结果中找到我们想要的信息了。
- 判断target整串是否符合正则表达式
boolean isMatch = m.matchers();
- 获取所有匹配的子串
List<String> result = new ArrayList<String>();
m.reset();
while(m.find()) {
result.add(target.subString(m.start(), m.end());
}
- 判断target内是否有符合正则规则的子串
boolean hasMatch = m.lookingAt();
- 替换符合正则规则的子串
String replaceAll = m.replaceAll("xxx");
String replaceFirst = m.replaceFirst("xxx");
注意事项:
- Matcher的matches(),lookingAt(),find()都会受到检索范围的影响。
- find(int start),replaceAll(),replaceFirst(),reset()不受影响。
- 另外Matcher的start和end属性是整个串的index,与检索范围无关。
- 可以使用region(int start, int end)来确定要检索的范围。
Java中正则的数量词模式
我们知道Java的量词有*和+。那么数量词模式又是什么呢?
正则表达式原理中我们知道NFA在匹配的时候需要两个原则,1是量词的优先类型,2是后进先出(回溯路径)。因为量词的优先类型不同可能得到的匹配结果,和匹配的速度也不相同。
Java有三种数量词模式:Greedy(贪婪),Reluctant(懒惰),Possessive(独占)。
听名字可能不知道是干啥,我们来分别介绍下。
- 贪婪模式是默认的模式,正常使用数量词时就是贪婪模式,他会尽可能多的匹配字符串。说白了就是量词优先。
- 懒惰模式,量词后面加?。尽可能少的匹配被量词修饰的字符串。
- 独占模式,量词后面加+,与贪婪相同量词优先,尽可能多匹配字符串,但是不会回溯。
举个例子,对于正则表达式(".*")来说,三种表达如下:
String target = "start \"hello world\" haha \"hello java\" 666 \"hello android\"";
Pattern greedy = Pattern.compile("\".*\"");
Pattern reluctant = Pattern.compile("\".*?\"");
Pattern possessive = Pattern.compile("\".*+\"");
我们可以看到这个正则表达式的意思就是要匹配双引号以及双引号包含的内容。那么这三种写法的结果分别是什么呢?
greedy的匹配结果:"hello world" haha "hello java" 666 "hello android"
reluctant的匹配结果:"hello world", "hello java", "hello android"(三个结果)
possessive的匹配结果:无
对于贪婪模式,尽可能多的匹配,因为是(.* ),所以最后的引号也会匹配到(.* )中,这时明显匹配失败了,这时就会回溯到上一个选择的路径就是对于引号的匹配是用(.*)还是(" ),这时选择(")匹配成功得到 "hello world" haha "hello java" 666 "hello android"。
懒惰模式是尽可能少的匹配量词修饰的字符串,因此会得到三个符合要求的结果。
而独占模式就是匹配失败后不会产生回溯因此匹配结果是空。
高效的使用正则表达式
优化使用正则表达式主要是从两点出发,1是优化正则表达式本身,我们知道NFA型的正则表达式是以正则为主导的,正则表达式的写法不同对于匹配的速度还是有影响的。优化正则表达式本身的思路就是减少回溯,仅可能精准的匹配想要匹配的字符。2、使用过程的优化,工具本身带有的给正则的属性以及自身使用正则的习惯优化。
- 避免重新编译
String target0 = "target0";
String target1 = "target1";
//这样的写法"abc“这个正则会被编译两次
target0.matcher("abc");
target1.matcher("abc");
//"abc"只会被编译一次
Pattern p = Pattern.compline("abc”);
boolean isMatch0 = p.matcher(target0).matchers();
boolean isMatch1 = p.matcher(target1).matchers();
不要滥用括号,使用非捕获型括号
Java中的括号表示捕获组的概念,捕获组在匹配时会有一些引用,造成一些额外的开销,因此能不用括号尽量不用括号。另外,当我们要使用括号表示一个组的时候,如果我们不需要引用括号内的文本,我们可以使用非捕获型括号:(?:)。量词等价转换
对java来说:\d\d\d\d要比\d{4}快(Perl、Python、PHP正好相反)
====比={4}要快(Perl、Ruby和.NET优化手段更高级,两者一样快)
另外,使用正确的量词(+、*、?、{n,m}),如果能够限定长度,那就最好了。不要滥用字符组
如果像这样的字符组”[:]“,里面只有字符,那么这样程序就有处理字符组的代价。使用.和这样的原符号时也不要用[.][]来转义,我们应该使用转义符来转义,而不是用字符组。使用边界匹配符
使用正确的边界匹配符(^、$、\b、\B等),限定搜索字符串位置避免过多回溯
首先优化量词的使用,对于特定的业务场景,查看是需要先优先量词还是忽略量词,对于java来说就是使用贪婪模式还是使用懒惰模式。
另外警惕过度回溯,比如”([abc]|[aef])+“这个正则表达式匹配”aaaaaaaaaaaaaa1"的时候耗时会非常的长,因为他在最后发现1匹配的时候,会回溯到上一个a,然后尝试用[aef]匹配,然后[aef]中有a,然后再去匹配1,发现不匹配,又回退到倒数第二个a,依次类推,回溯的次数变成了指数级。解决方式,第二个去掉a(会回溯一次),或者使用独占模式([abc][aef])++(不会产生回溯)。使用工具验证
检测修改,有时你认为的优化可能禁止了其他你不知道的已经生效的优化。或者你优化了正则,但是正确性却不存在了。附上一个调试正则表达式的网址:http://regex.zjmainstay.cn/。
总结
正则表达式是个很强大的东西,虽然平时我们只是用了它皮毛,但是如果使用不当还是会遇到一些问题的。因此了解它的原理,知道如何正确使用它还是很有必要的。这里我们也只是大致学习了平时自己不了解的正则相关的知识,当然还有很多正则相关的内容这里是没有覆盖到的,感兴趣的同学可以去看精通正则表达式这本书。