还不会正则吗?或者会,但需要经常查语法。
来试试本文,速学/速查正则表达式。
本文来自本人原创,可查看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.index为0,然后是4,最终res为null,循环此结果 | 
i 忽略大小写
ignoreCase,匹配时忽略大小写。
 测试:
exec()。
| 测试结果 | aBc | 
|---|---|
/abc/ | 
null | 
/abc/i | 
['aBc', index: 0, input: 'aBc', groups: undefined] | 
m 多行匹配
multiline ,一个多行输入字符串被看做多行。
例如,使用了m标志^和$将会从"只匹配字符串的开头或结果",变为"匹配字符串中任一行的开头或结尾"。
 测试:
使用正则表达式不断 exec() 字符串,记 exec() 的结果为 res。
            
            
              js
              
              
            
          
          const str1 = `abc
ab`;
        | 测试结果 | str1 | 
|---|---|
/^a/g | 
res.index先为0,再次调用则res为null,循环此结果 | 
/^a/mg | 
res.index为0,然后是4,最终res为null,循环此结果 | 
s 点号匹配所有字符
. 匹配除换行符外的任意字符,如果开启该标志,它也会匹配换行符,见[. - 匹配换行符外的任意字符](#. - 匹配换行符外的任意字符 "#%E5%8C%B9%E9%85%8D%E6%8D%A2%E8%A1%8C%E7%AC%A6%E5%A4%96%E7%9A%84%E4%BB%BB%E6%84%8F%E5%AD%97%E7%AC%A6")。
其他
还有其他的 flag,但是用途比较少,用到的时候再总结吧,有:u(unicode)、y(sticky,粘性匹配)。
元字符
正则表达式规定的特殊代码,类似于关键字。
这里只列出常用的元字符,许多不常用的诸如\a(报警字符)、\f(换页符)、\e(Escape) 等就不列出来了,后续有觉得有用的再补充。
^ 匹配字符串的开头
除了匹配字符串的开头,还有反向匹配的用法[^],见下文。
$ 匹配字符串的结尾
匹配字符串的结尾。
. 匹配换行符外的任意字符
换行符指 \n,如果正则字符串的标志里有 s(点号匹配所有字符),它也会匹配换行符。
 测试:
test()。
            
            
              js
              
              
            
          
          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.index为5 | 
null, 下划线算是单词的一部分 | 
res.index为5,短横杠可以 | 
res.index为5,斜杠可以 | 
res.index为4,这里匹配到了,所以对于某些语言来说,"边界"真的不好定 | 
量词
量词表示要匹配的字符或表达式的数量。
* 匹配 0 次或多次
+ 匹配 1 次或多次
{n} 匹配 n 次
{n,} 至少匹配 n 次
{n,m} 匹配 n ~ m 次
 测试: 这几个都很好理解,索性都放一起测试了。
exec(),记 exec() 的结果为 res。
| 测试结果 | goooogle | 
|---|---|
/(o*)/g | 
res.index为0,res[0]为空字符串,因为没匹配到字符,继续执行exec()也不会继续往后搜索。手动设置正则的lastIndex为1后,可以继续执行。 | 
/(o+)/g | 
res.index为1,res[0]为oooo,继续执行,res为 null,循环此结果 | 
/(o{2})/g | 
res.index为1,res[0]为oo,继续执行,res.index为3,然后res为 null,循环此结果 | 
/(o{3})/g | 
res.index为1,res[0]为ooo,继续执行,res为 null,循环此结果 | 
/(o{4})/g | 
res.index为1,res[0]为oooo,继续执行,res为 null,循环此结果 | 
/(o{5})/g | 
null | 
/(o{3,})/g | 
res.index为1,res[0]为oooo,继续执行,res为 null,循环此结果 | 
/(o{5,})/g | 
null | 
/(o{2,5})/g | 
res.index为1,res[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 以外的任意字符。
也可以写连字符,规则和[[] 字符集](#[] 字符集 "#%E5%AD%97%E7%AC%A6%E9%9B%86")一致。
 测试:
专门测试连字符 -。可以看到结果正好和"[] 字符集"相反。
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个分组。
            
            
              js
              
              
            
          
          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是同样的结果。
            
            
              js
              
              
            
          
          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组。
            
            
              js
              
              
            
          
          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组,也就是相当于...res。res为exec()的结果。
            
            
              js
              
              
            
          
          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(),如果参数是一个带捕获组的正则,那么捕获到的内容也会按组拼接到返回数组里。
            
            
              js
              
              
            
          
          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]分别为s 和go!,直到null | 
 测试:
使用 () 捕获组 中的例子。
replace(reg, ' <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 = = = = = = 1====== </math>1======2'),记 replace() 的结果为 res
| 测试结果 | Let's go! | 
|---|---|
/(?:[a-z]+)(?:\W+)/g | 
res为"Let======'s====== go======!"',可以看到replace()中的$n不受影响 | 
但是replace()的第二个参数为函数时,因为exec()的返回并不包含组了,所以参数队列里第2个为匹配的位置,第3个为原始输入,之后就是undefined了。
(?<Name>rep) 具名捕获组
可以指定组名的捕获组。
            
            
              js
              
              
            
          
          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, ' <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 = = = = = = 1====== </math>1======2'),记 replace() 的结果为 res
| 测试结果 | Let's go! | 
|---|---|
/(?<some>[a-z]+)(?<thing>\W+)/g | 
res为"Let======'s====== go======!"',可以看到replace()中的$n不受影响 | 
replace()的第二个参数为函数时表现也和普通捕获组相同,因为exec()的返回的数组一样。
\1 \2 引用捕获组
上面说的捕获组的使用,都是在正则表达式的外部。有些时候我们需要在表达式内部去使用之前捕获的组,比如匹配 html 字符串。
            
            
              js
              
              
            
          
          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。
| 测试结果 | 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中的透明度 |