重新认识正则表达式

前言

阅读对象:有正则表达式基础的

提几个有关正则表达式的问题检验一下,要求用正则表达式解决:

  • 如何利用正则表达式查找到字符串“Hello lolipop! love you, solo?”中以lo结尾的单词?
  • 如何将“http://10.1.1.1:8888/index.html#/get/user?id=1234”这个地址按照协议、IP、端口、路径、路由、参数解析拆分开来?
  • 如何利用正则表达式将“Hello World”变成“Hello Java”?
  • 密码长度8-20位,必须包含大写,小写字母,数字,特殊符号的三种及其以上,正则表达式如何写?
  • 如何查找“Windows98,Windows2000,Windows10, Windows2000”中所有Windows2000的Windows所在的位置?
  • 如何将“9999999999”变成“9,999,999,999”?

如果你都会,大佬你可以溜了,不会那就再温习一下正则表达式!

一、基础知识回顾

正则表达式,又称规则表达式,是一种文本模式,通常用来检索、替换和控制文本。主要包括a 到 z 的字母以及一些特殊的元字符

1.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?’。请注意在逗号和两个数之间不能有空格。

当然不同的语言实现的正则对正则表达式的支持不太一样,这里只是罗列的常见的语法符号,像Java还支持POSIX字符组,\p{Lower}小写字母,可以针对语言再查相关的说明文档。

二、进阶知识

很多同学可能对于正则表达式的用法仅仅停留在验证一个字符串是否满足简单的正则表达式的情况,而且有的是直接在使用时百度现成的表达式,如常用的邮箱验证正则等。但是以下这几样知识点了解一下,可能你会对正则表达式有新的认识。

2.1 DFA引擎和NFA引擎

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

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

对于两种引擎的匹配机制的具体细节可以去自行查找资料,涉及 的内容可能就需要了解编译原理这门课了。

2.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

使用场景:

2.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));
}

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

2.4 回溯

当一个正则表达式扫描目标字符串时,它从左到右逐个扫描正则表达式的组成部分,在每个位置上测试能不能找到一个匹配。对于每一个量词和分支,都必须决定如何继续进行。如果是一个量词(诸如*,+?,或者{2,}),正则表达式必须决定何时尝试匹配更多的字符;如果遇到分支(通过|操作符),它必须从这些选项中选择一个进行尝试。
每当正则表达式做出这样的决定,如果有必要的话,它会记住另一个选项,以备将来返回后使用。如果所选方案匹配成功,正则表达式将继续扫描正则表达式模板,如果其余部分匹配也成功了,那么匹配就结束了。但是如果所选择的方案未能发现相应匹配,或者后来的匹配也失败了,正则表达式将回溯到最后一个决策点,然后在剩余的选项中选择一个。它继续这样下去,直到找到一个匹配,或者量词和分支选项的所有可能的排列组合都尝试失败了,那么它将放弃这一过程,然后移动到此过程开始位置的下一个字符上,重复此过程。

回溯是正则匹配的基本过程,了解清楚回溯的基本工作原理,对于我们优化正则表达式是非常关键的,因为回溯的计算代价是比较昂贵的。

2.4.1 匹配基本过程
# 文本(文字下标从0开始):
after tonight
# 正则表达式
to(nite|nighta|night)

在NFA匹配的时候,从t开始匹配a,失败,继续,直到文本里面的第一个t(下标为2),然后接着比较o和文本中的e,失败,正则回退到t,继续,直到找到文本中的第二个t(下标为6),然后o和文本中的o比较,匹配,继续这样的过程,正则表达式后面有三个可选条件,依次匹配,直到匹配成功。

2.4.2 回溯图解

正则表达式

ab{1,3}c

文本

abc

回溯过程

在这里插入图片描述

1~2步应该都好理解,但是为什么在第3步开始,虽然已经文本中已经有一个b匹配了b{1,3},后面还会拉着字母c跟b{1,3}做比较呢?这个就是我们下面将要提到的正则的贪婪特性,也就是说b{1,3}会竭尽所能的匹配最多的字符。在这个地方我们先知道它一直要匹配到撞上南墙为止。 在这种情况下,第3步发生不匹配之后,整个匹配流程并没有走完,而是像栈一样,将字符c吐出来,然后去用正则表达式中的c去和文本中的c进行匹配。这样就发生了一次回溯。

非贪婪模式回溯过程

在这里插入图片描述

独占模式回溯过程:

示例改为:

正则:
ab{1,3}+bc
文本:
abbc

在这里插入图片描述

正如我们所提到的回溯的代价是比较大的,所以书写高效的正则表达式是非常重要的,否则可能会出现回溯失控的现象,导致我们系统的性能有所下降,降低用户的体验。

2.5 正向预查和反向预查(也有叫做环视)

这里我们要提到5组概念,分别是非获取匹配正向肯定预查正向否定预查反向肯定预查反向否定预查

非获取匹配 语法:(?:pattern)

即匹配pattern但不获取匹配结果,不进行缓存供以后使用。

正向肯定预查 语法:(?=pattern)

在任何匹配pattern的字符串开始处匹配查找字符串

# 正则
Windows(?=98|xp|7|10)
测试文本 ==> 查找结果
Windows98 ==> true
Windows2000 ==> false

正向否定预查 语法:(?!pattern)

在任何不匹配pattern的字符串开始处匹配查找字符串

# 正则
Windows(?!98|xp|7|10)
测试文本 ==> 查找结果
Windows98 ==> false
Windows2000 ==> true

反向肯定预查 语法:(?<=pattern)

与正常肯定预查类似,只是方向相反

# 正则
(?<=98|xp|7|10)Windows
测试文本 ==> 查找结果
98Windows ==> true
2000Windows ==> false

反向否定预查 语法:(?<!pattern)

与正向否定预查类似,只是方向相反

# 正则
(?<!98|xp|7|10)Windows
测试文本 ==> 查找结果
98Windows ==> false
2000Windows ==> true

三、Java正则表达式API

JDK提供了正则表达式相关的工具包,放在java.util.regex包下,我们简要了解一下其中核心的几个类:

  • Pattern

它是一个正则表达式的编译表示形式。一个字符形式的正则表达式必须通过Pattern的compile方法来构建一个Pattern实例,然后我们可以通过这个Pattern实例来获取一个匹配器Matcher的实例对象,我们就可以通过Matcher对象进行任意字符串的正则匹配操作并获取结果。这是一个不可变的类,并且是线程安全的。

  • Matcher

它是用于解释一个Pattern,并对输入的字符串进行匹配操作的引擎。它是通过调用Pattern的matcher方法获取,一旦创建,matcher可以执行以下三种匹配操作:

(1)matches方法,针对整个输入字符串进行匹配操作

(2)lookingAt方法,从字符串的开始位置进行匹配,不同于matches方法的是他不需要匹配到全部字符串

(3)find方法,用于匹配查找符合正则的子字符串序列的

@Test
public void testReg(){
    String str ="HelloWorld";
    Pattern r = Pattern.compile("Hel.*l");
    System.out.println(r.matcher(str).matches());// false
    System.out.println(r.matcher(str).lookingAt()); // true
    System.out.println(r.matcher(str).find()); // true
}

Java正则表达式支持很多字符包括POSIX字符组,这里就不贴表出来了,请查阅JavaSE的文档。

四、问题答案

(1)如何利用正则表达式查找到字符串“Hello lolipop! love you, solo?”中以lo结尾的单词?

@Test
public void testReg(){
    String str = "Hello lolipop! love you, solo?";
    Pattern pattern = Pattern.compile("(?=.+?)\\b\\w+lo\\b");
    Matcher matcher = pattern.matcher(str);
    while (matcher.find()){
        System.out.println(matcher.group());
    }
}

(2)如何将“http://10.1.1.1:8888/index.html#/get/user?id=1234”这个地址按照协议、IP、端口、路径、路由、参数解析拆分开来?

@Test
public void testReg(){
    String str = "http://10.1.1.1:8888/index.html#/get/user?id=1234";
    Pattern pattern = Pattern.compile("^([^:?/#]*)?(://([\\w\\.]+)*)?(:(\\d+))?(/([\\w\\./]+))?(#([/\\p{Alnum}]+))?(\\?([^/#]+))?$");
    Matcher matcher = pattern.matcher(str);
    if (matcher.matches()){
        System.out.println("全网址:" + matcher.group(0));
        System.out.println("协议:" + matcher.group(1));
        System.out.println("域名或IP:" + matcher.group(3));
        System.out.println("端口:" + matcher.group(5));
        System.out.println("路径:" + matcher.group(6));
        System.out.println("路由:" + matcher.group(9));
        System.out.println("参数列表:" + matcher.group(11));
    }
}

(3)如何利用正则表达式将“Hello World”变成“Hello Java”?

@Test
public void testReg(){
    String str = "Hello World";
    Pattern pattern = Pattern.compile("(?<=Hello\\s)(\\p{Alpha}+)");
    Matcher matcher = pattern.matcher(str);
    while (matcher.find()){
        System.out.println(matcher.replaceAll("Java"));
    }
}
// 想一想如何中间有两个空格怎么办?

(4)密码长度8-20位,必须包含大写,小写字母,数字,特殊符号的三种及其以上,正则表达式如何写?

@Test
public void testReg(){
    String str = "1234qwer";
    Pattern pattern = Pattern.compile("^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\\W_]+$)(?![a-z0-9]+$)(?![a-z\\W_]+$)(?![0-9\\W_]+$)[\\w\\W]{8,20}$");
    Matcher matcher = pattern.matcher(str);
    if (matcher.matches()){
        System.out.println("匹配");
    }else{
        System.out.println("不匹配");
    }
}
// 下面这个正则表达式代表什么呢
// ^(?![a-z]+$)(?![A-Z]+$)(?![\W_]+$)(?![0-9]+$)[\w\W]{8,20}$

(5)如何查找“Windows98,Windows2000,Windows10, Windows2000”中所有Windows2000的Windows所在的位置?

@Test
public void testReg(){
    String str = "Windows98,Windows2000,Windows10, Windows2000";
    Pattern pattern = Pattern.compile("Windows(?=2000)");
    Matcher matcher = pattern.matcher(str);
    while (matcher.find()){
        System.out.println("匹配命中");
        System.out.println("匹配开始位置为:" + matcher.start());
        System.out.println("匹配结束位置为:" + matcher.end());
    }
}

(6)如何将“9999999999”变成“9,999,999,999”?

@Test
public void testReg(){
    String str = "9999999999";
    Pattern pattern = Pattern.compile("(\\d{1,3})(?=(\\d{3})+$)");
    Matcher matcher = pattern.matcher(str);
    System.out.println(matcher.replaceAll("$0,"));
    /*StringBuilder sb = new StringBuilder();
		while(matcher.find()){
			matcher.appendReplacement(sb,"$0,");
		}
		matcher.appendTail(sb);
		System.out.println(sb.toString());*/
}
// 如果是9999999999.99999 ====>9,999,999,999.99999呢?

// Pattern pattern = Pattern.compile("(\\d{1,3})(?=(\\d{3})+[\\.])");

Reference

https://www.cnblogs.com/study-everyday/p/7426862.html

https://docs.oracle.com/javase/8/docs/api/index.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值