【正则表达式】学习笔记

本笔记基于《正则表达式30分钟入门教程》,是不是真的30分钟就能学会,这要看你的学习能力了,反正我是不行

前言

正则表达式是什么

最常见的地方,填密码的时候,会有一些要求,比如必须包含大小写字母和数字,不能有特殊符号,长度8-10之间,当你没按要求输入,它就会让你重新输入,这里用到的就是正则表达式,我们只需要用^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$就可以判断是否符合要求,看起来很麻烦吧,确实,谁让他可以精准匹配字符串呢

不少软件的搜索功能也支持正则表达式,比如vscode,使用正则能让你快速完成一些任务,比如

  • ^\s*(?=\r?$)\n 可以用来清除空行
  • //[\s\S]*?\n 用来清除单行注释

简单点说,正则表达式就是用来匹配某种特定格式的字符串的。一般的搜索就是,你输入单词baby,那就会所搜出所有的baby,其中也会包括angelababy。其实我只想搜索单独的baby,怎么办呢,一般单独的单词两边都会有分隔符,反正不能是字母,所以我们可以用正则表达式\bbaby\b\b表示所在位置的一侧为单词字符,另一侧为非单词字符

一些常用的正则工具

正则表达式的语法很令人头疼,即使经常使用的人都很难通过一个表达式判断它的功能,推荐两个在线的工具

开始

元字符

元字符是在正则表达式中具有特殊含义的符号或字符,正则表达式本质上就是通过元字符实现字符串精准匹配的
接下来,我讲的所有字符都是元字符,下面只是普通的元字符,后面的字符转义,限定符其实本质都是元字符

代码说明
.匹配除换行符以外的任意字符
\w匹配字母或数字或下划线
\s匹配任意的空白符
\d匹配数字
\b匹配单词的开始或结束
^匹配字符串的开始
$匹配字符串的结束

^说清楚点就是匹配每一行的开始位置, $匹配的是每一行的结尾位置
只要有了^ ,那就只会匹配每一行开头的字符串,而不会匹配每一行中间的,而$就是匹配每一行结尾的字符串,两个结合到一起,常用于单行字符串的匹配

反义

反义一般用上面元字符的大写表示,比如\d匹配任意数字,而\D匹配除数字外的所有字符,其他的也一样

对于字符类,下面再讲,使用^来匹配除方框里给出的字符之外的所有字符

代码/语法说明
\W匹配任意不是字母,数字,下划线,汉字的字符
\S匹配任意不是空白符的字符
\D匹配任意非数字的字符
\B匹配不是单词开头或结束的位置
[^x]匹配除了x以外的任意字符
[^aeiou]匹配除了aeiou这几个字母以外的任意字符

限定符

限定符是跟在其他元字符后面的,用于限定元字符匹配字符的重复次数

代码/语法说明
*重复零次或更多次
+重复一次或更多次
?重复零次或一次
{n}重复n次
{n,}重复n次或更多次
{n,m}重复n到m次

可能有点没理解清楚,那我来举几个例子

  • \d+ :作用是匹配由数字构成的字符串
    \d是匹配数字,+相当于无数个\d,数量取决于什么时候遇到非数字,必须连续
  • \d{1,}:作用和上面一毛一样,也是匹配由数字构成的字符串,只是可以自定义最少有几位,比如\d{3,}表示这个数字至少有三位
  • *相当于{0,}+相当于{1,}?相当于{0,1},后三个只是自定义程度更高,前三个使用更方便

字符转义

当你想搜索元字符本身怎么办,那就在前面加一个\ ,比如说想搜索. ,就需要用\.

字符类

之前说的\w\d\s这些只能匹配任意的字母数字,而不能匹配特定的几个字母或者数字

把你想匹配的装到方括号里,就像[12345]这样,你就能匹配到12345中任意一个数字了,同时你也可以用[1-5]表示
除了数字,其他的字符,字母都可以这样,并且在方括号里不用担心字符转义的问题,[.*+?$]这些都可以直接匹配
并且字符数字可以放在一起,比如[0-9A-Za-z] ,直接连在一起就行,相当于\w的效果

  • 举一个稍微复杂的例子,^[a-zA-Z]\w{5,17}$ ,用正则可视化我们可以看出

    这是一个校验密码的表达式,**以字母开头,长度在618之间,只能包含字母、数字和下划线**,根据`\w`的作用,你应该知道为什么大部分的密码都只支持下划线这一个特殊字符了,这样验证起来方便(我瞎猜的),`\w`重复了517次,加上开头的字母,也就是6~18位
  • 再举一个复杂点的例子,\(?0\d{2}[) -]?\d{8}

    他其实能匹配三种格式的电话号码,比如(010)88886666022-2233445502912345678他们的特点在于前三位数字,有点是括号包围的,有的后面跟着短横,有的什么都没有,这就是?在起作用,?表示重复零次或一次,也就是说?前面的字符可以有,但没有照样可以匹配。?前面是\(,这是由于(也是元字符需要转义,所以这个电话号码有没有括号都是可以匹配的,而后面的[) -]?表示)或者-或者为空,再接8位数字

分支条件

刚才举的第二个例子,可以匹配三种格式的电话号码,但你认真思考一下就会发现,它还会匹配010)12345678或者(022-87654321 这些错误的格式。因为?不会进行判断,前面的字符存不存在都可以。但是我们更多的时候需要进行判断,不存在是什么格式,存在是什么格式。就拿这个例子来说,如果存在( ,那后面也必须是) ,要实现这个我们需要要用分支条件|

这个符号就相当于取并集,两个条件满足一个即可。改写上面的表达式\(0\d{2}\)\d{8}|0\d{2}[- ]?\d{8}

分成了两个分支,一个是有括号的,一个是没括号的。而没有括号的中间可以有一条短横,也可以没有

值得注意的是,分支条件优先匹配左边的条件,只要满足左边的条件,就不会看右边的条件了

分组

之前说的在元字符后面加重复限定符就可以重复匹配这个字符,但是如果想重复匹配一个比较复杂的表达式呢,就需要把这个表达式放在括号()里面

比如常用的ip地址匹配的表达式,每3个数字(最多3个)一段,共四段,中间用.连接。可以看做三段3个数字(最多3个)+1个点,最后一段为3个数字(最多3个)

ip地址简单匹配表达式:(\d{1,3}\.){3}\d{1,3}

当然,这个表达式只是匹配个形式,ip每一段有大小限制,不能大于255,很遗憾正则表达式不能判断数字大小,所以我们只能把3个数字单独分开看,分为3个分支

  • 第一位为2,第二位为0-4,第三位为任意数字
  • 前两位为25,第三位0-5
  • 第一位为0或1或者为空,第二位为任意数字,第三位为空或任意数字

然后将这3个数字的表达式分为一组即可

ip地址完整匹配表达式:((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)

后向引用

上面这个ip地址的分组,其实写了两遍,十分麻烦。有没有什么办法可以直接引用这个分组呢,当然有,这就是马上要提到的后向引用
为什叫后向引用,因为必须要先分组,之后才能引用。分组后,就会默认给这个组一个编号,第一组为\1,第二组为\2 ,以此类推。之后我们可以直接用这个编号来引用这个组 以后我们就可以引用这个组匹配的内容需要注意的是,嵌套的分组,比如上面的ip地址完整表达式,最外层为\1 ,里面为\2
所以我可以将其简化为((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(\2)

我竟然理解错了,这里的引用的并不是这个分组表达式本身,而是这个表达式匹配的内容,也就是它捕获的内容。简单点来说,\b(\w+)\b可以匹配一个单词,而\b(\w+)\b\s+\1\b则会匹配两个相同的单词并且这两个单词用空格隔开,比如hello hello这个形式,所以你应该看出来了吧,引用的仅仅是当前这个分组表达式捕获的内容,而不是表达式本身

你也可以在分组的时候指定一个组名,比如(?<Word>\w+),这就生成了一个叫Word的组,我们可以用\k<Word>来引用,上面这个可以修改为\b(?<Word>\w+)\b\s+\k<Word>\b ,不过这样其实变的更麻烦了

上面两个都是捕获分组,也就是会把匹配的值缓存起来,并会占据一个组号。但大多数时候我们仅仅是想分组而已,不需要引用,那就可以使用不捕获分组,格式(?:hello) ,这样就不会缓存hello,也不会分配组号,匹配效率会高一点

零宽断言

这名字听着很高深,作用相当于^ $ \b这种的,匹配的是一个位置,而这个位置满足一些条件,这个条件就是断言。当然,断言也是用当做动词用,不就是下结论的意思吗,我认定就是这个位置。而零宽就是宽度为零,因为他不会匹配任何字符,匹配的仅仅只是一个位置

  • (?=ing) :叫做零宽度正预测先行断言,也叫做肯定式向前查找,作用是找到一个位置后面满足所给条件。比如\b\w+(?=ing\b) ,匹配的就是以ing结尾的单词的前面部分,Singing匹配的就是Sing 。想完整匹配这个单词,可以用\b\w+(?=ing\b)ing\b
  • (?<=re) :叫做零宽度正回顾后发断言,也叫做肯定式向后查找,作用是找到一个位置后面满足所给条件。比如(?<=\bre)\w+\b ,匹配的就是re开头的单词的后面部分,recycle匹配的就是cycle

断言的作用就是让我们可以准确定位到某个位置,就像^ $ \b一样

负向零宽断言

上面说的零宽断言匹配的就是一个位置,这个位置必须满足所给的条件。那么负向零宽断言中,匹配的位置是必须不满足所给条件。比如说,我想匹配一个单词,要求这个单词不是ing结尾,那我们可以使用\b(\w(?!ing))+\b这个表达式来解决,这就是零宽度负预测先行断言,也叫做否定式向前查找。作用是找到一个位置后面不满足所给条件。格式(?!ing)

关于这个表达式,用正则可视化后,我们可以看出

逻辑是任何字母后面都不能是ing,然后重复。但仔细一想就会发现,ing中的字母后面也不是ing呀,所以这个表达式会匹配到单独的ing 。简单的解决办法是\b((?!ing)\w)+\b ,把\w放在断言后面

现在的逻辑是,任何字符,甚至空白符后面都不能是ing,然后这个位置后面有一个字母,不断重复。
这个很重要,交换顺序后的意义并不相同

同样也有否定式向后查找,也叫做零宽度负回顾后发断言,作用是找到一个位置前面不满足所给条件。格式(?<!re) ,仿造上面的,比如\b(\w(?<!re))+\b ,所用是匹配开头不是re的单词,逻辑类似。自行体会

贪婪与非贪婪(懒惰)

什么是贪婪,举个例子
如果一个字符串1010000000001 ,我们需要匹配101
一般人就会使用1\d+1 ,但是这样你会匹配到1010000000001这整个字符串,这就是贪婪匹配,他会尽可能多的匹配字符。他的原理是当遇到第一个1的时候,会一直找到最后,在从后到前找第二个1 ,可以看出当这样效率会很低,常用于单行匹配
而如果想匹配到101 ,我们就需要使用1\d+?1 ,加一个? ,就变成了非贪婪匹配,也就是懒惰匹配。它的匹配原理是从前往后匹配
对了,最先开始的匹配有最高优先级,所以贪婪匹配也不会匹配到10000000001

在重复限定符后加?就可以变为非贪婪匹配,尽可能少重复,遇到第一个满足条件的就停止匹配。?本身就是重复限定符,表示重复0次或1次,所以也有.??这种形式,至于这有什么作用,我也不知道

举一个复杂的例子,<(\S*?)[^>]*>.*?</\1>|<.*?/> ,这是用来匹配html标签的,正则可视化后

这个表达式分为两个条件分支,可以匹配双标签和单标签
这里面用到的有后向引用,因为包含相同的内容。非贪婪匹配使用了使用了多次,比如.*?/>会在遇到第一个/>的时候停止匹配

注释

我不清楚正则表达式中注释的作用,这样只会把表达式变得更复杂
正则的注释格式(?#注释) ,也是用的小括号
如果启用了忽略空白符选项,那么也可以写成

1
2
3
4
5
6
(?<=    # 断言要匹配的文本的前缀
<(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)
) # 前缀结束.* # 匹配任意文本
(?= # 断言要匹配的文本的后缀
<\/\1> # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签
) # 后缀结束

正则平衡组

正则平衡组十分强大,但也很复杂
不是所有语言都支持平衡组,或者说不是所有语言的平衡组语法都一样,所以我这里就不写了,自己平时也没用过
大部分的教程都基于.Net,这个我不熟悉。想了解的自行学习

总结

正则可以说是每个程序员必学,所以我觉得写这个笔记对自己和大家都有益处。我尽可能的按自己肤浅的理解写出来,相信大家都能看得懂

文章作者: ourongxing
文章链接: https://orxing.top/post/25504e15.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 OURONGXING
打赏
  • 微信
  • 支付宝

评论