在此前没系统的学习过,直到看到此文章

正则表达式是匹配模式,要么匹配字符,要么匹配位置!!

字符串匹配

1、两种模糊匹配

1.1、横向模糊匹配

横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的
使用量词。譬如{m,n},表示连续出现最少m次,最多n次。

  1. var regex = /ab{2,5}c/g;
  2. var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
  3. console.log( string.match(regex) );
  4. // => ["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、量词

量词也称重复。掌握{m,n}的准确含义后

3.1、简写形式

  • **{m,}** 表示至少出现m次。
  • **{m}** 等价于{m,m},表示出现m次。
  • **?** 等价于{0,1},表示出现或者不出现。问号的意思表示,有吗?
  • **+**等价于{1,},表示出现至少一次。加号是追加的意思,得先有一个,然后才考虑追加。
  • ***** 等价于{0,},表示出现任意次,有可能不出现。

3.2、贪婪匹配和惰性匹配

有个例子:

  1. var regex = /\d{2,5}/g;//贪婪匹配
  2. var regex1 = /\d{2,5}?/g //惰性匹配
  3. var string = "123 1234 12345 123456";
  4. console.log( string.match(regex) );
  5. // => ["123", "1234", "12345", "12345"] -- 贪婪
  6. // => ["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),其中p1p2p3是子模式,用|(管道符)分隔,表示其中任何之一。

有个情况我们应该注意,比如我用/good|goodbye/,去匹配”goodbye”字符串时,结果是”good”:

  1. var regex = /good|goodbye/g;
  2. var string = "goodbye";
  3. console.log( string.match(regex) );
  4. // => ["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、什么是位置?

image.png

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” 的案例很经典

  1. 匹配后(后->前匹配)三位数字 \d{3}$
  2. 三位数字至少出现一次 (\d{3})+$
  3. 匹配三维数字前的位置(””)(?=(\d{3})+$)
  4. 不能匹配开头的位置 (?!^)
  5. 所以最后的表达式 /(?!^)(?=(\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返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。(注意:如果正则是否有修饰符gmatch返回的数组格式是不一样的)
或者

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、案例

回溯法原理

基本的回溯场景可以参见此链接,这里贴一个明晰的回溯例子:
这里再看一个清晰的回溯,正则是:
image.png
目标字符串是:”acd”ef,匹配过程是:
image.png
图中省略了尝试匹配双引号失败的过程。可以看出.*是非常影响效率的。
为了减少一些不必要的回溯,可以把正则修改为/"[^"]*"/

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的角度去挨着剖析,可以定位去具体查看