能用 AST 搞明白的正则语法,就不需要看文档
字符串的处理基本都会用正则表达式,用它来做字符串的匹配、提取、替换等很方便。
但是正则表达式的学习还是有些难度的,比如贪婪匹配、非贪婪匹配、捕获子组、非捕获子组等概念,不止初学者难理解,有很多工作几年的人都不理解。
那正则表达式怎么学比较好?怎么快速掌握正则表达式呢?
推荐一个我觉得很不错的学习正则的方式:通过 AST 来学习。
正则表达式的匹配原理是把模式串 parse 成 AST,然后通过这个 AST 去匹配目标字符串。
模式串中的各种信息在 parse 之后都会保存在 AST 里面。AST 是 abstract syntax tree,抽象语法树的意思,顾名思义,是按照语法结构组织的一棵树,那么从 AST 的结构上自然可以轻易的知道正则表达式支持的语法。
怎么查看正则表达式的 AST 呢?
可以通过 astexplorer.net 这个网站来可视化的查看:
切换 parse 的语言为 RegExp,就可以做正则表达式的 AST 的可视化。
就像前面所说,AST 是按照语法来组织的一棵树,那么从它的结构上自然能容易地理清各种语法。
那么我们就从 AST 的角度来学习下各种语法吧:
/abc/
先从简单的开始,/abc/ 这样一个正则就可以匹配 'abc' 的字符串,它的 AST 是这样的:
3 个 Char,值分别是 a、b、c,类型是 simple。那之后的匹配就是遍历 AST,分别匹配这三个字符了。
我们用 exec 的 api 测试了下:
第 0 个元素是匹配的字符串,index 是匹配字符串的开始下标。input 是输入的字符串。
再来试下特殊的字符:
/\d\d\d/
/\d\d\d/ 是匹配三个数字的意思,\d 是正则支持的有特殊含义的元字符(meta char)。
通过 AST 我们也可以看出来,它们虽然也是 Char,但类型确是 meta:
可以通过 \d 的元字符来匹配任意数字:
哪些是 meta char 哪些是 simple char,通过 AST 来看一目了然。
/[abc]/
正则支持通过 [] 的方式来指定一组字符,也就是说匹配其中任意一种字符都行。
通过 AST 我们也可以看出来,它被包裹了一层 CharacterClass,就是字符类的意思,也就是匹配它包含的任意一种字符都行。
测试下也确实是这样:
/a{1,3}/
正则表达式支持指定某个字符重复多少次,用 {from,to} 的形式,
比如 /b{1,3}/ 表示字符 b 重复 1 到 3 次,/[abc]{1,3}/ 表示 a/b/c 这个字符类重复 1 到 3 次。
通过 AST 可以看出来,这种语法叫做 Repetition(重复):
他有个 quantifier 的属性表示量词,这里的类型是 range,从 1 到 3。
正则也支持一些量词的简写,比如 + 表示 1 到无数次、* 表示 0 到无数次、? 表示 0 或 1 次。
分别是不同类型的量词:
有同学可能会问,这里的 greedy 属性是啥意思呢?
greedy 是贪婪的意思,这个属性就表示这个 Repetition 是贪婪匹配还是非贪婪匹配。
如果在量词后加个 ?,你就会发现 greedy 变成 false 了,也就是切换到了非贪婪匹配:
那贪婪和非贪婪是指啥呢?
我们看个例子就知道了。
默认 Repetition 的匹配是贪婪的,只要满足条件就一直匹配下去,所以这里 acbac 都能匹配到。
量词后加个 ? 就切换到了非贪婪,就只会匹配第一个了:
这就是贪婪匹配和非贪婪匹配,通过 AST 我们能够清楚的知道贪婪和非贪婪是针对重复语法来说的,默认是贪婪匹配,在量词后加个 ? 就可以切换到非贪婪。
(aaa)bbb(ccc)
正则表达式支持通过()把匹配到的一部分字符串放到子组里返回。
通过 AST 看一下:
对应的 AST 就叫做 Group。
而且你会发现它有个 capturing 的属性,默认是 true:
这是啥意思呢?
这就是子组捕获的语法。
如果不想捕获子组,可以这样写 (?:aaa)
看,capturing 变为 false 了。
那捕获和非捕获有什么区别呢?
我们试一下:
哦,原来 Group 的 capturing 属性代表的是是否提取的意思啊。
我们通过 AST 可以看出来,捕获是针对子组来说的,默认是捕获,也就是提取子组的内容,可以通过 ?: 切换到非捕获,就不会提取子组的内容了。
我们对用 AST 来了解正则语法已经轻车熟路了,那来看点难的:
/bbb(?=ccc)/
正则表达式支持通过 (?=xxx) 的语法来表示先行断言,用来判断某个字符串是否前面是某个字符串。
通过 AST 可以看到这种语法叫做 Assertion,并且类型为 lookahead,也就是往前看,只匹配前面的意思:
这是啥意思呢?为啥要这么写?和 /bbb(ccc)/ 还有 /bbb(?:ccc)/有啥区别呢?
我们试一下:
从结果可以看出来:
/bbb(ccc)/ 匹配了 ccc 的子组并且提取出来了这个子组,因为默认子组是捕获的。
/bbb(?:ccc)/ 匹配了 ccc 的子组但没有提取出来,因为我们通过 ?: 设置了子组不捕获。
/bbb(?=ccc)/ 匹配了 ccc 的子组也没有提取出子组,说明也是非捕获的。它和 ?: 的区别是 ccc 没有出现在匹配结果里。
这就是先行断言(lookahead assertion)的性质:先行断言代表某段字符串前面是某段字符串,对应的子组是非捕获的,而且断言的字符串不会出现在匹配结果中。
如果后面不是跟着那段字符串就不匹配:
/bbb(?!ccc)/
把 ?= 改成 ?! 之后意思就变了,通过 AST 看一下:
虽然还是先行断言 lookahead assertion,但是多了个 negative 为 true 的属性。
这个意思很明显,本来是前面是某段字符串,否定之后就是前面不是某段字符串。
那匹配结果正好就反过来了:
现在前面不是某段字符串的话才匹配了,这就是否定先行断言。
/(?<=aaa)bbb/
有先行断言自然也有后行断言,也就是后面是某段字符串才匹配。
同理,也可以否定:
(?<=aaa)对应的 AST 很容易想到,就是 lookbehind assertion:
(?
先行断言、后行断言就是最难理解的正则表达式语法了,通过 AST 来学习是不是就容易理解多了~
总结
正则表达式是处理字符串的很方便的工具,但它的学习还是有些难度的,像贪婪匹配、非贪婪匹配、捕获子组、非捕获子组、先行断言、后行断言等语法很多人都搞不清楚。
我推荐通过 AST 来学习正则,AST 是按照语法结构来组织的一颗对象树,各种语法通过 AST 节点的名字和属性可以轻易的理清楚。
比如我们通过 AST 理清楚了:
重复语法(Repetition)就是字符 + 量词的形式,默认是贪婪匹配(greedy 为 true),代表一直匹配到不匹配为止,量词后加个 ? 就切换成了非贪婪匹配,匹配到一个字符就停止。
子组语法(Group)是用于提取某段字符串的,默认是捕获(capturing 为 true),代表需要提取,可以通过 (?:xxx)切换到非捕获,只匹配不提取。
断言语法(Assertion)代表前面或后面有某段字符串,分为先行断言(lookahead assertion)和后行断言(lookbehind assertion),语法分别是(?=xxx)和 (?<=xxx),可以通过把 = 换成 ! 来表示否定(negative 为 true),意思正好反过来。
是各种文档对语法理解的深还是编译器对语法理解的深?
那还用问,肯定是编译器呀!
那么通过它按照语法 parse 出来的语法树来学习语法自然比文档更好。
正则表达式是这样,其他的语法的学习也是这样,能用 AST 学会的语法,就不需要看文档。