在看博客的时候,不经意间看到了一道这样的面试题:如何使用正则表达式从URL上获取查询参数?
自己思考了一下,然后尝试用正则表达式来实现这个功能,结果发现自己对正则表达式还是不够熟悉;于是就顺着这道题目,一步一步的去深挖这里面所涉及到的知识点。
以后会不定期更新“我从xxx学到了什么”系列,也是给自己学到的知识进行一个阶段性的总结吧!
通过这边文章,你可以学到以下知识点(以下所有代码都是基于JavaScript语言)
- 正则表达式的修饰符
- 正则表达式的贪婪模式与懒惰模式
- 正则表达式的分组、分组编号、分组引用
- 正则表达式的先行断言/先行否定断言,后行断言/后行否定断言
前提条件
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、在表达式中进行引用
-
在结果中获取特定的分组匹配结果
执行
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号分组匹配到的字符串;依次类推
-
在表达式中进行引用
在一些场景中,需要匹配和前面某个子表达式匹配结果一样的结果,比如说,需要匹配重复的单词,那么就可以使用分组引用。
同样举个栗子,匹配重复的单词
abc abc
可以通过以下表达式进行匹配
/\b(\w+)\b\s+\1\b/
\1
:引用第一个分组匹配到的结果,上面的栗子就是引用(\w+)
得到结果,也就是abc
需要注意的是,引用分组进行匹配,仅仅是引用,它并不是真正的分组,所以在结果的数组中是不会有这个引用匹配到的结果。
知识点补充
JavaScript正则表达式的修饰符
- g:全局匹配
- i:忽略大小写
- m:多行匹配
- s:使用dotAll模式匹配
- y:“粘连”匹配
以上修饰符,如果有不理解的,可以去ES入门教程-正则表达式学习下,这里就不再赘述了。
JavaScript正则表达式的元字符
不熟悉的可以去菜鸟教程学习下,这里就不再赘述了。
第二版
经过第一版的惨痛失败,意识到了要使用懒惰模式进行匹配,所以改进以下得到第二版表达式
[?&]?(.*)?=(.*)?
输出结果
1、?name=
2、jim&age=
3、20&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
(?<=[?&])
:后向断言,匹配后面的表达式之后,再次进行验证,如果都满足,则匹配
[^&]*
:匹配不是&的任意字符
总结
- 默认匹配模式是贪婪模式,如果要转换成懒惰模式,则在元字符或量词后加
?
- 分组编号规则,设置分组后的到的匹配结果,以及分组的两个使用场景
- 断言可以额外新增匹配条件,断言内匹配上的表达不会出现在最终匹配结果中