正则表达式
本次分享,帮助大家整体过一遍正则表达式的常用基础知识点,能够在以后的工作中,举一反三,不必害怕。
从最简单开始——尽可能严谨的给出匹配规则
现在有一串坚果pro在微信下的useragent信息,通过大量的观察,可以发现,所有的坚果pro都有一个OD标识,且后面紧跟一串数字作为他的具体型号。1
Mozilla/5.0 (Linux; U; Android 7.1.1; zh-CN; OD105 Build/NMF26F) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.1.3.993 Mobile Safari/537.36
自然的,立刻想到了一个最简单的正则表达式/OD/。
问题马上就来了,考虑如下正则1
Mozilla/5.0 (iPOD; CPU iPhone OS 6_0_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A523 Safari/8536.25
使用/OD/竟然也测试通过了?但是这一条,是早期ipod toch的ua信息。
为了避免类似的情况,我们的正则表达式至少应该写成这样/\bOD\d+/,其中\b匹配一个单词的开头或者结尾(注:它是一个位置,不是单词和单词之间的空格),\d代表数字,+代表出现的次数为至少出现1次。其中\d\、\b和+,被称为元字符,其中+表示数量,有时候也被称之为限定符。接下来详细展开这2个类型。
元字符
除了\d\和\b,常用的元字符如下所示
| 元字符 | 说明 |
|---|---|
| . | 匹配除了换行符外任意字符 |
| \s | 匹配任意的空白符 |
| \d | 匹配数字 |
| \w | 匹配字母、数字、下划线、汉字 |
| \b | 匹配单词边界 |
| ^ | 匹配字符串的开始 |
| $ | 匹配字符串的结束 |
仍然以上面提到的useragent为例1
Mozilla/5.0 (iPOD; CPU iPhone OS 6_0_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A523 Safari/8536.25
.匹配除了换行符的任意字符,如果没做全局匹配,则匹配结果为整个ua信息的首字母M,单独使用没啥意义\s匹配了ua信息中的空格,如果没做全局匹配,则匹配结果为Mozilla/5.0后面的空格\d匹配数字,上面有提过,如果没做全局匹配,匹配结果为5\w匹配字母、数字、下划线、汉字,如果没做全局匹配,匹配结果为M\b匹配边界,一般是配合别的一起使用,比如\bA,如果没做全局匹配,匹配结果为AppleWebKit的首字母A^匹配字符串的开始,一般是配合别的一起用,比如^\w,匹配结果为整个ua信息的首字母M$匹配字符串的结束,一般是配合别的一起用,比如\w$,匹配结果为整个ua薪资的最后一个数字5
注: $和^都是为了写出更精准的匹配条件。
道理我都懂,看了不会用。
举个栗子,判断一个串,是不是手机号
- 粗糙一点,
\d{11},一个数字重复11次就行了。但是形如12345678987654321或者abc12345678987645sdf也会通过,怎么办? - 加个限制
^\d{11}$,一个数字重复11次,且必须是数字作为串的开头,数字作为串的结尾。但是01234567898也会通过,有0开头的手机号么?没有。 - 加个限制
^1\d{10}$,是不是好多了?到目前为止,我们先写到这里
限定符
| 限定符 | 说明 |
|---|---|
| * | 重复0次或者更多次 |
| + | 重复1次或者更多次 |
| ? | 重复0次或者1次 |
| {n} | 重复n次 |
| {n,} | 重复n次或者更多次 |
| {n,m} | 重复n到m次 |
限定符的使用比较简单,仍然以上面提到的useragent为例
1 | Mozilla/5.0 (iPOD; CPU iPhone OS 6_0_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A523 Safari/8536.25 |
比如我们要提取ua中最后的Safari/8536.25,我们假定Safari/后面的数字是版本号,且小数点不是必须的。
- 最粗糙的,可以写
Safari\/\d+但是小数点会被忽略 - 改造下,可以写
Safari\/\d+\.\d+但是没小数点的就挂了 - 再改造下,可以写
Safari\/\d+\.?\d* - 同样的,如果确定是完整的一个串,且不与正文混合,
Safari\/\d+\.?\d*,但是问题又来了,Safari/12.也能通过? - 原来,
.和后面的数字,必须同时出现,所以Safari\/\d+(\.\d+)?这样是不是好多了?
转义
你应该发现了,跟元字符一样的匹配,需要使用\反斜杠来转义。比如我们刚刚Safari/8536.25中的/在匹配时,需要写为\/,中间的小数点,在匹配时,也要写为\.。其他元字符同理。
类和条件
如果单独想匹配出数字、字母、空白等,有元字符已经可以直接帮助获取,有木有自己建立的集合呢?
考虑我们刚刚所写的手机号匹配^1\d{10}$,这里限制了第一位数,必须是1,那么,第二位数字是不是也可以限制下,保证匹配更加准确?目前已知的第二位有35678,我们可以使用[34678]来表示匹配其中的一个。
更改后的正则为^1[34678]\d{9}$,这样就准确多了。
对于连贯性的数字或者字母,[]内甚至可以直接写[0-9],等价于\d,同样的,字母的匹配可以写[a-z][A-Z],甚至可以组合使用[0-9a-zA-Z],如果再加上一个下划线,[0-9a-zA-Z_],等价于\w。
但是,pm不要脸,换需求了
pm要求,匹配15开头的号段,结尾必须是0,匹配13开头的号段,结尾必须是1。
本能反应,我们噼里啪啦甩出去^1[35]\d{8}[01]$,但不幸的是同样的,15开头的号段匹配了1结尾,13号段也匹配了0结尾。
最粗糙的写法,^13\d{8}[0]$|^15\d{8}[1]$,看着像不像2个正则表达式给拼起来了?中间用了个 |分割了下?你会优化么?
这里|即为正则表达式的条件。同样的场景,由于javascript内,对正则的支持有限,无法满足对括号的匹配,所以对于简单的括号情况,需要用条件语法去处理。但是,对于复杂的括号需求,还是建议使用递归,进行处理。
逆匹配
上面,提到了常用的元字符,以及自定义的类,还有一种常用的需求,求一个类的倪匹配,比如,需要匹配除了数字以外,其他所有的情况。这时候需要用逆匹配来进行处理,规则非常简单:
| 元字符 | 说明 |
|---|---|
| \S | 匹配任意的非空白符 |
| \D | 匹配非数字 |
| \W | 匹配非字母、非数字、非下划线、非汉字 |
| \B | 匹配非单词边界 |
| [^abc] | 匹配非abc的其他所有 |
特殊的地方只有1处,就是对自定义类的逆匹配,需要一个[^]进行区分,常用的需求为,匹配一个xml标签,通常这么写<[^\s>]+>。
分组与引用
对于分组,上面应该已经注意到,我们已经很顺其自然的写出了一个形如Safari\/\d+(\.\d+)?的正则,这里面的()即分组。分组非常有用,比如对单个字符的数量控制,可以使用限定符控制,对于一组字符串的处理,加个()就可以了。比如上面刚刚所写的(\.\d+)?就是小数点和数字在一个括号内,后面的限定符?,表示必须同时出现。
对于分组,更多的应用场景,其实是引用。考虑一个需求,有一串电话号码<010>85600596,我们需要匹配他的区号,他的区号,都会以尖括号包括
- 粗糙的,我们写出了
<\d{3,4}>\d{7,8},因为区号有3-4位数,电话号码有7-8位数。但是这个正则在使用的时候,只能'<010>85600596'.match(/<\d{3,4}>\d{7,8}/),怎么能在确认了是电话号码的情况下,拿到区号? - 增加分组
<(\d{3,4})>\d{7,8},这样,let res = '<010>85600596'.match(<(\d{3,4})>\d{7,8})就能在结果内,res[1]拿到我们的区号010。
对于引用,还有一种使用方式,叫向后引用。考虑一个字符串,'abc',它的引号,有可能是'也有可能是",做匹配怎么处理?
- 仍然最粗糙的开始,写出了
['"][a-z]+['"],不幸的是,'abc"这样的字符串竟然也能通过。 - 这里考虑使用向后引用,
(['"])[a-z]+\1,考虑这里的\1,意思是第一个分组。如果有多个括号,这个反斜杠的数字持续累加,对应的是从左到右的括号顺序,就是对应的分组。
为了加深印象,我们看下一个偏实际的需求:
1 | div { |
我们需要对background-image后面的url内容进行收集。但是,url的内容,可以是单引号,也可以是双引号。根据我们上面提到的知识点,很容易想到
- 首先可以得出
url\((['"])([^\s'"]+)\1\) - 观察可以发现,引号可以单双,但是也可以完全不要引号,还记得上面提过的限定符么?
url\((['"])?([^\s'"]+)\1\)
不分组捕获和零宽断言
为啥给这2个放到一起,因为他们的表达式很像,方便记忆。。
首先看下不分组捕获,考虑上面提到的css内容1
2
3div {
background-image: url('https://a.com/b.png');
}
现在不但要考虑url的内容,还需要考虑background-image
- 粗糙的,
background-image\s*:\s*url\((['"])?([^\s'"]+)\1\),因为冒号前后的空格个数,是随意的。 - 但是不难发现,在编写
css时,会有连写属性,比如background: url('https://a.com/b.png'),-image并不是必须,所以正则改写为background(-image)?\s*:\s*url\((['"])?([^\s'"]+)\2\) - 问题来了,刚刚我们
\1的地方,现在改为了\2,但是\1并没有什么卵用,等于浪费了一个标号数字,怎么解决? - 不分组捕获的作用就来了,写法非常简单,
(?:)即可。 - 修改后的正则
background(?:-image)?\s*:\s*url\((['"])?([^\s'"]+)\1\),这样,虽然前面有一个括号,但是并没有给他赋予\1的编号。
零宽断言的例子,其实,在开始的时候已经提过了。零宽,顾名思义,匹配的是个位置,而不是实际的内容,也就是说^、$、\b都算是零宽断言。
考虑更复杂的需求,还是开头的那一行useragent1
Mozilla/5.0 (Linux; U; Android 7.1.1; zh-CN; OD105 Build/NMF26F) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.1.3.993 Mobile Safari/537.36
我们分析发现,在OD105这个手机型号之前,总是有一个系统语言的标识,我们想得到他。
- 通过上面掌握的内容,可以轻松得到
([\w-]+);\s*\bOD\d+\b,这样分组\1就是想要的内容。 - 有木有更优雅的方法?
[\w-]+(?=;\s*\bOD\d+\b),注意这里的(?=),代表的意思为零宽正预测先行断言,名字很屌,也相当拗口,只需记住,匹配以XXX为结尾的串,但是不捕获它,就ok了。
同样的,有正预测,就有负预测,负预测的意思就是,匹配不以XXX为结尾的串,但是不捕获它。应用场景也很常见,这里举一个简单的例子,已知一个生日,匹配不是9月1日生的人,是哪一年生的。
\d{4}(?!0901),(?!)的意思为零宽负预测先行断言。
有正也有负,有先行,必然也有后发,然而,后发断言javascript不支持,感兴趣的同学可以自己搜索下学习。总结下:
| 名称 | 语法 | 解释 |
|---|---|---|
| 不分组捕获 | (?:exp) | 匹配exp,但是不捕获,也不给分组,也没有组号 |
| 零宽正预测先行断言 | (?=exp) | 匹配exp前面的内容,单独使用没意义 |
| 零宽负预测先行断言 | (?!exp) | 匹配非exp前面的内容,单独使用没意义 |
贪婪与懒惰
正常的限定符,都是贪婪匹配,有时候,我们会使用懒惰匹配,只需在限定符后面加?即可。
举个栗子作为结束:a.*b,标识匹配a开头,b结尾的串
- 对于输入
abbbbb,a.*b将为贪婪匹配到整个字符串,也就是说匹配结果是abbbbb - 如果将限定符后面加上
?得到a.*?b,则同样的输入,输出只会是ab
其他没聊到的
以上基本上为web开发中大部分的应用场景,至于平衡组、递归匹配等高级内容,目前已知的只有.Net环境下支持,感兴趣的话可以自己搜索。
至于常用的正则学习方法,有2个网站供大家参考
https://regexr.com/ 主要用来编写正则
http://140.143.237.171/reg 用来可视化正则,方便找错
玩的开心!