在此前没系统的学习过,直到看到此文章
正则表达式是匹配模式,要么匹配字符,要么匹配位置!!
字符串匹配
1、两种模糊匹配
1.1、横向模糊匹配
横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的
使用量词。譬如{m,n}
,表示连续出现最少m次,最多n次。
var regex = /ab{2,5}c/g;
var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
console.log( string.match(regex) );
// => ["abbc", "abbbc", "abbbbc", "abbbbbc"]
表示 第一个字符是“a”,接下来是2到5个字符“b”,最后是字符“c”。
最后的字符 g 表示 global,是正则的修饰符,表示匹配全部,而不是“第一个”
1.2、纵向模糊匹配
纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能
使用字符组。譬如[abc]
,表示该字符是可以字符“a”、“b”、“c”中的任何一个。
2、字符组
2.1、范围表示法
比如[123456abcdefGHIJKLM]
,可以写成[1-6a-fG-M]
。用连字符-
来省略和简写
因为连字符有特殊用途,那么要匹配“a”、“-”、“z”这三者中任意一个字符,该怎么做呢?
不能写成[a-z]
,因为其表示小写字符中的任何一个字符。
可以写成如下的方式:[-az]
或[az-]
或[a\-z]
2.1、排除字符组
排除字符组(反义字符组)的概念。例如[^abc]
,表示是一个除”a”、”b”、”c”之外的任意一个字符。字符组的第一位放^
(脱字符),表示求反的概念
2.3、常用简写形式
\d
就是[0-9]
。表示是一位数字。digit(数字)\D
就是[^0-9]
。表示除数字外的任意字符。\w
就是[0-9a-zA-Z_]
。表示数字、大小写字母和下划线。w是word的简写,也称单词字符。\W
是[^0-9a-zA-Z_]
。非单词字符。\s
是[ \t\v\n\r\f]
。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。s是space character的首字母。\S
是[^ \t\v\n\r\f]
。 非空白符。.
就是[^\n\r\u2028\u2029]
。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。
3、量词
3.1、简写形式
**{m,}**
表示至少出现m次。**{m}**
等价于{m,m}
,表示出现m次。**?**
等价于{0,1}
,表示出现或者不出现。问号的意思表示,有吗?**+**
等价于{1,}
,表示出现至少一次。加号是追加的意思,得先有一个,然后才考虑追加。*****
等价于{0,}
,表示出现任意次,有可能不出现。
3.2、贪婪匹配和惰性匹配
有个例子:
var regex = /\d{2,5}/g;//贪婪匹配
var regex1 = /\d{2,5}?/g //惰性匹配
var string = "123 1234 12345 123456";
console.log( string.match(regex) );
// => ["123", "1234", "12345", "12345"] -- 贪婪
// => ["12", "12", "34", "12", "34", "12", "34", "56"] -- 惰性
/\d{2,5}/g
表示匹配连续的2,3,4,5位数字,但是最多就是连续的五个(贪婪的概念在于尽可能的多,往多的次数尝试,所以每次都能匹配最多情况),因此在结果中可以看到 123456
-> 最后只匹配到了 12345
惰性匹配往往是在贪婪匹配的量词后加 ?
来修饰。/\d{2,5}?/
表示,虽然2到5次都行,当2个就够的时候,就不在往下尝试了。所以结果都是两次连续的数字
在量词后面加个问号就能实现惰性匹配,所有惰性匹配的形式如下:
**{m,n}?**
**{m,}?**
**??**
**+?**
***?**
4、分支结构(多选分支)
一个模式可以实现横向和纵向模糊匹配。而多选分支可以支持多个子模式任选其一。
具体形式如下:(p1|p2|p3)
,其中p1
、p2
和p3
是子模式,用|
(管道符)分隔,表示其中任何之一。
有个情况我们应该注意,比如我用/good|goodbye/
,去匹配”goodbye”字符串时,结果是”good”:
var regex = /good|goodbye/g;
var string = "goodbye";
console.log( string.match(regex) );
// => ["good"]
原因是分支结构是惰性的,及当前面的匹配上了,后面的就不再尝试了
所以要完全匹配 “goodbye”,提到前面就行,及/goodbye|good/
案例分析
匹配16进制颜色值
16进制字符数组表示方式 [0-9A-Fa-f]
3位或者6位字符({6}|{3})
注意6在前面的原因是因为分支结构是惰性的
因此完整的结果为:/#(``[0-9A-Fa-f]``{6}|``[0-9A-Fa-f]``{3})/g
位置匹配
1、什么是位置?
2、如何匹配位置?
在ES5中,共有6个锚字符:
**^**
>**$**
**\b**
>**\B**
**(?=p)**
>**(?!p)**
2.1、
^
$
^
(脱字符)匹配开头,在多行匹配中匹配行开头。$
(美元符号)匹配结尾,在多行匹配中匹配行结尾。
注意多行的概念,表示每一行的开头或者结尾
var result = "I\nlove\njavascript".replace(/^|$/gm, '#');
console.log(result);
/*
#I#
#love#
#javascript#
*/
2.2、\b
\B
\b
是单词边界,具体就是\w
和\W
之间(单词字符和非单词字符)的位置,也包括\w
和^
之间(单词字符和字符起始位置)的位置,也包括\w
和$
之间(单词字符和字符结尾)的位置 — boder ??
如下例子体会一下:
var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result);
// => "[#JS#] #Lesson_01#.#mp4#"
\B
就是\b
的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉\b
,剩下的都是\B
的。
例子对比下:
var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
console.log(result);
// => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
2.3、(?=p) (?!p)
(?=p)
,其中p
是一个子模式,即p
前面的位置。
比如(?=l)
,表示’l’字符前面的位置,例如:
var result = "hello".replace(/(?=l)/g, '#');
console.log(result);
// => "he#l#lo"
而(?!p)
就是(?=p)
的反面意思,比如:
var result = "hello".replace(/(?!l)/g, '#');
console.log(result);
// => "#h#ell#o#"
3、位置的特性
对于位置的理解,我们可以理解成空字符””。
比如”hello”字符串等价于如下的形式:
"hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";
4、实例分析
把”12345678”,变成”12,345,678” 的案例很经典
- 匹配后(后->前匹配)三位数字 \d{3}$
- 三位数字至少出现一次 (\d{3})+$
- 匹配三维数字前的位置(””)(?=(\d{3})+$)
- 不能匹配开头的位置 (?!^)
- 所以最后的表达式 /(?!^)(?=(\d{3})+$)/g
如果要把”12345678 123456789”替换成”12,345,678 123,456,789”。
此时我们需要修改正则,把里面的开头^
和结尾$
,替换成\b
:
var string = "12345678 123456789",
reg = /(?!\b)(?=(\d{3})+\b)/g;
var result = string.replace(reg, ',')
console.log(result);
// => "12,345,678 123,456,789"
其中(?!\b)
怎么理解呢?
要求当前是一个位置,但不是\b
前面的位置,其实(?!\b)
说的就是\B
。
因此最终正则变成了:/\B(?=(\d{3})+\b)/g
。
括号的作用
1、引用分组
1.1、提取数据
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( string.match(regex) );
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
match
返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。(注意:如果正则是否有修饰符g
,match
返回的数组格式是不一样的)
或者
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( regex.exec(string) );
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
同时,也可以使用构造函数的全局属性$1
至$9
来获取:
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
regex.test(string); // 正则操作即可,例如
//regex.exec(string);
//string.match(regex);
console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"
1.2、替换
想把yyyy-mm-dd格式,替换成mm/dd/yyyy怎么做?
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result);
// => "06/12/2017"
其中replace
中的,第二个参数里用$1
、$2
、$3
指代相应的分组。等价于如下的形式:
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function() {
return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result);
// => "06/12/2017"
也等价于:
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function(match, year, month, day) {
return month + "/" + day + "/" + year;
});
console.log(result);
// => "06/12/2017"
2、反向引用
除了使用相应API来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用
我们想要求分割符前后一致怎么办?
此时需要使用反向引用:
var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
注意正则表达式中的的\1
,表示的引用之前的那个分组(-|\/|\.)
。不管它匹配到什么(比如-),\1
都匹配那个同样的具体某个字符。
我们知道了\1
的含义后,那么\2
和\3
的概念也就理解了,即分别指代第二个和第三个分组。
括号嵌套怎么办?
以左括号(开括号)为准。比如:
var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
var string = "1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3
引用不存在的分组会怎样?
因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。例如\2
,就匹配”\2”。注意”\2”表示对”2”进行了转意。
var regex = /\1\2\3\4\5\6\7\8\9/;
console.log( regex.test("\1\2\3\4\5\6\7\8\9") );
console.log( "\1\2\3\4\5\6\7\8\9".split("") );
3、非捕获分组
之前文中出现的分组,都会捕获它们匹配到的数据,以便后续引用,因此也称他们是捕获型分组。
如果只想要括号最原始的功能,但不会引用它,即,既不在API里引用,也不在正则里反向引用。此时可以使用非捕获分组(?:p)
,例如本文第一个例子可以修改为:
var regex = /(?:ab)+/g; //匹配一个或者多个ab
var string = "ababa abbb ababab";
console.log( string.match(regex) );
// => ["abab", "ab", "ababab"]
4、案例
回溯法原理
基本的回溯场景可以参见此链接,这里贴一个明晰的回溯例子:
这里再看一个清晰的回溯,正则是:
目标字符串是:”acd”ef,匹配过程是:
图中省略了尝试匹配双引号失败的过程。可以看出.*
是非常影响效率的。
为了减少一些不必要的回溯,可以把正则修改为/"[^"]*"/
。
1、回溯形式
本质上就是深度优先搜索算法。其中退到之前的某一步这一过程,我们称为“回溯”
1.1、贪婪量词
之前的例子都是贪婪量词相关的。比如b{1,3}
,因为其是贪婪的,尝试可能的顺序是从多往少的方向去尝试。首先会尝试”bbb”,然后再看整个正则是否能匹配。不能匹配时,吐出一个”b”,即在”bb”的基础上,再继续尝试。如果还不行,再吐出一个,再试。如果还不行呢?只能说明匹配失败了
1.2、惰性量词
惰性量词就是在贪婪量词后面加个问号。表示尽可能少的匹配
区别下贪婪和惰性区别
//贪婪
var string = "12345";
var regex = /(\d{1,3})(\d{1,3})/;
console.log( string.match(regex) );
// => ["12345", "123", "45", index: 0, input: "12345"]
前面的\d{1,3}匹配的是"123",后面的\d{1,3}匹配的是"45"
//惰性
var string = "12345";
var regex = /(\d{1,3}?)(\d{1,3})/;
console.log( string.match(regex) );
// => ["1234", "1", "234", index: 0, input: "12345"]
其中\d{1,3}?只匹配到一个字符"1",而后面的\d{1,3}匹配了"234"。
1.3、分支结构
分支也是惰性的,比如/can|candy/
,去匹配字符串”candy”,得到的结果是”can”,因为分支会一个一个尝试,如果前面的满足了,后面就不会再试验了
正则效率案例
1.1、使用具体型字符组来代替通配符,来消除回溯
匹配双引用号之间的字符。如,匹配字符串123”abc”456中的”abc”。
如果正则用的是:/".*"/
,,会在第3阶段产生4次回溯
如果正则用的是:/".*?"/
,会产生2次回溯(粉色表示.*?
匹配的内容)
此时要使用具体化的字符组,来代替通配符.
,以便消除不必要的字符,此时使用正则/"[^"]*"/
,即可
1.2、使用非捕获型分组
因为括号的作用之一是,可以捕获分组和分支里的数据。那么就需要内存来保存它们。
当我们不需要使用分组引用和反向引用时,此时可以使用非捕获分组。例如:/^[+-]?(\d+\.\d+|\d+|\.\d+)$/
可以修改成:/^[+-]?(?:\d+\.\d+|\d+|\.\d+)$/
1.3、独立出确定字符
例如/a+/
,可以修改成/aa*/
。
因为后者能比前者多确定了字符a。这样会在第四步中,加快判断是否匹配失败,进而加快移位的速度。
1.4、提取分支公共部分
比如/^abc|^def/
,修改成/^(?:abc|def)/
。
又比如/this|that/
,修改成/th(?:is|at)/
。
这样做,可以减少匹配过程中可消除的重复。
1.5、减少分支的数量,缩小它们的范围
/red|read/
,可以修改成/rea?d/
。此时分支和量词产生的回溯的成本是不一样的。但这样优化后,可读性会降低的。
正则表达式编程
从JS的角度去挨着剖析,可以定位去具体查看