原文参考自: https://www.jianshu.com/p/681d3e07fb0f
一、原理概论
1、正则引擎 大致可以分为两类:DFA \ NFA
- DFA (Deterministic finite automaton) 确定型有穷自动机
- NFA (Non-deterministic finite automaton) 非确定型有穷自动机,大部分都是NFA
2.1、知识储备
这一小节对于你理解正则表达式很有用,尤其是明白什么是字符,什么是位置。
- 正则眼中的字符串 ===> n个字符,n+1个位置
在上面的字符串中,一共有8个字符,这是你能看到的,还有9个位置 。为什么要有字符还要有位置呢?因为位置是可以被匹配的。
那么进一步我们再来理解“占有字符”和“零宽度”:
如果一个子正则表达式匹配到的是字符,而不是位置,而且会被保存到最终的结果中,那个这个子表达式就是占有字符的,比如/ha/(匹配ha)就是占有字符的;
如果一个子正则匹配的是位置,而不是字符,或者匹配到的内容不保存在结果中(其实也可以看做一个位置),那么这个子表达式是零宽度的,比如/read(?=ing)/(匹配reading,但是只将read放入结果中,下文会详述语法,此处仅仅举例用),其中的(?=ing)就是零宽度的,它本质代表一个位置。
占有字符是互斥的,零宽度是非互斥的。也就是一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。
举个栗子,比如/aa/
是匹配不了a
的,这个字符串中的a
只能由正则的第一个a
字符匹配,而不能同时由第二个a
匹配(废话)。但是位置是可以多个匹配的,比如/\b\ba/
是可以匹配a
的,虽然正则表达式里有2个表示单词开头位置的\b
元字符,这两个\b
是可以同时匹配位置0(在这个例子中)的。
注意:我们说字符和位置是面向字符串说的,而说占有字符和零宽度是面向正则说的。
2.2 控制权和传动
控制权是指哪一个正则子表达式(可能为一个普通字符、元字符或元字符序列组成)在匹配字符串,那么控制权就在哪。
-
传动是指正则引擎的一种机制,传动装置将定位正则从字符串的哪里开始匹配。
正则表达式当开始匹配的时候,一般是由一个子表达式获取控制权,从字符串中的某一个位置开始尝试匹配,一个子表达式开始尝试匹配的位置,是从前一子表达匹配成功的结束位置开始的。
举一个栗子,read(?=ing)ing\sbook
匹配reading book
,我们把这个正则看成5个子表达式read
、(?=ing)
、ing
、\s
、book
,当然你也可以吧read看做4个单独字符的子表达式,只是我们这里为了方便这么看待。read
从位置0开始匹配到位置4,后面的(?=ing)
继续从位置4开始匹配,发现位置4后面确实是ing
,于是断言匹配成功,也就是整一个(?=ing)
就是匹配了位置4这一个位置而已(这里更能理解什么是零宽了吧),然后后面的ing
再从位置4开始匹配到位置7,然后\s
再从位置7匹配到位置8,最后的book
从位置8匹配到位置12,整一个匹配完成。
3. 匹配之旅“浅”度游(可跳过)
说了那么多,我们把自己当做一个正则引擎,一步一步以最小的单位——“字符”和“位置”——去看一下正则匹配的过程,举几个栗子。
3.1 基本匹配
正则表达式:easy
源字符串:So easy
匹配过程:首先由正则表达式字符e
取得控制权,从字符串的位置0开始匹配,遇到字符串字符‘S’
,匹配失败,然后正则引擎向前传动,从位置1开始尝试,遇到字符串字符‘o’
,匹配失败,继续传动,后面的空格自然也失败,于是从位置3开始尝试匹配,成功匹配字符串字符‘e’
,控制权交给正则表达式子表达式(这里也是一个字符)‘a’
,尝试从上次匹配成功的结束位置4开始匹配,成功匹配字符串字符‘a’
,后面一直如此匹配到‘y’
,然后匹配完成,匹配结果为easy
。
3.2 零宽匹配
正则:^(?=[aeiou])[a-z]+$
源字符串:apple
首先这个正则表示:匹配这样一个从头到尾完整的字符串,这整一个字符串仅由小写字母组成,并且以a、e、i、o、u这5个字母任一字母开头。
匹配过程:首先正则的^
(表示字符串开始的位置)获取控制权,从位置0开始匹配,匹配成功,控制权交给(?=[aeiou])
,这个子表达式要求该位置右边必须是元音小写字母中的一个,零宽子表达式相互间不互斥,所以从位置0开始尝试匹配,右侧是字符串的‘a’
,符合因此匹配成功,所以(?=[aeiou])
匹配此处的位置0匹配成功,控制权交给[a-z]+
,从位置0开始匹配,字符串‘apple’
中的每个字符都匹配成功,匹配到字符串末尾,控制权交回正则的$
,尝试匹配字符串结束位置,成功,至此,整个匹配完成。
3.3 贪婪匹配和非贪婪匹配
正则1:{,*}
正则2:{,*?}
源字符串:{233}
这里有两个正则,在限定符(语法会讲什么是限定符)后面加?符号表示忽略优先量词,也就是非贪婪匹配,这个栗子我剥得快一点。
首先开头的{匹配,
两个正则都是一样的表现。
正则1的.*
为贪婪匹配,所以一直匹配余下字符串'233}'
,匹配到字符串结束位置,只是每次匹配,都记录一个备选状态,为了以后回溯,每次匹配有两条路,选择了匹配这条路,但记一下这里还可以有不匹配这条路,如果前面死胡同了,可以退回来,此时控制权交还给正则的},去匹配字符串结束位置,失败,于是回溯,意思就是说前面的.*
你吃的太多了,吐一个出来,于是控制权回给.*
,吐出一个}
,(其实是用了前面记录的备选状态,尝试不用.*
去匹配'}'
),控制权再给正则的}
,这次匹配就成功了。
正则2的.*?
为非贪婪匹配,尽可能少地匹配,所以匹配'233}'
的每一个字符的时候,都是尝试不匹配,但是一但控制权交还给最后的}就发现出问题了,赶紧回溯乖乖匹配,于是每一个字符都如此,最终匹配成功。
其它详细参考:http://blog.csdn.net/lxcnn
二、语法一览
其它正则入门参考:deerchao写的30分钟入门教程
1、常见字符--简单元字符
为什么这里要加简单2个字,因为在正则中,\d
、\w
这样的叫元字符,而{n,m}
、(?!exp)
这样的也叫元字符,所以元字符是在正则中有特定意义的标识,而这一小节讲的是简单的一些元字符。
.
匹配除了换行符以外的任意字符,也即是[^\n]
,如果要包含任意字符,可使用(.|\n)
.\w
匹配任意字母、数字或者下划线,等价于[a-zA-Z0-9_]
,在deerchao的文中还指出可匹配汉字,但是\w
在JS中是不能匹配汉字的.\s
匹配任意空白符,包含换页符\f
、换行符\n
、回车符\r
、水平制表符\t
、垂直制表符\v
\d
匹配数字\un
匹配n
,这里的n
是一个有4个十六进制数字表示的Unicode字符,比如\u597d
表示中文字符“好”,那么超过\uffff
编号的字符怎么表示呢?ES6的u修饰符会帮你。
2. 要表示出现次数(重复)——限定符
a*
表示字符a
连续出现次数 >= 0 次a+
表示字符a
连续出现次数 >= 1 次a?
表示字符a
出现次数 0 或 1 次a{5}
表示字符a
连续出现次数 5 次a{5,}
表示字符a
连续出现次数 >= 5次a{5,10}
表示字符a
连续出现次数为 5到10次 ,包括5和10
3. 匹配位置——定位符和零宽断言
匹配某个位置的表达式都是零宽的,这是主要包含两部分,一是定位符,匹配一个特定位置,二是零宽断言,匹配一个要满足某要求的位置。
定位符有以下几个常用的:
\b
匹配单词边界位置,准确的描述是它匹配一个位置,这个位置前后不全是\w
能描述的字符,所以像\u597d\babc
是可以匹配“好abc”
的。^
匹配字符串开始位置,也就是位置0,如果设置了 RegExp 对象的 Multiline 属性,^
也匹配'\n'
或'\r'
之后的位置$
匹配字符串结束位置,如果设置了RegExp 对象的 Multiline 属性,$
也匹配'\n'
或'\r'
之前的位置
零宽断言(JS支持的)有以下两个:
(?=exp)
匹配一个位置,这个位置的右边能匹配表达式exp
,注意这个表达式仅仅匹配一个位置,只是它对于这个位置的右边有要求,而右边的东西是不会被放进结果的,比如用read(?=ing)
去匹配“reading”
,结果是“read”
,而“ing”
是不会放进结果的.(?!exp)
匹配一个位置,这个位置的右边不能匹配表达式exp
4. 想表达“或”的意思——字符簇和分歧
我们经常会表达“或”的含义,比如这几个字符中的任意一个都行,再比如匹配5个数字或者5个字母都行等等需求。
字符簇可用来表达字符级别的“或”语义,表示的是方括号中的字符任选一:
[abc]
表示a
、b
、c
这3个字符中的任意一个,如果字母或者数字是连续的,那么可以用-连起来表示,[b-f]
代表从b
到f
这么多字符中任选一个[(ab)(cd)]
并不会用来匹配字符串“ab”
或“cd”
,而是匹配a
、b
、c
、d
、(
、)
这6个字符中的任一个,也就是想表达“匹配字符串ab
或者cd
”这样的需求不能这么做,要这么写ab|cd
。但这里要匹配圆括号本身,讲道理是要反斜杠转义的,但是在方括号中,圆括号被当成普通字符看待,即便如此,仍然建议显式地转义.
分歧用来表达表达式级别的“或”语义,表示的是匹配|左右任一表达就可:
ab|cd
会匹配字符串“ab”
或者“cd”
会短路,回想下编程语言中逻辑或的短路,所以用
(ab|abc)
去匹配字符串“abc”
,结果会是“ab”
,因为竖线左边的已经满足了,就用左边的匹配结果代表整个正则的结果.
5. 想表达“非”的意思——反义
有时候我们想表达“除了某些字符之外”这样的需求,这个时候就要用到反义.
-
\W
、\D
、\S
、\B
用大写字母的这几个元字符表示就是对应小写字母匹配内容的反义,这几个依次匹配“除了字母、数字、下划线外的字符”、“非数字字符”、“非空白符”、“非单词边界位置”
-[^aeiou]
表示除了a
、e
、i
、o
、u
外的任一字符,在方括号中且出现在开头位置的^
表示排除,如果^
在方括号中不出现在开头位置,那么它仅仅代表^字符本身.
6. 整体看待和捕获——分组和后向引用
其实你在上面的一些地方已经看到了圆括号,是的,圆括号就是用来分组的,括在一对括号里的就是一个分组。
上面讲的大部分是针对字符级别的,比如重复字母 “A”
5次,可以用A{5}
来表示,但是如果想要字符串“ABC”
重复5次呢?这个时候就需要用到括号。
括号的第一个作用,将括起来的分组当做一个整体看待,所以你可以像对待字符重复一样在一个分组后面加限定符,比如(ABC){5}
。
分组匹配到的内容也就是这个分组捕获到的内容,从左往右,以左括号为标志,每个分组会自动拥有一个从1开始的编号,编号0的分组对应整个正则表达式,JS不支持捕获组显示命名。
括号的第二个作用,分组捕获到的内容,可以在之后通过\分组编号的形式进行后向引用。比如(ab|cd)123\1
可以匹配“ab123ab”
或者“cd123cd”
,但是不能匹配“ab123cd”
或“cd123ab”
,这里有一对括号,也是第一对括号,所以编号为捕获组1,然后在正则中通过\1去引用了捕获组1的捕获的内容,这叫后向引用。
括号的第三个作用,改变优先级,比如abc|de
和(abc|d)e
表达的完全不是一个意思。
7. 转义
任何在正则表达式中有作用的字符都建议转义,哪怕有些情况下不转义也能正确,比如[]中的圆括号、^符号等。
8. 优先级问题
优先级从高到低是:
- 转义 \
- 括号(圆括号和方括号)(), (?:), (?=), []
- 字符和位置
- 竖线 |
9. 贪婪和非贪婪
在限定符中,除了{n}确切表示重复几次,其余的都是一个有下限的范围。
贪婪
在默认的模式(贪婪)下,会尽可能多的匹配内容。比如用ab*
去匹配字符串“abbb”
,结果是“abbb”
。非贪婪
而通过在限定符后面加问号?
可以进行非贪婪匹配,会尽可能少地匹配。用ab*?
去匹配“abbb”
,结果会是“a”
。
10. 修饰符(匹配选项)
其实正则的匹配选项有很多可选,不同的宿主语言环境下可能各有不同,此处就JS的修饰符作一个说明:
加
g
修饰符:表示全局匹配,模式将被应用到所有字符串,而不是在发现第一个匹配项时停止加
i
修饰符:表示不区分大小写加
m
修饰符:表示多行模式,会改变^
和$
的行为,上文已述
元字符 | 描述 |
---|---|
\ | 将下一个字符标记符、或一个向后引用、或一个八进制转义符。例如,“\n”匹配\n。“\n”匹配换行符。序列“\”匹配“\”而“(”则匹配“(”。即相当于多种编程语言中都有的“转义字符”的概念。 |
^ | 匹配输入字行首。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置。 |
$ | 匹配输入行尾。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。 |
* | 匹配前面的子表达式任意次。例如,zo能匹配“z”,也能匹配“zo”以及“zoo”。等价于o{0,} |
+ | 匹配前面的子表达式一次或多次(大于等于1次)。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。 |
? | 匹配前面的子表达式零次或一次。例如,“do(es)?”可以匹配“do”或“does”中的“do”。?等价于{0,1}。 |
{n} | n是一个非负整数。匹配确定的n次。例如,“o{2}”不能匹配“Bob”中的“o”,但是能匹配“food”中的两个o。 |
{n,} | n是一个非负整数。至少匹配n次。例如,“o{2,}”不能匹配“Bob”中的“o”,但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。 |
{n,m} | m和n均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。例如,“o{1,3}”将匹配“fooooood”中的前三个o为一组,后三个o为一组。“o{0,1}”等价于“o?”。请注意在逗号和两个数之间不能有空格。 |
? | 当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串“oooo”,“o+”将尽可能多的匹配“o”,得到结果[“oooo”],而“o+?”将尽可能少的匹配“o”,得到结果 ['o', 'o', 'o', 'o'] |
.点 | 匹配除“\n”之外的任何单个字符。要匹配包括“\n”在内的任何字符,请使用像“[\s\S]”的模式。 |
(pattern) | 匹配pattern并获取这一匹配。所获取的匹配可以从产生的Matches集合得到,在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性。要匹配圆括号字符,请使用“(”或“)”。 |
详见百度百科URL: "https://baike.baidu.com/item/正则表达式/1700215?fr=aladdin"