我从“使用正则表达式获取URL的查询参数”学到了什么

1,401 阅读7分钟

在看博客的时候,不经意间看到了一道这样的面试题:如何使用正则表达式从URL上获取查询参数?

自己思考了一下,然后尝试用正则表达式来实现这个功能,结果发现自己对正则表达式还是不够熟悉;于是就顺着这道题目,一步一步的去深挖这里面所涉及到的知识点。

以后会不定期更新“我从xxx学到了什么”系列,也是给自己学到的知识进行一个阶段性的总结吧!

通过这边文章,你可以学到以下知识点(以下所有代码都是基于JavaScript语言)

  1. 正则表达式的修饰符
  2. 正则表达式的贪婪模式与懒惰模式
  3. 正则表达式的分组、分组编号、分组引用
  4. 正则表达式的先行断言/先行否定断言,后行断言/后行否定断言

前提条件

URL上的查询参数可以直接从window.location.search获取,得到的字符串就是以下形式

?key1=value1&key2=value2

所以题目就可以转换成:使用正则表达式从以上字符串获取所有的键值对。

以下所有的匹配字符串都是 '?name=jim&age=20&hobby=basketball'

预期得到的结果

1、name=jim
2、age=20
3、hobby=basketball

第一版

思路:url查询参数部分的字符串格式是固定的:以?开头,后面接key=val,每组键值对以&连接

话不多说,思路有了,就直接上代码

/[?&](.*)=(.*)/g

输出的结果

'?name=jim&age=20&hobby=basketball'

居然整个字符串都被匹配上了,问题究竟出在哪里?

想法是匹配以?&开头的子串,后面跟着key=val,所以写出了 (.*)=(.*),这个按理应该能匹配出name=jim这样的字符串。为什么在第一个(.*)就把后面的整个字符串都匹配上了呢?想要的结果是第一个(.*)匹配到 = 就结束了,也就是key,然后第二个(.*)继续匹配val。之所以给他们都加上()就是想在=前停止匹配,但实际上却没有按照预期得到正确结果。

为什么会出现这种情况呢?于是去百度了以下,结果发现,正则表达式默认元字符,量词是采用贪婪模式

什么是贪婪模式?

贪婪模式会尽可能多的匹配满足条件的字符。

如下,都是会匹配最大长度字符串

.*
.+
.{1,}
.{0,} 

正则表达式元字符,量词默认首先最大匹配字符串,这些量词有:+,*,?,{m,n} 。一开始匹配,就直接匹配到最长字符串。

如下 <h3>abd</h3><h3>bcd</h3> 通过正则表达式:/<h3>.*</h3>/g 进行匹配,得到的结果为:<h3>abd</h3><h3>bcd</h3> 也就是整个字符串都被匹配上了。

知道了正则表达式默认是贪婪模式,那么/[?&].*=.*/g为什么会匹配整个字符串的原因找到了。

问题就出现在.*,由于它会尽可能多的匹配符合条件的字符串,所以它会把整个字符串都匹配上。

这时就有一个问题:有没有办法让它最小匹配呢?

答案是肯定有的,可以使用懒惰模式

什么是懒惰模式

和贪婪模式相反,懒惰模式是进行最小匹配。

如何转换成懒惰模式

很简单,在表示重复字符元字符,后面加多一个?字符即可。

上面的正则表达式如果想最小长度匹配,则可以这样写:/<h3>.*?</h3>/g 那么匹配出的结果为:

<h3>abd</h3>   
<h3>bcd</h3>

分组

在一个正则表达式中,如果给子表达式加上(),就代表这部分是一个分组,整个表达式是第一个分组。

举个栗子

/[?&](.*)=(.*)/g

其中(.*)就是一个分组。

分组有什么用?

一般分组有两个作用:1、在结果中获取特定的分组匹配结果;2、在表达式中进行引用

  1. 在结果中获取特定的分组匹配结果

    执行regex.exce(str)时,得到的结果是一个数组;如下

    假如要匹配一个年份的年月日,一个简单的正则可以是这样的

    /(\d{4})-(\d{2})-(\d{2})/g
    

    匹配字符串1993-01-01得到的结果如下

    [
        "1993-01-01",
        "1993",
        "01",
        "01"
    ]
    

    第一个是整个表达式的匹配结果,第二个是第一个分组的匹配结果,依次类推。

    那么问题来了,我怎么知道某个分组是第几个?

    分组的编号规则

    分组可以通过从左到右计算其开括号来编号。整个表达式始终是第一个分组。

    如下表达式 (A)(B(C)) 有四个分组 0 (A)(B(C)) 1 (A) 2 (B(C)) 3 (C) 在执行exec函数时,返回的结果也是按照上面的分组返回结果。 第一个返回结果是0号分组匹配到的字符串;第二个返回结果是1号分组匹配到的字符串;依次类推

  2. 在表达式中进行引用

    在一些场景中,需要匹配和前面某个子表达式匹配结果一样的结果,比如说,需要匹配重复的单词,那么就可以使用分组引用。

    同样举个栗子,匹配重复的单词

    abc abc

    可以通过以下表达式进行匹配

    /\b(\w+)\b\s+\1\b/
    

    \1:引用第一个分组匹配到的结果,上面的栗子就是引用(\w+)得到结果,也就是abc

    需要注意的是,引用分组进行匹配,仅仅是引用,它并不是真正的分组,所以在结果的数组中是不会有这个引用匹配到的结果。

知识点补充

JavaScript正则表达式的修饰符
  1. g:全局匹配
  2. i:忽略大小写
  3. m:多行匹配
  4. s:使用dotAll模式匹配
  5. y:“粘连”匹配

以上修饰符,如果有不理解的,可以去ES入门教程-正则表达式学习下,这里就不再赘述了。

JavaScript正则表达式的元字符

不熟悉的可以去菜鸟教程学习下,这里就不再赘述了。

第二版

经过第一版的惨痛失败,意识到了要使用懒惰模式进行匹配,所以改进以下得到第二版表达式

[?&]?(.*)?=(.*)?

输出结果

1、?name=
2、jim&age=
320&hobby=

一共匹配到了三个,但结果还是不是我预期的,

问题在哪里呢?

[?&]?:匹配上的字符串以?或&或都没有开头

.*?:匹配任意字符串,匹配数可以使0或者1,也就是说可以是什么都不匹配

.*?=.*?:第一个.*?会匹配到=前的字符串,而第二个.*可以什么都不匹配,因为它是懒惰模式

而且从预期的结果来看,在匹配结果中,并不需要?&。仅仅是想匹配?或&后面的ke=val。

这时候,后向断言就可以发挥作用了。

什么是反向断言

这个名字读起来就很别扭,不用去理解它的具体含义,权当是一个名字就行。

语法

(?<=exp)x

断言部分在()内,它的作用是匹配满足x的字符串时,校验x前面的字符串是否满足exp,如果满足,则匹配,否则不匹配。

举个具体的栗子

只匹配在#后面的数字

const reg = /(?<=#)\d+/g
const str = '#90abc'

str.match(reg)
// [90]

反行否定断言

语法

(?<!=exp)x

它的作用是匹配满足x的字符串时,校验x前面的字符串是否满足exp,如果不满足,则匹配,否则不匹配。

举个具体栗子

不匹配#后面的数字

const reg = /(?<!#)\d+/g
const str = '#90abc$100edf%102ooo'

str.match(reg)
// [0, 100, 102]

正向断言

语法

x(?=exp)

它的作用是匹配满足x的字符串时,校验x后面的字符串是否满足exp,如果满足,则匹配,否则不匹配。

举个具体栗子

只匹配百分号前的数字

const reg = /d+(?=%)/  
const str = '90%'

str.match(reg)
// [90]

正向否定断言

语法

x(?!exp)

它的作用是匹配满足x的字符串时,校验x后面的字符串是否满足exp,如果不满足,则匹配,否则匹配。

举个具体栗子

只匹配不在百分号前的数字

const reg = /d+(?!%)/g  
const str = '100$'

str.match(reg)
// [100]

终版

又多了一件厉害的装备,现在是时候祭出最终版的表达式了

/(?<=[?&])[^&]*/g

输出结果

1、name=jim
2、age=20
3、hobby=basketball

(?<=[?&]):后向断言,匹配后面的表达式之后,再次进行验证,如果都满足,则匹配

[^&]*:匹配不是&的任意字符

总结

  1. 默认匹配模式是贪婪模式,如果要转换成懒惰模式,则在元字符或量词后加 ?
  2. 分组编号规则,设置分组后的到的匹配结果,以及分组的两个使用场景
  3. 断言可以额外新增匹配条件,断言内匹配上的表达不会出现在最终匹配结果中