一文带你搞懂java中的正则式表达(上)

294 阅读5分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

一、正则式表达

正则表达式,,又称规则表达式,一个十分古老而又强大的文本处理工具,仅用一段非常简短的表达式语句,便能够快速实现一个非常复杂的业务逻辑。熟练地掌握正则表达式的话,能够使你的开发效率得到极大的提升。通常我们用其来进行检索、替换和控制文本,主要包括a 到 z 的字母以及一些特殊的元字符。

二、基础知识

2.1 基础语法

正则表达式中一般由普通字符非打印字符特殊字符(元字符)、**限定符(数量限定符)**组成。

如: [a-z]+

打印字符:任何可以打印的字符均可以

非打印字符

字符含义
\cx匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
\f匹配一个换页符。等价于 \x0c 和 \cL。
\n匹配一个换行符。等价于 \x0a 和 \cJ。
\r匹配一个回车符。等价于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]
\S匹配任何非空白字符。等价于 [^\f\n\r\t\v]
\t匹配一个制表符。等价于 \x09 和 \cI。
\v匹配一个垂直制表符。等价于 \x0b 和 \cK。

元字符:

元字符含义
$匹配输入字符串的结尾位置。JS如果设置了 RegExp 对象的 Multiline 属性,则 也匹配 '\n' 或 '\r'。要匹配 字符本身,请使用 \$
( )标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用 \(\)
*匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 \*
+匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+
.匹配除换行符 \n之外的任何单字符。要匹配 .,请使用 \.
[标记一个中括号表达式的开始。要匹配 [,请使用 \[
?匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 \?
\将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, 'n' 匹配字符 'n'。'\n' 匹配换行符。序列 ' \\' 匹配 " \",而 ' \(' 则匹配 "("。
匹配输入字符串的开始位置,除非在方括号表达式中使用,此时它表示不接受该字符集合。要匹配 ^ 字符本身,请使用 \^
{标记限定符表达式的开始。要匹配 {,请使用 \{
\

数量限定符

字符含义
*匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于{0,}。
+匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。
?匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 、 "does" 中的 "does" 、 "doxy" 中的 "do" 。? 等价于 {0,1}。
{n}n 是一个非负整数。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。
{n,}n 是一个非负整数。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。
{n,m}m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格。

不同的语言实现的正则对正则表达式的支持不太一样,这里只是罗列的常见的语法符号。

三、相关拓展

正则式的便捷方式不仅仅在于验证一个字符串是否满足简单的正则表达式的情况,巧妙利用还能少些不少代码。比如以下几个知识点的拓展:

3.1 DFA引擎和NFA引擎

正则引擎主要可以分为基本不同的两大类:一种是DFA(确定型有穷自动机),另一种是NFA(不确定型有穷自动机)。简单来讲,NFA 对应的是正则表达式主导的匹配,而 DFA 对应的是文本主导的匹配

DFA从匹配文本入手,从左到右,每个字符不会匹配两次,它的时间复杂度是多项式的,所以通常情况下,它的速度更快,但支持的特性很少,不支持捕获组、各种引用等等;而NFA则是从正则表达式入手,不断读入字符,尝试是否匹配当前正则,不匹配则吐出字符重新尝试,通常它的速度比较慢,最优时间复杂度为多项式的,最差情况为指数级的。但NFA支持更多的特性,因而绝大多数编程场景下(包括java,js),我们面对的是NFA。

3.2 贪婪模式与非贪婪模式

前面我们讲到了限定符* 、 + 、? 三个都是限定数量的,这三个字符在正则匹配字符串的时候会尽可能多的匹配文字,这种情况我们称之为贪婪匹配,即趋于最大长度匹配。反之,如果刚好匹配到结果,即最小长度匹配就是称为非贪婪匹配

如何使用呢?默认情况下一般都是贪婪模式,如果需要开启非贪婪模式,我们只需要在数量限定符后加一个 ?符号即可。

举个例子:

HelloRegexExpression

如果用正则表达式 Hello.*e去匹配的话,结果就会匹配到 HelloRegexExpre,那如果是非贪婪匹配的话,我们就需要将正则表达式改为 Hello.*?e,这样匹配的结果就会是 HelloRe了。

验证代码

@Test
public void testReg(){
    String str = "HelloRegexExpression";
    Pattern pattern1 = Pattern.compile("Hello.+e");
    Matcher matcher = pattern1.matcher(str);
    while (matcher.find()){
        System.out.println("贪婪模式:");
        System.out.println(matcher.group(0));
    }
    Pattern pattern2 = Pattern.compile("Hello.+?e");
    Matcher matcher2 = pattern2.matcher(str);
    while (matcher2.find()){
        System.out.println("非贪婪模式:");
        System.out.println(matcher2.group(0));
    }
}

运行结果

贪婪模式:
HelloRegexExpre
非贪婪模式:
HelloRe

使用场景:

3.3 独占模式

同贪婪模式一样,独占模式一样会匹配最长。不过在独占模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯。如何开启独占模式呢?只需要在数量限定符后面加上 +符号即可。我们还是以上节的HelloExpression为例,如果使用独占模式,正则表达式就会是这样:

Hello.*+e

那么同样我们的测试代码如下:

Pattern pattern3 = Pattern.compile("Hello.++e");
Matcher matcher3 = pattern3.matcher(str);
while(matcher3.find()){
    System.out.println("独占模式:");
    System.out.println(matcher3.group(0));
}

结果是什么输出都没有!因为在匹配时发生失败了,具体过程我们可以在下一节的回溯过程中了解一下,本章我们就先讲到这里。