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

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

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

标志

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

g 全局匹配

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

好记性不如烂笔头!学习和查阅正则表达式👊🏻 测试:
使用正则表达式不断 exec() 字符串,记 exec() 的结果为 res。

测试结果 abcdabc
/abc/ res.index一直是0
/abc/g res.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/g res.index先为0,再次调用则resnull,循环此结果
/^a/mg res.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';
测试结果 str1 str2 str3 str4
/^.+$/g true false true true
/^.+$/gs true true true true

\d 匹配数字

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

好记性不如烂笔头!学习和查阅正则表达式👊🏻 测试:
test()。

测试结果 1998 19.98 1e+2
/^\d+$/ true false,小数不行 false,科学计数法也不行

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

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

好记性不如烂笔头!学习和查阅正则表达式👊🏻 测试:
test()。

测试结果 hello hel_lo hello2 你好 enchanté
/^\w+$/ true true true false,汉语不行 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。

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

量词

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

* 匹配 0 次或多次

+ 匹配 1 次或多次

{n} 匹配 n 次

{n,} 至少匹配 n 次

{n,m} 匹配 n ~ m 次

好记性不如烂笔头!学习和查阅正则表达式👊🏻 测试:
这几个都很好理解,索性都放一起测试了。
exec(),记 exec() 的结果为 res。

测试结果 goooogle
/(o*)/g res.index0res[0]空字符串,因为没匹配到字符,继续执行exec()也不会继续往后搜索。手动设置正则的lastIndex1后,可以继续执行。
/(o+)/g res.index1res[0]oooo,继续执行,res为 null,循环此结果
/(o{2})/g res.index1res[0]oo,继续执行,res.index3,然后res为 null,循环此结果
/(o{3})/g res.index1res[0]ooo,继续执行,res为 null,循环此结果
/(o{4})/g res.index1res[0]oooo,继续执行,res为 null,循环此结果
/(o{5})/g null
/(o{3,})/g res.index1res[0]oooo,继续执行,res为 null,循环此结果
/(o{5,})/g null
/(o{2,5})/g res.index1res[0]oooo,继续执行,res为 null,循环此结果
/(o{3,3})/g 等同于/(o{3})/g
/(o{4,4})/g 等同于/(o{4})/g
/(o{4,3})/g n > 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。

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

好记性不如烂笔头!学习和查阅正则表达式👊🏻 测试:
专门测试连字符 -。
test()。

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

| 或

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

好记性不如烂笔头!学习和查阅正则表达式👊🏻 测试:
test()。

测试结果 app22ex orangex
/(app\d{2}e|orange)x true true

反义

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

元字符反义

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

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

[^] 反向字符集

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

好记性不如烂笔头!学习和查阅正则表达式👊🏻 测试:
专门测试连字符 -。可以看到结果正好和“[] 字符集”相反。
test()。

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

分组

() 捕获组

匹配 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======\'); // 加反义符也没用,"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+)/g res[0]et'没有res[1]res[2] ,继续执行,res[0]分别为go!,直到null

好记性不如烂笔头!学习和查阅正则表达式👊🏻 测试:
使用 () 捕获组 中的例子。
replace(reg, ‘1======1======2′),记 replace() 的结果为 res

测试结果 Let’s go!
/(?:[a-z]+)(?:\W+)/g res"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+)/g res"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); // 后面的  引用了前面括号里匹配到的 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。

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

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

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

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

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

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

原文链接:https://juejin.cn/post/7316343619107438627 作者:剑齿丶

(0)
上一篇 2023年12月26日 上午11:13
下一篇 2023年12月26日 下午4:06

相关推荐

发表回复

登录后才能评论