八道题带你看懂正则表达式!

1,039 阅读8分钟

前言

无论是写代码还是脚本,当我们要处理字符串或者提炼重要信息的时候,正则表达式都可以是我们的好帮手。

不过很多同学都有一种这样的感触,正则 = 天书 ,比如下面的邮箱表达式:

\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*

不知道你有没有晕,反正第一次我看的时候,这啥玩意!

于是,当我们要使用正则的时候,第一选择肯定是网上复制已有的正则表达式,网上没有?不好意思,只能选择无脑一把梭的方法。

表情包

比如我在写脚本的时候,要获取当前 maven 库中最新版本的信息,都是从 maven-metadata.xml 获取 <latest> 标签 :

<metadata>
    <groupId>QDReader.QDAarCenter</groupId>
    <artifactId>QDUI_Component</artifactId>
    <versioning>
        <latest>0.0.10</latest>
        <release>0.0.10</release>
        <versions>
            <version>0.0.1</version>
            <version>0.0.2</version>
            <version>0.0.3</version>
            <version>0.0.4</version>
            <version>0.0.5</version>
            <version>0.0.6</version>
            <version>0.0.7</version>
            <version>0.0.8</version>
            <version>0.0.9</version>
            <version>0.0.10</version>
        </versions>
        <lastUpdated>20210817095518</lastUpdated>
    </versioning>
</metadata>

之前,我是先获取 <latest> 那一行,再使用 <> 做切割,最后取切割后的第三条,步骤属实有点多。

而有了正则,通过 (?<=latest>)[\d\.]+ 就可以获取到我想要的内容!

先分享给大家一个学习正则表达式的宝藏网站:

image.png 这个网站可以输入正则表达式和你想验证的文本,并将匹配的内容高亮,甚至会告诉你为什么会这么匹配,当然还有一些其他小技巧:

网站地址:regex101.com/r/lL3C8c/2

目录

目录

一、如何匹配字符

方括号 [] 的使用频率很高,括号中的内容是用来匹配的,多个内容是或的关系,匹配其中的一项就算匹配成功。

比如,我们使用 [World]W 可以匹配、o 也可以匹配,也就是说,World 它们是或的关系,匹配其中的一项就行,比如我们来验证老伙伴 Hello World,高亮部分就是匹配内容:

image.png 注意一下,匹配结果中灰色的点可不是点号,而是空格,可能这个网站只是为了让空格显著一点儿~

常用的表达式如下:

符号解释
[World]匹配 World,可以替换成我们想要的任何字符
[0-9]匹配数字
[a-z]匹配小写字母,如果匹配大写字母,需要使用 [A-Z]
[\s\S]匹配所有,\s 匹配所有空白符,包括换行,\S 匹配非空白符,不包括换行
[\u4e00-\u9fa5]匹配所有汉字

既然有想匹配括号中的内容,自然也就有了不想匹配括号中的内容。如果我不想匹配 [World] 中的任何一个字符,怎么办?

简单,使用 ^ 符号,在 [] 中使用 ^ 就是取反的意思,上面的内容就变成了 [^World],验证一下 Hello World 就是:

image.png

很多情况下,仅仅匹配字母肯定不够,怎么匹配大小写字母加数字呢?

加一起就行,[a-zA-Z0-9] 就表示匹配大小写字母和数字,那有没有更简单的写法呢?

还真有,\w 表示 [a-zA-z0-9_],不过比上面的内容多了一个下划线,问题不大。除此以外,\d 表示匹配所有数字,. 就厉害了,它匹配出换行符(\n\r)的任何单个字符,整理一下:

符号解释
[a-zA-Z0-9]匹配单个大小写字母和数字
\w等价于 [a-zA-Z0-9_],匹配单个字母数字和下划线
.匹配除换行符之外的任何一个单个字符
\d匹配任何单个数字

上面讲了这么多,不知道大家有没有发现,我们去查看匹配结果的时候,发现一次只能匹配一个值,比如我用 [am] 去匹配 I am JiuXinDev,发现结果是 am,而不是 am

image.png

因为 [] 只能匹配一个字符,下面我们再看如何匹配数量。

二、如何匹配数量

正则表达式常常用 {} 进行数量匹配,用法是:

符号解释
{n}匹配 n 次,例如 a{2} 可以匹配 aa,但是不能匹配 a
{n,m}匹配 n 到 m 次,例如 a{1,2} 既可以匹配 aa,也可以匹配 a
{n,}匹配至少 n 次,也就是 n 到正无穷,例如 a{2,},除了单个 a,其他多少个连续 a 都可以匹配

这个时候,我们就可以匹配 I am JiuXinDev 中的 am 了,如果你仅仅想匹配 am 这个单词,文本字符 am 就可以搞定:

image.png

啊,这也太简单了,如果我们还想匹配 mac 中的 ma 呢,文本字符 am 就没有作用了,可以再加一个匹配项,中间使用 | 隔开,像这样 ma|am

但再匹配 aamm 呢?我们刚刚学的 {} 就派上了用场,可以通过正则表达式 [am]{2} 去匹配 amma

来做个练习

题目1:怎么从一个句子中找出 2-5 个字母的单词?提醒一下,单词与空格之间的边界可以用 \b 表示

字母对应着 [a-zA-Z],数量限制用 {n,m},2-5个单词的限制对应着 {2,5} ,加上边界,就是 \b[a-zA-Z]{2,5}\b

image.png

限定多少次才能匹配成功的叫做限定符,包括上面介绍的 {} 系列,还有一些简单版本的限定符:

符号解释
?匹配0或1次,等价于{0,1}
+匹配1到无穷次,等价于 {1,}
*匹配0到无穷次,等价于 {0,}

获取标签内容是常有的事儿

题目2:给定<groupId>QDReader.QDAarCenter</groupId>, 如何提取 <groupId></groupId>

还记得上面介绍过的万能匹配符 . 吗?我们只要保证必须以 < 开头 和 > 结尾,可以写成 <.*>

image.png

这结果不对啊,并不是我想要的匹配标签

image.png

别急,再加个 ?,写成 <.*?>

image.png

一个 ?,涉及到了一个贪婪非贪婪的知识,简单来说:

  • 贪婪:尽可能多的匹配
  • 非贪婪匹配:尽可能少的匹配

这么说还有点似懂非懂,拿上面的例子来讲,<groupId><groupId>QDReader.QDAarCenter</groupId> 都以 < 开头,以 > 结尾,如果是贪婪匹配,它的匹配结果是 <groupId>QDReader.QDAarCenter</groupId>,贪婪嘛,能匹配多的,绝不匹配少的;如果是非贪婪匹配,它有两个匹配结果,<groupId></groupId>

本质上,+* 都是贪婪匹配,? 则是非贪婪匹配

三、如何匹配位置

定位符通常由下面这几种:

符号解释
^匹配以指定字符串开始的位置,可以指定一行文本或者整段文本以指定字符开始
$匹配以指定字符串结束的位置,可以指定一行文本或者整段文本以指定字符结束
\b单词边界,字与空格的位置
\B非单词边界匹配,指字符与字符之间的边界
\b 在上文已经用过,可以定位一个单词的开始和结束。^ 用在 [] 中进行字符匹配的时候,是取反的意思,我们也可以用来定位。

重回本文开头的 maven-metadata.xml 文件:

题目3:如何获取以 <version> 开头的该行所有内容?

从上面的 maven-metadata.xml 可以看出,<version> 标签前其实是有若干空格符号的,可以用 \s* 表示,结合定位符 ^,整个正则表达式可以写成 ^\s*<version>.*

image.png

上面的解决方法还可以使用 $ 解决,即匹配以 </version> 结尾的所有行,正则表达式为 .*</version>$

在一些情况下,^$ 还可以放在一起使用,例如 ^JiuXin$ 就可以用来表示匹配只有内容为 JiuXin 的行。

四、如何匹配多内容

多个选项的匹配一般使用选择来完成。

1. 圆括号

敲黑板,选择中最重要的就是圆括号 ()

符号解释
()可以将所有的选项放在括号里面,用 | 隔开,匹配多个选项中的一项就算成功。() 会将分组捕获,() 内匹配到的每一个词都会被放入缓存,通过数字查看对应的缓存

这么听有点晕,我们挨个介绍一下。前半句听着有点像方括号 [] 的意思,多个选项,匹配其中的一项就算完成匹配,不过它们的区别是:

  • []:每个选项是一个字符,也只能匹配一个字符。
  • ():每个选项可以是一个字符,也可以是一个表达式,功能更强!

比如,(am|\d+) 就表示匹配 am 或者不定数数字量的字符串。

巩固一下:

题目四,从给定的语句中,匹配以 a 和 b 开头的单词?

提取单词用 \b,以 a 开头的单词对应着 a[a-zA-Z]*,匹配 a 或 b 可以用上面的选择表达式,结合一下就是 \b(a[a-zA-Z]*|b[a-zA-Z]*)\b

image.png

如果你注意到了匹配信息,会发现有点不一样的东西:

image.png

通常的匹配只有匹配结果 Match,这里却多了显示信息 Group,没错,它就是我们上面介绍的缓存,使用 \n 可以引用第 n 个缓存。

我们可以使用这个缓存做点不一样的事,比如 ([a-zA-Z])\1 表示匹配两个连续的字符。

巩固一下:

题目五:给定的语句中,匹配两个连续的单词。

表示一个单词可以用 \b\w+\b,因为要使用缓存 \1,所以得加上一个括号,组合在一起就是 (\b\w+\b)\s\1

image.png

使用缓存这种操作我们叫做反向引用

2. 非捕获元

上面提到了,使用 () 匹配的结果都会被缓存,而在括号中使用 ?: 就会消除缓存,非捕获元一共有如下几个:

符号解释
?:在选择 () 中使用,去除缓存
?=exp1(?=exp2) 匹配以 exp2 结尾的 exp1
?!exp1(?!exp2) 匹配不以 exp2 结尾的 exp1
?<=(?<=exp2)exp1 匹配以 exp2 开头的 exp1
?!=(?!=exp2)exp1 匹配不以 exp2 开头的 exp1

在介绍定位符的时候,我们介绍过 ^$,它们可以匹配整段和行开始和结束,也可以直接使用文本字符去匹配文本的开始和结束,但总觉得差点意思!

image.png

那这里的 ?=?<= 有什么不同呢?

非捕获元 ?<=?!=,顾名思义,指的匹配部分以指定内容开头或者结束,匹配部分不包括指定内容。比如 (?<=he)\w+ 匹配以 he 开头字符串,但匹配结果却不包含 he

image.png

还记得一开始的的题目吗?

题目6:如何获取 maven 库中最新库的版本信息?(获取 <latest>0.0.10</latest> 中间的 0.0.10)

有的版本号会包括字母和 -,所以版本号正则可以概括成 [\w\.-]+,加上以 <latest> 开头,可以写成 (?<=<latest>)[\w\.-]+

image.png

再来练习以什么结尾:

题目7:如何获取QQ邮箱里面的QQ账号?

首先,QQ邮箱一般以 qq.com 结尾,QQ账号一般5-11位数,第一个数字不是0(其他限制暂时没有想到),对应 [1-9]\d{4,10},两者结合就是 [1-9]\d{4,10}(?=@qq\.com)

image.png

到这里,我们应该可以看懂大部分的正则表达式了~

五、如何按优先级匹配

正则表达式从左往右计算,但是,它里面的各种符号也是有优先级的,由高到低如下:

符号解释
\转义字符
(), (?:), (?=), []圆括号和方括号
*, +, ?, {n}, {n,}, {n,m}限定符
^, $, \ 任何元字符、任何字符定位点和序列
|或操作

就跟我们写代码一样,运算符也有各种优先级,不过难度不大!

实操一下:

题目8:正则表达式 ((\d)([a-zA-Z]{2}(\d)))\1\2\3\4 和字符串 7ac87ac87ac88 可以匹配吗?如果可以,请输出它们的 \1\2\3\4

这个正则表达式看着很复杂,好在它并没有使用 *+ 这两种限定符,所以匹配的结果的长度是一定的,之后我们按照优先级来看。

最外层是一个大括号,因为它是从左往右的第一个括号,所以它匹配的结果 \1

去除外层的括号,是 (\d)([a-zA-Z]{2}(\d)),还是从左往右,接下来先匹配左边的 (\d),所以它是 \2

去除刚刚左边的 (\d),还有 ([a-zA-Z]{2}(\d)),最外层又是一个括号,跟第一种情况一样,它就是 \3

最后还剩一个右边的 (\d),它是 \4

仔细看一下整个表达式 (\d)([a-zA-Z]{2}(\d)),两边两个数字,中间是两个字母,一共四个数。\1 等于整个表达式,\2 + \3 等于 \1\4 是整个表达式最后一个数字。

image.png

所以最终结果是匹配的,匹配部分看上图就好。

下期再见

不会正则的时候,总觉得正则难,一顿操作下来,发现正则也就那么多东西。

希望看完这篇文章的你,也能很快掌握正则!

感谢阅读,下期再见 👋

参考文章:

《菜鸟正则教程》
《正则表达式括号的作用》