还不会正则吗?或者会,但需要经常查语法。
来试试本文,速学/速查正则表达式。
本文来自本人原创,可查看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中的透明度 |