正则表达式入门

384 阅读8分钟

初识

正则表达式是主要用于进行文本匹配的表达式,就是判断一段为文字是否与设定的规则相同。

元字符

正则中的字符,有些代表字符本身,例如abc,其中的字符都代表其本身。在字符串中不可以随便书写空格或者换行符,因为也会被当作匹配内容的一部分。

预设字符组合

  • \d:代表任意一个数字字符,包括0~9的任意数字。d为单词digit数字的首字母
  • \D:代表任意一个非数字字符,除了0~9以外的数字,与\d互为补集。
  • \s:代表任意一个空白字符,其中包括了空格、换行符(\r或者\n)、制表符、换页符和垂直制表符。s为单词space空白格的首字母。(该字符一般不会包括全角空格或者其他异性空格,个别编程中有例外)。
  • \S:代表任意一个非空白字符,与\s互为补集。
  • \w:代表单词内的任意一个可用字符,一般情况下包括数组、大小写字母和下划线。w为单词word单词的首字母。
  • \W:一般情况下代表数字、大小字母和下划线以外的任意一个字符,与\w互为补集。

以上六个元字符,大写与小写互为补集,为了方便记忆,可以记住首字母所代表的含义,然后小写为包含,大写为不包含。并且大小写一起使用可以匹配所有字符。

自定义字符组

除了预设的字符组合,我们还可以使用方括号[]来自定义字符组合。 例如:[xyz]就是一个字符组合,可以匹配xyz中的任意一个。 针对字母和数字,还可以上使用连字符-来进行简写,表示一定的范围。 例如:[a-z]表示小写的a到z的所有字母。需要注意的是,这里是区分大小写的。 [0-9]表示数字的0到9的所又数字。

需要注意的是,[a-zA-Z]不能直接简写成为[a-Z]

当连字符必须夹在字符中间才表示连字符,若连字符在首位或者最后一位,则代表-本身。为了让变分辨正则公式,若需要匹配-,一般建议将其放在首位。

因为这里的连写是和字符编码顺序挂钩的。在字符编码中,小写字母是在大写字母之后的,该写法无法满足从小到大的要求,会导致引擎报错。

这时候就会有小伙伴可能觉得反着过来,用[A-z]就可以了。我只能说那你可真是个小机灵鬼,然而实际上这种写法虽然不会报错,但是会包含一些我们不需要的字符,因为字符编码中,大写字母与小写字母中间还会包含着一些其他字符,包括[]_等。

在没有使用量词的情况下,一个方括号内的字符组合,只能匹配从左到右第一个符合规则的字符。 另外需要注意的是,在使用数字简写的时候,对应范围的开始到截止,只以简写符两边的单个字符作为标准。例如,[0-34-67-9]等价于[0-9]

排除型字符组

排除性字符组也可以简单理解为取反或者补集。在方括号内使用脱字符^来表示,一般放在首位,代表需要排除方括号内包含的字符。

例如[^\s]等价于\S

排除性字符组的含义,正确的理解为“匹配除了所列字符以外的任意一个字符”,而不是“不匹配所列出的字符”。

前者的理解,是将列出的字符组,作为一个范围来看待。后者则会将列出的字符组作为一个整体来看待。

在部分语言中,可以使用[^]来代替.,排除字符组中没有任何字符,也就意味着不需要排除字符,所以就能匹配所有。但是前提必须是对应的语言支持该种写法。

^必须在首位才代表排除性字符组,在其他位置时,只表示^符号本身。

转义序列

常见的转义序列有\r(回车符)、\n(换行符)、\t(制表符)、\f(换页符)、\v(垂直制表符)、\0(NULL字符)。

转义序列,是通过在字符前加上\来进行转义的。除了上方提到的,将原先无特殊含义的字符通过转义代表某些字符以外。还可以将原先有特殊含义的字符转义成其本身。 例如.代表全部字符,但是我们使用转义字符,将其写成\.之后,他就代表了.符号本身。

正则表达式中的\转义是一种安全转义。当使用\转义时,无法进行正常的转义时,它就达标后续字符其本身。这种情况一般是在某些特定语言内才会出现。

字符编码转义

正则的字符转义时,支持使用ASCII 码。 \nnn可以表示8进制ASCII码对应的字符。这里的n代表0.7范围内的一个数字。 \xnn可以表示16进制ASCII码对应的字符。这里的n代表的是0-9a-f范围内的一个数字。x则不变。例如\x41代表A。

同时正则也支持Unicode字符,格式是\unnnnn与上文16进制下ASCII码的n的取值范围相同。u不变。

量词

在正则匹配中,我们可以通过使用量词,来设定正则表达式匹配的次数。

  • ?代表匹配0次或者1次。
  • *代表匹配0次或者次。
  • +代表匹配1次以上,至少匹配1次。
  • {n,m}则是限定了一个范围次数值,最少匹配n次,最多匹配m次。
  • {n}代表恰好匹配n次。
  • {n}代表最少匹配n次,最多不限。

其中?等价于{0,1}*等价于{0,}+等价于{1,}

量词所控制的,是对应字符可出现的次数,例如ab*c字符组,可以匹配abbbbbbbbc

匹配位置

之前所提到的元字符都是用来匹配字符的,除此之外还有些元字符可以用来匹配位置。

  • ^可以用来匹配开始位置,一般是指整个字符串的首位。这个字符与排除型字符组是一致的,两者的区分方法是,排除型字符需要在方括号内使用才生效。
  • $代表结束的位置,一般情况下代表结束的位置。
  • \b代表单词边界,用来匹配单词的开始和结尾,例如\bname\b可以匹配namename=1a\rname中的name,但是无法匹配mynamemy_namename1中的name。**需要注意的是,\b在方括号内使用时,在部分语言中代表退格符,而不是单词边界。
  • \B代表非单词边界,按照我们上文中说的,大小写代表着取反,所以\B\b也是互为补集的。

**必须特别注意,匹配位置与匹配字符不同,匹配位置在匹配时不会占用原字符的任何字符。**所以实际在使用匹配位置时,这类元字符的后一位字符才是首位字符。

分支

字符组在匹配的时候,会出现分支情况。例如我们在设置了一个字符组[abc],这个字符组不仅能匹配abc,还有abacbcabc等情况。而在使用了量词之后,分支情况则会更加的复杂。

所以当我们在使用字符组的时候,需要注意分支情况,在不需要其他多余分支情况时,我们可以使用管道操作符 |来隔开,|在我们常见的情况中一般代表,所以表示“匹配符号左边或者匹配符号右边都是成功的”。

|可以多次使用,创建多个分支情况。需要注意的是,我们在使用的时候,需要按照可包含性进行一个排序来写。例如我们需要匹配abcabc234这两种情况的时候,最好写成(abc234|abc),这是因为如果我们使用(abc|abc234)来匹配1abc234的话,可能在匹配到abc的时候就停止了,而实际上他是能匹配到abc234的。

分组

上面使用了方括号[]可以自定义字符组,那么更常见的圆括号肯定也有其特别的用法。圆括号可以将字符组进行分组,然后配合量词和分支进行使用,使我们的正则表达式更加清晰明了。

例如我们在使用分支时,例如ab|cd*的情况,该字符组中的*只会作用于右分支的d字符上,例如匹配cd或者cdddddddd。而当我们使用了分组,可以将其作用于整个字符组,例如(ab|cd)*。或者只作用于右分支,例如ab|(cd)*

当我们使用量词加分组时,量词所作用的目标就并不是前一位的字符,而是整个分组内的字符。

当我们需要匹配()圆括号本身时,记得要使用转义符号哦。

捕获分组

分组不仅用于区分和配合量词、分支使用,自身也具有捕获特性。

每一个分组都会依照出现的顺序被标注数字序号。例如(1(2(3))(4)),用这个字符组来匹配1234的字符。

尝试用控制台打印一下。

var a = /(1(2(3))(4))/;
console.log("1234".match(a));
//"1234"
//"1234"
//"23"
//"3"
//"4"

不难发现,主要是以从左到右首次出现的括号所在的分组来排序的。

这里特别在最外圈用圆括号全部包起来,可以发现,全值"1234"被打印了两遍。这是因为这个字符组整体本身就是一个最大的分组。

捕获分组主要用于替换字符串。

非捕获分组

我们有时在使用分组的时候只是为了配合量词或者分支来使用,并不希望其占用分组序号,这时候就可以考虑使用非捕获分组。非捕获分组,就是在左括号后面使用一个冒号,写成(?:......)的形式。

使用非捕获分组,一般是因为希望结果只包含我们所关心的部分,不希望其他无关的分组干扰这个结果。

例如:

var a = /1(?:2|3)/;
console.log("12".match(a));
//"12"
console.log("13".match(a));
//"13"
console.log("2".match(a));
//null

这里可以发现,我们无法只匹配非捕获分组内的内容,这是因为这里并没有被捕获,不占用分组的序号。

命名分组

前面我们提到,捕获分组可以给分组分别标记序号。而命名分组,则是可以对分组进行命名。所以你可以把命名分组看做特殊的捕获分组来使用。可能会有人疑惑,实际上,命名分组你可以把它看做一个变量来看,名字是变量名,对应的正则则是变量的值,这样我们在使用的时候就会更加方便简洁。

在JavaScript中,我们使用(?<groupname>)的语法来进行命名分组。在replace()方法来替换字符串时,我们可以使用$<groupname>的方法来引用对应的命名分组

var reg = /<(?<tag>[a-z][a-z\d]*)\b[^>]*>([\d\D]*?)<\/\k<tag>>/ig;

console.log('<p class="p1">文本</p>'.replace(reg, '<$<tag>>$2</$<tag>>'));
//<p>文本<p>

这里在写正则的时候,重复的部分可以直接使用对应的命名。

命名分组2018年正式成为ECMAScript的标准特性。

不过需要注意的时,即便命名分组已经成为了`ECMAScript的正式标准,我们在生产环境中也不应该随便使用,因为我们无法保证用户使用的浏览器标准。

反向引用

有时候我们需要实现配对匹配,例如我们在匹配带有引号的内容是时,需要确保前后引号一致,这时候就需要用到反向引用。

反向引用使用\符号加上捕获分组的序号或者命名分组的名字,来代表匹配过程中捕获的第一个分组的值。

var a = 'a123b';
var b = 'a123a';
var reg = /([\w])([\d]*)\1/g;
a.replace(reg,'x'); // 'a123b'
a.replace(reg,'x'); // 'x'

需要注意的是,反向引用不能在字符组里使用,例如[\1]并不会代表第一个分组的值,而是单纯的代表字符1

环视

环视用于对位置进行匹配,在特定位置上,如果之前或者之后的字符/字符序列符合预设的条件,则匹配成功。

顺序环视

顺序环视也称正向环视。使用(?=)来表示,括号内的其余部分必须与当前位置之后的部分相符合。

var a = 'a-';
var b = 'a ';
var reg = /\w+(?=\s)/;
console.log(a.match(reg));  //null
console.log(b.match(reg)); //"a"

上述代码中,第二个输出结果为"a "。这里需要注意,环视匹配的是位置,并不包含在字符中,所以输出的是"a"而不是"a "(后者最后有个空格)。

环视的语法使用括号,因此可以使用管道符号|来书写分支,同时也可以配合量词来使用。并且不会产生捕获分组。所有的环视语法都有这种特性

顺序否定环视

顺序否定环视,也成为正向否定环视。使用(?!)来表示,括号内的其余部分需要与当前位置之后的部分不相符合

var a = 'a-';
var b = 'a ';
var reg = /\w+(?!\s)/;
console.log(a.match(reg)); // "a"
console.log(b.match(reg)); // null

其实从名字我们就能知道,顺序否定环视与顺序环视是取反的。、

逆序环视

逆序环视也称反向环视。使用(?<=)来表示,括号内的其余部分必须与当前位置之前的部分相符合。

这里特别需要注意顺序环视和逆序环视分别对应的位置,否则会影响使用。

逆序否定环视

逆序否定环视,也成为反向否定环视。使用(?<!)来表示,括号内的其余部分需要与当前位置之前的部分不相符合

逆序与顺序唯一的差别就是语法和对应位置不一样,这里不多赘述,小伙伴自己尝试即可。

贪婪匹配与惰性匹配

量词在NFA正则表达式引擎中,一般会尝试进行尽可能多的匹配,这就是所谓的贪婪匹配。

var reg = /\d+(?=[57])/g;
var str = "124567;
console.log(str.match(reg));
// 输出 ["12456"]

上述代码可以发现,输出结果最后选择了较长的"123456"而不是"1234"

我们需要注意的是,贪婪匹配有时候会导致匹配到我们意想不到的结果。

例如:

var reg = /<!--[\d\D]*-->/g;
var html = "<!-- 内容开始 --><p>文本内容</p><!-- 内容结束 -->";

console.log(html.match(reg));
// 输出 ["<!-- 内容开始 --><p>文本内容</p><!-- 内容结束 -->"]

与贪婪匹配相对应的是惰性匹配,说了是相对,所以惰性匹配会优先尝试尽可能少的文本匹配

惰性匹配的使用方式,是在量词后面增加一个?

例如

var reg = /<!--[\d\D]*?-->/g;
var html = "<!-- 内容开始 --><p>文本内容</p><!-- 内容结束 -->";

console.log(html.match(reg));
// 输出  ["<!-- 内容开始 -->", "<!-- 内容结束 -->"]

惰性匹配的匹配过程是从最少的情况开始匹配,不符合就增加匹配的部分。而贪婪匹配的匹配过程是从最多的情况开始匹配,不符合就减少匹配的部分。

正则表达式的模式

全局模式

我们在使用正则表达式进行匹配时,通常只会返回首次匹配的结果,需要多次调用匹配方法才能返回所有匹配结果。

JavaScript中,提供了一个全局模式,让我们可以轻松地返回所有匹配结果。全局模式的使用方式很简单,就是在表达式的最后加上一个g,需要注意的是,这个g^$等符号一样,应该放在表达式之外。例如:

var a = "123vdsh234dd78s";
console.log(a.match(/\d+/g));
//["123", "234", "78"]

js语言中,还有另外一种匹配方式是调用exec()方法。

var str = "123vdsh234dd78s";
var reg = /\d+/g;
var match;

while (match = reg.exec(str)) {
  console.log(match[0]);
}

//123
//76
//4

exec()方法在匹配成功时,会返回一个对象, 其中包含一次匹配结果的相关信息。若匹配失败,则会返回null

上述代码的reg变量必须使用全局模式,在使用全局模式时,正则表达式对象实际上存储了一个索引位置(``lastIndex),用于标识上一次匹配成功的结束为止。多次调用exec()方法时,都会从lastIndex位置开始尝试匹配,直到匹配失败位置。当没有使用全局模式时,lastIndex0,则exec()每次都是从第一个位置开始匹配,最终会导致while()出现死循环。

同时需要注意的是,对应的正则表达式必须赋值给变量之后,使用变量来执行上述的匹配。若直接使用字面量的形式,会导致每次while()的迭代执行时,都会创建一个新的正则表达式对象尝试进行匹配,同样会陷入死循环。

忽略大小写

忽略大小写也是一种常用的模式,启用了忽略大小写之后,[a-z]等价于[a-zA-Z]。忽略大小写使用字母i来启用。例如:

var str = '1a2A3a';
var reg = /a/ig;
str.replace(reg,'b');
// "1b2b3b"

单行模式

在单行模式下,.匹配可以包含换行符在内的任意字符。使用字母s来启用。

多行模式

多行模式主要是用来改变^$两个符号的意义,让其不仅可以匹配字符串的开始位置和结束位置,还可以匹配每一行文字的开始与结束位置。

使用m来启用。

多行模式和单行模式之间并不是互补或者反义的关系,这里需要特别注意。

粘连模式

在正则表达式的匹配过程中,如果需要连续进行匹配,每一次匹配都会从上一次匹配结束的位置开始进行尝试。

有时我们希望在上一次匹配结束位置进行匹配尝试时,只要匹配失败就退出匹配过程,不要改变起始位置继续进行尝试。这种方式以便被称为粘连模式( sticky ),在不同语言中有不同的使用方式

ES6中,引入了模式修饰符y,用于表示粘连模式。

例如:

var reg = /([a-z]\w+),?/gi;
var str = "fdf5,f38s,dfk4,4d_f";
var match;
while (match = reg.exec(str)) {
  console.log(match[1]);
}
//fdf5
//f38s
//d_f
//dfk4

上述代码我们原本需要的是输出以逗号隔开且首字符为英文字母的片段,但是我们得到了一个意外的值:d_f

当我们使用粘连模式时:

var reg = /([a-z]\w+),?/giy;
var str = "fdf5,f38s,4d_f,dfk4";
var match;
while (match = reg.exec(str)) {
  console.log(match[1]);
}
//fdf5
//f38s

可以看到,匹配到4d_f时,已经不符合要求,此时匹配并没有继续去进行尝试之后的字符,所以没有输出最后一个符合要求的dfk4

粘连模式并不是一个常用的模式,一般用于检验特定文本是否完全严格符合预设的规则

JavaScript正则的书写

在JavaScript中,最常用的书写方式是使用字面量方式。

var reg = /[\d]+/;

字面量方式,在正则表达式前后分别使用斜杠/来作为界定符,用于区分该部分是否为正则表达式。当我们需要使用模式时,则直接在后面得斜杠之后加上对应的模式即可。

另一种方式则是构造器方式

var reg = new RegExp("http://[a-z]+\\.google\\.com");

他们之间的区别在于:

  • 构造器方式使用字符串作为参数,不需要使用斜杠/当做界定符,也不需要对表达式内部的斜杠进行转义。
  • 在js语言的字符串中,反斜杠\是具有特殊意义的,用于转移,所以在使用时,需要再加一个反斜杠,对反斜杠本身进行一个转义。主要注意的是,不仅是表达是内部,在js的字符串中书写也需要进行转义。

如果需要在构造器中使用模式,则需要为狗在其提供第二个字符串参数。

var reg = new RegExp("^[a-z]:(\\\\[\\w]+)+$", "i");

总结

正则表达式,是一种非常强大,可以让我们轻松检索、替换符合规则的文本。让我们可以省下很多时间和过程,但是同时需要注意的是,使用的表达式结构越长越复杂,阅读难度也就越大。所以我们在使用的时候还是需要尽量熟练掌握正则的编写规则,避免忽略掉某些必要的因素,导致无法获得我们最终想要的结果。