This page looks best with JavaScript enabled

复习正则表达式

 ·  ☕ 9 min read

因为一些开发需求需要用到正则表达式,但是实际开发中每用一次正则就重看一次这样的事情实在是羞愧至极,所以在这里再进行一次正则的复习。以求能够较全面地理解正则表达式。

好了,那么什么是正则表达式呢?正则表达式,个人目前的理解是遵照一定的限定规则进行字符(串)匹配。百度百科的说法:使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。通常被用来检索、替换那些符合某个模式的文本。看来,概念理解还可以,下面进行正则的复习阶段。

基础

使用正则表达式可以直接通过生成一个字符串作为最基础的匹配。在JavaScript中就是myReg = /Will/;,使用这个正则对象执行exec操作就可以把字符串中的匹配部分拿出来。可是,有时候,不一定输入的就是大写的Will,有可能用户输入的是“Hi,will!”,这个时候,使用正则的i参数可以忽略大小写进行匹配。即myReg = /Will/i;

好了,上面的正则表达式可以匹配到需要的单词了,然而真的是这样么,假如有一个用户叫JMwill,现在使用meReg来测试一下这个句子“Hi,JMwill”,毫无疑问,这个表达式一样可以找到will,当然如果不管什么情况下你只要will,这个表达式是可以用的,可是大多数时候需要的是精确匹配,那么,这里可以用什么办法来进行匹配出精确的will呢?在这里需要引入元字符这个新的东西,

元字符

正则表达式由普通字符与元字符组成,可见元字符就是正则表达式中的重中之重,那元字符是什么?元字符其实就是具有特殊含义在正则表达式中用于进行规则制定的字符。上面说到,myReg会对任意含有will的词组进行匹配,无法找出单独的will单词,那么,在这里我们就需要加上一个\b作为限定,\b的含义是匹配一个单词边界,也就是指单词和空格(符号)间的位置,在这里需要知道的一个事情是:正则表达式的匹配有两种概念,一种是匹配字符,一种是匹配位置。这里的\b正是用于匹配位置的,所以现在myReg变成了myReg = /\bwill\b/i;这里是元字符的表格。下面会列出一些常用的元字符。

代表数量:
+   --代表前面的子表达式(内容)至少出现一次或多次出现(大于等于1次)
*   --代表前边的内容可以连续重复使用任意次(零次或更多次)以使整个表达式得到匹配
?   --代表前边的内容可以重复零次或一次以使整个表达式得到匹配
{}  --有三种使用方式,第一,{number},表示仅匹配number次前面的子表达式。
      第二,{number,},表示至少匹配number次前面的子表达式。
      第三,{numberOne,numberTwo},表示最少匹配numberOne次且最多匹配numberTwo次前面的子表达式

匹配字符:
.   --代表匹配除了换行符以外的任意字符一次。
\d  --匹配0~9中的一位数字
\s  --匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]
\w  --匹配包括下划线的任何单词字符(Unicode字符集,即包括中文)。
[]  --用于定义一个字符集合,匹配所包含的任意一个字符。如:[xyz]可以匹配zoo中的z,或者定义一个范围[0-9],匹配0~9任意一个数字。

匹配位置:
\b  --匹配一个单词边界,指单词和空格(符号)间的位置
^   --匹配输入字符串的开始位置
$   --匹配输入字符串的结束位置

分支条件

在正则表达式中可能需要进行一些逻辑选择,这个时候,可以使用|符号进行或选择,这个元字符作用是:将两个匹配条件进行逻辑“或”(Or)运算,即如有一个这样的表达式:(JMwill|will),这个将会匹配“Hi JMwill!”或者匹配“Hi will”,但是在使用|时需要注意的是它进行的是短路求值,这将会对一些表达式造成影响。如:“z|food”能匹配“z”或“food”或"zood"而“(z|f)ood”则匹配“zood”或“food”。

反义

当有一些需求如:需要一些除某某某以外的数据,这个时候,使用正常的表达式来进行定义可能会变得麻烦或者无法达成,这个时候可以使用反义,在正则表达式中定义一般的反义,可以使用[^some chart you need],在方括号里面需要在最靠前的地方添加一个^符号,这个符号在方括号的最前面才有意义,代表‘除了…’的意思。同时,在正则表达式中也提供了一些常用的反义:

\W  --代表匹配任意不是字母,数字,下划线,汉字的字符
\S  --代表匹配任何可见字符。
\D  --代表匹配一个非数字字符。
\B  --代表匹配非单词边界位置。

分组

在正则表达式中需要进行多个字符或多个内容匹配应该怎么进行呢?在这里,可以使用分组(子表达式)来实现需求。通过使用()来进行分组的定义,将需要的内容包括在里面,然后就可以使用元字符来对分组进行限制了。

后向引用

这里,将会讲述后向引用的基本内容,后向引用需要跟分组配合一起使用,通过后向引用可以重复搜索前面某个分组匹配的文本。一般的分组使用\n来进行匹配,n代表的是分组的组号,即要匹配第一个分组就使用\1来进行匹配。来个例子说明一下:testReg = /(will|JMwill)\s+\1/;这个表达式就会匹配文本“Hi JMwill JMwill, where is Lucas?”。

当需要对分组进行命名的时候,可以使用(?<name>exp)来进行,?<name>(也可以替换为:?'name')是进行命名所必须的,name处替换为想要的名字。而exp就是正常想要匹配的内容。当需要进行分组又不想要让分组具有引用的时候,可以使用(?:exp)这样的写法,?:代表不捕获匹配文本,也不分配组号。还有一些特定用途的语法,不过这是进阶问题了。

零宽断言

零宽断言,嗯,好高级的名字,乍一看,我都吓怕了。其实,零宽的意思是没有宽度,那什么是没有宽度的呢?在正则表达式里,位置是没有宽度的,因此这个用于指定位置。那断言呢?断言也就是指满足一定的条件。所以,零宽断言就是满足一定条件的位置。下面进行解释:

(?=exp)这个叫做零宽度正预测先行断言,还是深奥的名字,它的作用是:在自身出现位置的后面能匹配表达式exp,这个自身是指分组前的表达式匹配出来的内容如:nameReg = /\b+w(?=will\b)/;,这里的式子能够匹配字符串“name is JMwill”中的JM出来。

(?<=exp)这个叫做零宽度正回顾后发断言,它的作用跟上面的相反:用于在自身出现位置的前面能匹配表达式exp,这个自身跟上面一样。

负向零宽断言

这里的负向,顾名思义就是相反。即这个位置不能去匹配分组内的表达式exp。

(?!exp)这个叫做零宽度负预测先行断言,即在自身出现位置的后面不能匹配表达式exp,自身指分组前的表达式匹配出来的内容。这里使用位置来进行判断,不会出现使用元字符判断出现跳过,或误判的情况。如使用[^exp]来进行判断,有可能会把不希望要的空格、标点等包括进去。

(?<!exp)这个叫做零宽度负回顾后发断言,作用:在自身出现位置的前面不能匹配表达式exp,自身指分组前的表达式匹配出来的内容。

注释

有时候,我们可以对正则表达式添加一些注释,(what?Are you kidding me?),没错,这是真的。可以通过使用(?#comment)来添加注释,comment就是注释的内容。或者可以通过忽略正则表达式内的空白符,以这样的方式来写注释:

(?=     #comment
<(\w+)> #comment
...
)

这种方式可以方便理解表达式的内容。

贪婪匹配与懒惰匹配

正则表达式是非常勤劳的,它在对进行要求的匹配的同时尽可能地匹配最长的内容,当你需要匹配重复的字符时,如:testReg = /ha.*!/;这个式子会对句子“hahahaha!hahahaha!”整个匹配,但是当在*号后面添加一个?号之后,式子就会仅仅匹配前面的“hahahaha!”了,即testReg = /ha.*?!/;。这个添加?的方法对所有重复匹配的元字符都有用。

*?      --尽可能少地重复任意次
+?      --尽可能少地重复一次或多次
??      --尽可能少地重复零次或一次
{n,m}?  --尽可能少地重复n次到m次
{n,}?   --尽可能少地重复n次以上

平衡组/递归匹配

总有些需求是需要将一组成对的符号内的内容匹配出来,如果仅仅是一对,这个问题就好办了,直接写就好。但是,有些时候需要对多个成对且相互嵌套符号进行配对,这时使用简单的方式就无法进行准确的匹配。(由于由于这个主题我理解得也不是很深入,只好借助deerchao的解释以及例子)这个问题可以通过以下语法构造解决:

(?'group')          --把捕获的内容命名为group,并压入堆栈(Stack)
(?'-group')         --从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败
(?(group)yes|no)    --如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分
(?!)                --零宽负向先行断言,由于没有后缀表达式,试图匹配总是失败
<                         #最外层的左括号
    [^<>]*                #最外层的左括号后面的不是括号的内容
    (
        (
            (?'Open'<)    #碰到了左括号,在堆栈上压入一个"Open"
            [^<>]*       #匹配左括号后面的不是括号的内容
        )+
        (
            (?'-Open'>)   #碰到了右括号,堆栈弹出一个"Open"
            [^<>]*        #匹配右括号后面不是括号的内容
        )+
    )*
    (?(Open)(?!))         #在遇到最外层的右括号前面,判断堆栈上还有没有没弹出的"Open";如果还有,则匹配失败

>                         #最外层的右括号

上面的正则表达式每碰到了左括号,就压入一个"Open”,每碰到一个右括号,就弹出一个,到了最后就看看堆栈是否为空--如果不为空那就证明左括号比右括号多,那匹配就会失败。正则表达式引擎就会进行回溯(放弃最前面或最后面的一些字符),尽量使整个表达式得到匹配。

总结

在进行编写简单正则表达式的时候,需要记住的东西不多,这一次复习,通过归纳整理,相信已经对正则表达式了解更深刻一些了。最后的最后,需要补充的是JavaScript中的正则表达式可以添加三个处理选项跟别是:

i       --不区分大小写
m       --多行匹配
g       --全局匹配

参考资料:

Share on

Will
WRITTEN BY
Will
Web Developer