好记性不如烂笔头!学习和查阅正则表达式👊🏻

512 阅读12分钟

还不会正则吗?或者会,但需要经常查语法。
来试试本文,速学/速查正则表达式。

本文来自本人原创,可查看Blog,搭配页面内嵌入的正则表达式测试工具来获取最佳体验。
希望大家多多提建议或者意见,或者提出有意思的测试用例,欢迎评论。

标志

描述正则表达式匹配的整体规则。
如果是字面量正则,直接附在后面即可,如/abc/g,如果是用构造函数声明,则放在构造函数的第二个参数里,如new RegExp('abc', 'g')
可以并行使用,比如 /abc/igm
可以使用RegExp.prototype.flags获取某字符串的标志,返回一个字符串。

g 全局匹配

global,找到所有的匹配,而不是在第一个匹配之后停止。

测试:
使用正则表达式不断 exec() 字符串,记 exec() 的结果为 res。

测试结果abcdabc
/abc/res.index一直是0
/abc/gres.index0,然后是4,最终resnull,循环此结果

i 忽略大小写

ignoreCase,匹配时忽略大小写。

测试:
exec()。

测试结果aBc
/abc/null
/abc/i['aBc', index: 0, input: 'aBc', groups: undefined]

m 多行匹配

multiline,一个多行输入字符串被看做多行。
例如,使用了m标志^$将会从“只匹配字符串的开头或结果”,变为“匹配字符串中任一行的开头或结尾”。

测试:
使用正则表达式不断 exec() 字符串,记 exec() 的结果为 res。

const str1 = `abc
ab`;
测试结果str1
/^a/gres.index先为0,再次调用则resnull,循环此结果
/^a/mgres.index0,然后是4,最终resnull,循环此结果

s 点号匹配所有字符

. 匹配除换行符外的任意字符,如果开启该标志,它也会匹配换行符,见. - 匹配换行符外的任意字符

其他

还有其他的 flag,但是用途比较少,用到的时候再总结吧,有:u(unicode)、y(sticky,粘性匹配)。

元字符

正则表达式规定的特殊代码,类似于关键字。
这里只列出常用的元字符,许多不常用的诸如\a(报警字符)、\f(换页符)、\e(Escape) 等就不列出来了,后续有觉得有用的再补充。

^ 匹配字符串的开头

除了匹配字符串的开头,还有反向匹配的用法[^],见下文。

$ 匹配字符串的结尾

匹配字符串的结尾。

. 匹配换行符外的任意字符

换行符指 \n,如果正则字符串的标志里有 s(点号匹配所有字符),它也会匹配换行符。

测试:
test()。

const str1 = '1a^&˙˚sd©ß∂å≈åß∂∆åø$b%c^';
const str2 = `a$b%c^
ab`;
const str3 = '1\n2';
const str4 = '1\n3';
测试结果str1str2str3str4
/^.+$/gtruefalsetruetrue
/^.+$/gstruetruetruetrue

\d 匹配数字

digit,等同于[0-9],只匹配0123456789这10个字符。

测试:
test()。

测试结果199819.981e+2
/^\d+$/truefalse,小数不行false,科学计数法也不行

\w 匹配字母、数字、下划线

word,等同于[A-Za-z0-9_]强调一下,\w 也匹配数字!

测试:
test()。

测试结果hellohel_lohello2你好enchanté
/^\w+$/truetruetruefalse,汉语不行false,有些语言里带注音?的英文也不行

\s 匹配任意空白符

space,匹配一个空白字符,包含空格、制表符、换页符和换行符,等价于[\f\n\r\t\v\u0020\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]
基本包含了所有的空白符了,测试用例也不好写,不测了。

\b 匹配单词的开始或结束

border,匹配一个词的边界,比如在字母和空格之间。
匹配中不包括边界,也就是说,一个匹配的词的边界内容长度为 0。

JavaScript 的正则表达式引擎将特定的字符集定义为“字”字符。 不在该集合中的任何字符都被认为是一个断词。这组字符相当有限:它只包括大写和小写的罗马字母,十进制数字和下划线字符。 不幸的是,重要的字符,例如“é”或“ü”,被视为断词。

以上是 mdn 的注释,我理解的意思是,\b 所谓的”单词“,并不满足所有的语言系统。

测试: exec(),记 exec() 的结果为 res。

测试结果somethingsome thingsome_thingsome-thingsome/thingsométhing
/\bt/nullres.index5null, 下划线算是单词的一部分res.index5,短横杠可以res.index5,斜杠可以res.index4,这里匹配到了,所以对于某些语言来说,“边界”真的不好定

量词

量词表示要匹配的字符或表达式的数量。

* 匹配 0 次或多次

+ 匹配 1 次或多次

{n} 匹配 n 次

{n,} 至少匹配 n 次

{n,m} 匹配 n ~ m 次

测试: 这几个都很好理解,索性都放一起测试了。
exec(),记 exec() 的结果为 res。

测试结果goooogle
/(o*)/gres.index0res[0]空字符串,因为没匹配到字符,继续执行exec()也不会继续往后搜索。手动设置正则的lastIndex1后,可以继续执行。
/(o+)/gres.index1res[0]oooo,继续执行,res为 null,循环此结果
/(o{2})/gres.index1res[0]oo,继续执行,res.index3,然后res为 null,循环此结果
/(o{3})/gres.index1res[0]ooo,继续执行,res为 null,循环此结果
/(o{4})/gres.index1res[0]oooo,继续执行,res为 null,循环此结果
/(o{5})/gnull
/(o{3,})/gres.index1res[0]oooo,继续执行,res为 null,循环此结果
/(o{5,})/gnull
/(o{2,5})/gres.index1res[0]oooo,继续执行,res为 null,循环此结果
/(o{3,3})/g等同于/(o{3})/g
/(o{4,4})/g等同于/(o{4})/g
/(o{4,3})/gn > m,直接报错,Uncaught SyntaxError: Invalid regular expression: /o{4,3}/g: numbers out of order in {} quantifier

? 懒惰匹配

量词默认是贪婪的,也就是尽可能找到更多的匹配
有时候我们需要懒惰匹配,也就是尽可能找到更少的匹配,只需要在上述量词后面加一个?

  • *?  重复任意次,但尽可能少重复
  • +?  重复1次或更多次,但尽可能少重复
  • ??  重复0次或1次,但尽可能少重复,实际上跟单个?一样
  • {n,m}?  重复n到m次,但尽可能少重复
  • {n,}?  重复n次以上,但尽可能少重复

测试:
exec(),记 exec() 的结果为 res。

测试结果aabab
/a.*b/res[0]aabab,找到了尽可能长的匹配项
/a.*?b/res[0]aab,到这里就满足要求了,不再继续,懒惰

分支条件

[] 字符集

我们也可以用[]轻松指定一个字符范围,只需要在方括号里列出它们,比如[aeiou]匹配任何一个英文元音字母,[.?!]匹配标点符号(.或?或!)。
可以使用连字符-来指定字符范围,但如果连字符用的不规范会被当做普通-处理。

[]中的特殊字符不用加上反斜杠\转义,除非想在[]中列出和][也可以不加转义符。

关于[]里匹配\,很疑惑。比如我想匹配 \a 这个字符串,写[\]a会被认为]为一组,没有闭合的中括号,直接报错;写[\\]a则被认为是两个连续的\,只能匹配\a。没搞懂,因此下面示例中不再测试[]里带\的情况。

测试:
exec(),记 exec() 的结果为 res。

测试结果openAiopen.iopen[iopen]i
/open[AB.]i/res[0]openAires[0]open.inullnull
/open[AB.[]]i/null,这里[被当做[,后面的]把中括号闭合了,再后面的]被当做普通字符,匹配不到]i,所以失败nullnullnull
/open[AB.[]i/res[0]openAires[0]open.ires[0]open[i,中括号里的[不用加转义符null
/open[AB.[]]i/res[0]openAires[0]open.ires[0]open[ires[0]open]i

测试: 专门测试连字符 -。
test()。

测试结果openbopendopen-
/open[a-c]/truefalsefalse
/open[a-]/false-在这里是普通连字符falsetrue
/open[-c]/false-在这里是普通连字符falsetrue
/open[a-1]/直接报错,Uncaught SyntaxError: Invalid regular expression: /open[a-1]/: Range out of order in character class
/open[1-c]/true,数字到字母可以falsefalse

| 或

js 里常见的||在正则里是单竖线|
写法也和 js 里差不多,每个单独的条件不需要加括号,直接可以写作str1|str2|str3,条件里也可以加上别的特殊语法,如元字符、量词等。
括号一般用于不引起歧义、或者分支条件的边框。

测试:
test()。

测试结果app22exorangex
/(app\d{2}e|orange)xtruetrue

反义

有时候需要反向查找,比如除了数字以外,其他任意字符都行。

元字符反义

对于上面的几个元字符,直接把小写换成大写,就是对应的反义。

反义说明
\W匹配任意不是字母、数字、下划线的字符
\S匹配任意不是空白符的字符
\D匹配任意不是数字的字符
\B匹配任意不是单词开头或结束的位置

[^] 反向字符集

[] 是字符集,里面是的关系;^ 匹配开头。两者结合却是反义。
比如:[^abc] 匹配除了 abc 以外的任意字符。
也可以写连字符,规则和[] 字符集一致。

测试:
专门测试连字符 -。可以看到结果正好和“[] 字符集”相反。
test()。

测试结果openbopendopen-
/open[^a-c]/falsetruetrue
/open[^a-]/true-在这里是普通连字符truefalse
/open[^-c]/true-在这里是普通连字符truefalse
/open[^a-1]/直接报错,Uncaught SyntaxError: Invalid regular expression: /open[a-1]/: Range out of order in character class
/open[^1-c]/false,数字到字母可以truetrue

分组

() 捕获组

匹配 exp 并记住匹配项。例如,/(foo)/匹配并记住foo bar中的foo

捕获组会带来性能损失。如果不需要收回匹配的子字符串,请选择非捕获括号。

mdn 说捕获组会带来性能损失,但是我觉得并不会损失很多。
测试项目较多,且都比较重要,此节不再使用表格列出的形式测试。

对于 exec()

对于exec()会体现在exec()的结果里,数组的第n项,就是第n个分组。

const pattern = /([a-z]+)(\W+)/g;
const str1 = "Let's go!";

// 在这个示例里,第一次匹配的结果为["et'","et","'"],其中:
// res[0] 为匹配的结果,et'
// res[1] 为匹配到的第一个分组,也就是正则表达式里的第一组括号内的字符,et
// res[2] 为匹配到的第二个分组,也就是正则表达式里第二组括号内的字符,'
// 继续匹配,同理可得 ["s ", "s", " "] 和 ["go!", "go", "!"]
pattern.exec(str1);

捕获组可以嵌套,对于上面的例子,/([a-z]+)(\W+)/g/([a-z]+(\W+))/g是同样的结果。

const pattern = /([a-z]+(\W+))(\d+)/g;
const str1 = "Let'1s 2go!3";

// 第一次匹配的结果为["et'1","et","'", "1"],可以看到组的顺序是从左到右从外到里。
// 继续匹配,同理可得 ["s 2", "s", " ", "2"] 和 ["go!3", "go", "!", "3"]
pattern.exec(str1);

对于 String.prototype.replace()

对于 String.prototype.replace(),可以直接使用$n来代指匹配到的组,比如$1就是第1组。

const pattern = /([a-z]+)(\W+)/g;
let str1 = "Let's go!";

str1 = str1.replace(pattern, '$1======$2'); // "Let======'s====== go======!"
str1 = str1.replace(pattern, '$'+'1======$2'); // 一样,"Let======'s====== go======!"
str1 = str1.replace(pattern, '$1======\$2'); // 加反义符也没用,"Let======\'s======\ go======\!",不过注意这里单个反义符和两个反义符的区别

String.prototype.replace()的第二个参数还可以是一个函数,函数的返回值就是要替换的项。
函数的参数是一个队列,队列的第1是整体匹配到的字符,第n+1个就是第n组,也就是相当于...resresexec()的结果。

const pattern = /([a-z]+)(\W+)/g;
let str1 = "Let's go!";

str1 = str1.replace(pattern, function(a,b,c) {
    // 打印三次,分别是:
    // { a: "et'", b: "et", c: "'" }
    // { a: "s '", b: "s", c: " " }
    // { a: "go!", b: "go", c: "!" }
    console.log({ a, b, c })
    // 另外,这里也是可以写 $1 的好地方,函数里不认 $1,所以结果是:"Let$1's$1 go$1!"
    return b+'$1'+c;
});

对于String.prototype.split()

对于String.prototype.split(),如果参数是一个带捕获组的正则,那么捕获到的内容也会按组拼接到返回数组里。

const pattern1 = /[a-z]+\W+/g;
const pattern2 = /([a-z]+)\W+/g;
const pattern3 = /([a-z]+)(\W+)/g;
let str1 = "Let's go!";

str1.split(pattern1); // ['L', '', '', ''],全匹配
str1.split(pattern2); // ['L', 'et', '', 's', '', 'go', ''],匹配到的结果也被塞到了数组里
str1.split(pattern3); // ['L', 'et', "'", '', 's', ' ', '', 'go', '!', ''],匹配到的结果也被塞到了数组里

(?:) 非捕获组

匹配 exp,但是不记得组。

测试:
使用 () 捕获组 中的例子。
exec(),记 exec() 的结果为 res

测试结果Let's go!
/(?:[a-z]+)(?:\W+)/gres[0]et'没有res[1]res[2] ,继续执行,res[0]分别为go!,直到null

测试:
使用 () 捕获组 中的例子。
replace(reg, '1======1======2'),记 replace() 的结果为 res

测试结果Let's go!
/(?:[a-z]+)(?:\W+)/gres"Let======'s====== go======!"',可以看到replace()中的$n不受影响

但是replace()的第二个参数为函数时,因为exec()的返回并不包含组了,所以参数队列里第2个为匹配的位置,第3个为原始输入,之后就是undefined了。

(?<Name>rep) 具名捕获组

可以指定组名的捕获组。

const pattern = /(?<some>[a-z]+)(?<thing>\W+)/g;
const str1 = "Let's go!";

// 在这个示例里,第一次匹配的结果为["et'","et","'"],其中res.groups为 { some:"et", thing:"'" };
// 可以看到,数组的返回和普通的捕获组相同,但是一直为空的 groups 变成了具名捕获的一个对象
const res = pattern.exec(str1);

测试:
使用 () 捕获组 中的例子。
replace(reg, '1======1======2'),记 replace() 的结果为 res

测试结果Let's go!
/(?<some>[a-z]+)(?<thing>\W+)/gres"Let======'s====== go======!"',可以看到replace()中的$n不受影响

replace()的第二个参数为函数时表现也和普通捕获组相同,因为exec()的返回的数组一样。

\1 \2 引用捕获组

上面说的捕获组的使用,都是在正则表达式的外部。有些时候我们需要在表达式内部去使用之前捕获的组,比如匹配 html 字符串。

const str1 = '<div><span></span></div>'
const patt1 = /<\w+>.+?</\w+>/
const patt1 = /<(\w+)>.+?</\1>/

patt1.exec(str1); // 这里我们使用了懒惰匹配,所以只匹配到了 <div><span></span> 就结束了
patt2.exec(str1); // 后面的 \1 引用了前面括号里匹配到的 div,所以必须找到 </div> 才算结束,因此结果为 <div><span></span></div> 

零宽断言

zero-width assertions,这些语法像\b^$一样指定一个位置,位置没有宽度,所以称为零宽。这个位置应该满足一定的条件,所以是断言

(?=) 与 (?<=) 在某些内容前或后

(?=)称为先行断言(?<=)称为后行断言。 见到的可能少,但是实际上非常常用。
比如我想在一篇文章里匹配所有以ing结尾的单词,并提取ing前面的部分。
结合我们之前学到的知识,我们可以用分组轻松完成:/\b(\w+)ing\b/g,取匹配到第一组即可。
现在我们不用分组,换个写法试试:
/\b\w+(?=ing\b)/g,这个正则表达式,所有ing\b之前的\b\w+字符,并且不包括ing\b(?=exp)中的exp就是指定这个位置的条件。


与之相反,(?<=exp)指向在某些内容之后的条件。
比如:/(?<=\bre)\w+\b/g匹配所有\bre之后的\w+\b字符。

测试:
exec(),记 exec() 的结果为 res。

测试结果readingsinging
/\w+(?=ing)/res[0]readres[0]sing,这里的匹配是贪婪的,尽可能多地匹配到了sing
/\w+?(?=ing)/res[0]readres[0]s,在前面加个?进行非贪婪匹配
/(?<=re)\w+(?=ing)/res[0]adnull

(?!) 与 (?<!) 不在某些内容前或后

(?!)称为先行否定断言(?<!)称为后行否定断言
和前面一组相反,前面的两个匹配在 xxx 之前或之后,这两个匹配不在 xxx 之前或之后。
比如:匹配小数点后的部分:/\d+(?!.)/匹配3.14的结果就是14,因为3.前面。

测试:

exec(),记 exec() 的结果为 res

测试结果13.24
/\d+(?!\d*.)/gres[0]14,上面的例子小数点前只能匹配一位数字,这个写法可以匹配多个
测试结果rgba(11,222,3, 0.4)
/[\d.]+(?!\d*,)/gres[0]0.4,匹配rgba中的透明度