js正则表达式

177 阅读5分钟

JS正则表达式使用

简介

大多数程序语言都有正则表达式,根据语言不同也都存在部分去呗,这里简单介绍一下在js层面,对正则的一些使用,其他语言可以作为参考

主要作用其实是作为一个开发正则时的示例,因为相对来说平时使用正则的机会是比较少的,而正则又是非常复杂的,不同符号的不同组合都有不同的含义,不经常用到就非常容易遗忘,这个时候看看笔记辅助工具就能快速上手遗忘的部分,也可以作为一个学习参考。正则并不局限于只做校验,也能帮助我们从冗余数据中拿到自己想要的数据

主要参考

大多数内容来自之前写的文章,做了一些调整补充

  1. 以前学js记的笔记
  2. www.w3school.com.cn/jsref/jsref…
  3. developer.mozilla.org/zh-CN/docs/…

通用工具

以正则大全中网址正则为例:

/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/
  • 图解正则:将复杂的正则转化分解,如图,对复杂的正则非常有用,哪怕是层层叠加也可以直观的看到每一步在干什么,有没有问题。但是这个工具也存在部分局限,对于部分正确正则无法解析image-20221229182629891

  • 在线正则验证:可以在各语言环境下检验正则匹配,并分解匹配项,右边有释义。虽然是英文但是理解含义来说可能比mdn\w3c的翻译版更准确,而且检测正确性来说应该是几个网站里最准确的

image-20221229182819074

  • 正则大全:现成正则,平时写正则不多的,直接用这个做校验也够了,他也有vscode插件安装后可以非常方便的引入常用正则

image-20221229183124998

image-20221229183056516

规则

1、最先匹配上的优先,例

'1wer123s'.replace(/1/,'')	//"wer123s"

2、如果需要完全匹配字符需要加上^开始$结尾,否则定义的正则都是只要包含就行

let reg = /1/;
let str = '157788';
reg.test(str);	//true

创建

let reg = /abc/;

let reg = new RegExp("abc");

修饰符g i m

放在正则最后面,可以叠加,比如//gi 就是全局匹配并忽略大小写,除了gi,其他都不常使用。

  • g 表示全局匹配
  • i 忽略大小写
  • m 多行匹配(如下示例,每行都会单独匹配/^mtest/m,这种情况^就不是完全匹配字符串了,不加上起始标识符整体就是全局匹配没区别)
  • s 单行匹配
  • u 使用 unicode 码的模式进行匹配。js提供了一些事件来获取unicode码,有需要可以自行查阅。使用需要注意某些情况
  • y 粘性标志,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始,即粘性
let reg = /1/g;
let str = '1516';
str.split(reg);	// ["", "5", "6"]

let reg = /mtest/g;
let reg2 = /mtest/s;
let reg3 = /^mtest$/m;
let str = `123
mtest
46`;
str.match(reg)	// ['mtest']
str.match(reg2)	// ['mtest', index: 4, input: '123\nmtest\n46', groups: undefined]
str.match(reg3)	// ['mtest', index: 4, input: '123\nmtest\n46', groups: undefined]

/\u{61}/u.test('a') // true,不加u修饰符,正则表达式无法识别\u{61}这种表示法


let s = 'aaa_aa_a';
let r1 = /a+/g;
let r2 = /a+/y;

r1.exec(s) // ["aaa"]
r2.exec(s) // ["aa"]

r1.exec(s) // ["aaa"]
r2.exec(s) // null

mdn示例

带有sticky标志的正则表达式将会从源字符串的RegExp.prototype.lastIndex位置开始匹配,也就是进行“粘性匹配”。

分组()和|或

|表示或,如果分组中没有|,就必须匹配分组内完整的字符串

let reg = /^(1|2)/;	
let str = '22$15';
reg.test(str);	//true

let reg = /^(12)/;	
let str = '22$15';
reg.test(str);	// false

当正则有使用分组时,可以通过RegExp.1取正则对应分组的内容,1取正则对应分组的内容,后跟的数字表示是匹配的正则中第几个分组。

let reg = /^(1)\d*(9)$/;	
let str = '1345789';
reg.test(str);	//true
RegExp.$1 	// '1'
RegExp.$2	// '9'

灵活使用分组可以方便快速解决我们很多问题,是非常重要的一个特性

捕获

let reg = /^(1)\d*(9)$/;	
let str = '1345789';
reg.exec(str);	// ['1345789', '1', '9', index: 0, input: '1345789', groups: undefined]
reg.test(str);	// true
RegExp.$1 	// '1'
RegExp.$2	// '9'

非捕获

(?:x)

匹配 'x' 但是不记住匹配项。这种括号叫作非捕获括号,使得你能够定义与正则表达式运算符一起使用的子表达式。

let reg = /^(?:1)\d*(9)$/;	
let str = '1345789';
reg.exec(str);	// ['1345789', '9', index: 0, input: '1345789', groups: undefined]
reg.test(str);	// true
RegExp.$1 	// '9',
RegExp.$2	// ''

需要转义的特殊字符

特殊字符:() [] {} ^ $ * ? \ | + . 需要用\转义。其实就是在正则使用场景有特殊含义的就需要转义

let reg = /\$/;
let str = '15$';
reg.test(str);	//true

元字符(特殊含义缩写字符)

特殊字符(换行等)\t \n \f ...

***注意:***空格符没有特殊字符,直接输入空格即可

let reg = / /g;
let str = `a b`;
reg.test(str);	// true

let reg = / /g;
let str = `ab`;
reg.test(str);	// false

符号含义
\t制表符
\n换行符
\f换页符
\r回车符
\0NUL 字符
\v垂直制表符
\xxx以八进制数 xxx 规定的字符
\xdd查找以十六进制数 dd 规定的字符。
\u{xxxx}匹配一个四位十六进制数 xxxx 规定的 Unicode 字符,使用需要添加u修饰符
let reg = /\n/;
let str = `a
b`;
reg.test(str);	// true

类 (字符集缩写). \d \s \w ...

字符集含义
.[^\n\r]非换行、回车符
\d[0-9]数字
\D[^0-9]非数字
\s[ \t\n\x0B\f\r]空白(\x0B表示垂直tab,无法书写),含空格
\S[^ \t\n\x0B\f\r]非空白
\w[a-zA-Z_0-9]单词字符(字母数字下划线)
\W[^a-zA-Z_0-9]非单词
let reg = /\d/;
let str = '15$';
reg.test(str);	//true

断言(位置相关操作)

分界符^ $ \b \B

  • ^ 以某字符开头

  • $ 已某字符结尾

    let reg = /^1/;
    let str = '16+19';
    reg.test(str);  // true
    
字符含义
^n以n开头
n$以n结尾
\b单词边界(单词字符的边界)例:2a+3b=5c,边界就是2a,3b,5c左右两边的位置
\B非单词边界,如上的例子,非单词边界就是2和a,3和b,5和c之间的位置
let str = 'm13';
let reg1 = /\bm1/;	// \b放左就表示匹配字符m1其左边是边界
let reg2 = /m1\b/;	// \b放右就表示匹配字符m1其右边是边界

reg1.test(str); 	// true,
reg2.test(str); 	//false,

现行&后发断言

模式含义
x(?=y)x后紧跟y, 匹配'x'仅仅当'x'后面跟着'y'.这种叫做先行断言。 例如,/Jack(?=Sprat)/会匹配到'Jack'仅当它后面跟着'Sprat'。/Jack(?=Sprat|Frost)/匹配‘Jack’仅当它后面跟着'Sprat'或者是‘Frost’。但是‘Sprat’和‘Frost’都不是匹配结果的一部分。
(?<=y)`xx前紧跟y, 匹配'x'仅当'x'前面是'y'.这种叫做后行断言。 例如,/(?<=Jack)Sprat/会匹配到' Sprat '仅仅当它前面是' Jack '。/(?<=Jack|Tom)Sprat/匹配‘ Sprat ’仅仅当它前面是'Jack'或者是‘Tom’。但是‘Jack’和‘Tom’都不是匹配结果的一部分。
x(?!y)x后不跟y, 仅仅当'x'后面不跟着'y'时匹配'x',这被称为正向否定查找。 例如,仅仅当这个数字后面没有跟小数点的时候,/\d+(?!.)/ 匹配一个数字。正则表达式/\d+(?!.)/.exec("3.141")匹配‘141’而不是‘3.141’
(?<!y)xx前不跟y, 仅仅当'x'前面不是'y'时匹配'x',这被称为反向否定查找。 例如, 仅仅当这个数字前面没有负号的时候,/(?<!-)\d+/ 匹配一个数字。 /(?<!-)\d+/.exec('3') 匹配到 "3". /(?<!-)\d+/.exec('-3') 因为这个数字前有负号,所以没有匹配到。
let str = 'foofoo';
let reg1 = /foo{2,3}/;
let reg2 =  /(?:foo){2,3}/;
reg1.test(str);	// false
reg2.test(str);	// true

先行断言(前瞻)

符号含义
x(?=y)x其后紧跟 y
x(?!y)x后不跟y
let str="Is this all there is";
let reg=/is(?= all)/g;
reg.test(str) // true

let str="Is this all there is";
let reg=/is(?=all)/g;
reg.test(str) // false

后发断言(后顾)

符号含义
(?<=y)xx前紧跟y
(?<!y)xx前不跟y

字符集[]

基础

let reg = /[a13kc]/;	
let str = '12$15';
reg.test(str);	//true

范围字符集

常用

  • [a-z] 小写子母集合,也可以从中间开始取[b-d]表示bcd
  • [A-Z] 大写子母集合
  • [0-9] 数字集合

特殊含义字符

^非

[^123] 表示不包含1的字符集

\b退格符

[\b]表示退格符(非元字符的单词边界)

注意:

除了 \,-,^, ] 之外的非字母数字字符在字符类中都没有特殊含义,不需要转义

组合拼接[0-9]

常用

[a-zA-Z] 子母集合

[\u4e00-\u9fa5] 中文集合 (汉字Unicode编码

量词? * + {}

符号出现次数(x)
?0 || 1 {0,1}
*x>=0 {0,}
+x>=1 {1,}
{n}0 || n
{n,m}n<= x <=m (中间不能有空格,有空格就不能识别为量词)
{n,}x >= n
let reg = /\$+/;
let str = '15$16$$';
str.split(reg);	// ["15", "16", ""]

/^\d{6}$/.test('123456') // true,带上^$才能标识只匹配6个数字,不带就表示字符串包含6个数字就为true

模式

参考较多

性能优化探究影响性能的因素NFA自动机的回溯

正则表达式的回溯方式

正则表达式三种模式:贪婪模式、懒惰模式、独占模式

js正则表达式引擎、贪婪与懒惰

js量词-MDN

注意:

1、同贪婪模式一样,独占模式一样会匹配最长。不过在独占模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯,可用于性能优化

2、js不支持独占模式,会报错

贪婪(量词, 匹配尽可能多的字符串)懒惰(量词?, 一旦找到匹配就会停止)独占(量词+,不支持)
X?X??X?+
X*X*?X*+
X+X+?X++
X{n}X{n}?X{n}+
X{n,}X{n,}?X{n,}+
X{n,m}X{n,m}?X{n,m}+

贪婪模式(最长匹配).+ , .*

1、量词默认贪婪模式(会先匹配最长的,匹配不上再回溯,往前推。所以量词要避免嵌套否则会导致指数级回溯)

let reg = /b{1,3}/;
let str = 'aaabbc';
str.match(reg) // ['bb', index: 3, input: 'aaabbc', groups: undefined]

2、/a.*b/表示匹配a到b的最长字符串

let reg = /a.*b/;	// .在元字符类(下文)中表示非(换行&回车符),.*即表示存在>=0个字符,放在中间会触发最长匹配
let str = '15a$a16b$b$bc';
str.match(reg) // ['a$a16b$b$b', index: 2, input: '15a$a16b$b$bc', groups: undefined]
str.split(reg);	// ["15", "c"]
str.replace(reg, '-');	//'15-c'

懒惰模式(最短匹配) .*?

1、量词后加?,变成懒惰模式

let reg = /b{1,3}?/;
let str = 'abbc';
reg.test(str)	//true

let reg = /b{1,3}?/;	// 懒惰
let str = 'aaabbc';
str.match(reg)	// 先匹配最短['b', index: 3, input: 'aaabbc', groups: undefined]
str.split(reg)	// ['aaa', '', 'c']

let reg = /b{1,3}/;	// 贪婪
let str = 'aaabbbc';
str.match(reg)	// 先匹配最长['bbb', index: 3, input: 'aaabbbc', groups: undefined]

2、/a.*?b/表示匹配a到b的最短字符串

let reg = /a.*?b/;
let str = '15a$a16b$b$bc';
str.split(reg);	// ["15", "$b$bc"]

性能

参考

正则也是存在性能问题的,某些优化不好的正则会不断回溯,占用cpu很长时间去匹配,影响页面效果。

以下案例都是比较简单很容易发现,但是在非常复杂且长的正则中出现了这种情况就比较难发现,所以平时写代码的时候就要多关注

console.time('start');
console.log(/(A+A+)+B/.exec('AAAAAAAAAA'));	// (A+A+)+量词嵌套导致回溯
console.timeEnd('end')

image-20210930111111195

在使用量词匹配时尽量使用[最长](#贪婪模式(最长匹配).+ , .*)或[最短](#懒惰模式(最短匹配).+? , .*?)匹配

js正则相关方法

传入方法的数字一般会匹配去对应的字符串

下面两种方法即指的是谁调用的这些方法,有时候可能会记混这个方法倒是正则调用还是字符串调用,可以通过方法英文含义区分,正则调用一般是检测,字符串调用一般是查找匹配

RegExp 正则对象方法

test

含有与 RegExpObject 匹配的文本,则返回 true,否则返回 false。

RegExpObject.test(string)

exec

字符串中的正则表达式的匹配。返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。

RegExpObject.exec(string)
/\d/.exec('21ssfr') // ["2", index: 0, input: "21ssfr", groups: undefined]

let reg = /\[(.*?)\]/g;
let str = `<div>[开心]addff[强壮]24</div>`
reg.exec(str); // (2) ['[开心]', '开心', index: 5, input: '<div>[开心]addff[强壮]24</div>', groups: undefined]
reg.exec(str); // ['[强壮]', '强壮', index: 14, input: '<div>[开心]addff[强壮]24</div>', groups: undefined]


let str = '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}';
let reg = /\{\{\s*(.+?)\s*\}\}/g;

str.match(reg); // ['{{ day.value }}', '{{ day.weather }}']

reg.exec(str); // ['{{ day.value }}', ' day.value ', index: 11, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]
reg.exec(str); 	//  ['{{ day.weather }}', ' day.weather ', index: 29, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]

// matchAll替代多次执行exec,返回迭代器
Array.from(str.matchAll(reg)); // (2) [Array(2), Array(2)]
/*
0: (2) ['{{ day.value }}', ' day.value ', index: 11, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]
1: (2) ['{{ day.weather }}', ' day.weather ', index: 29, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]
length: 2
[[Prototype]]: Array(0)
*/

注意:

1、此数组的第 0 个元素是与正则表达式相匹配的文本,如果有分组的话第 1 个元素是与 RegExpObject 的第 1 个子表达式(括号分组)相匹配的文本,如果有第二个分组的话第 2 个元素是与 RegExpObject 的第 2 个子表达式相匹配的文本

即返回的只有第0个元素才是完全匹配的文本,后面两个(非index、input等属性)都只是子表达式(括号中的分组捕获)匹配的结果

2、当正则表达式设置 g 标志位时,可以多次执行 exec 方法来查找同一个字符串中的成功匹配。当你这样做时,查找将从正则表达式的 lastIndex 属性指定的位置开始。(test() 也会更新 lastIndex 属性)。注意,即使再次查找的字符串不是原查找字符串时,lastIndex 也不会被重置,它依旧会从记录的 lastIndex 开始。

3、exec叠加g标志,多次执行可以用matchAll代替

4、

image-20221230155956722

String 字符串对象方法

search

w3c

stringObject.search(regexp|string)	

返回值

stringObject 中第一个与 regexp 相匹配的子串的起始位置。

注释:如果没有找到任何匹配的子串,则返回 -1。

说明

search() 方法不执行全局匹配,它将忽略标志 g。它同时忽略 regexp 的 lastIndex 属性,并且总是从字符串的开始进行检索,这意味着它总是返回 stringObject 的第一个匹配的位置

let str = '11te3st123'
let reg = /1/;
str.search(reg);	// 0

match

match() 方法可在字符串内检索指定的值,或找到一个或多个(有无全局匹配g标识)正则表达式的匹配。

该方法类似 indexOf() 和 lastIndexOf(),但是它返回指定的值,而不是字符串的位置。

同exec,如果正则中存在分组,match返回的数组第一个值是匹配的字符串,后续的值都是匹配的子表达式(分组)

stringObject.match(string | regexp)

let str = 'test123';
let reg = /^test/;
str.match(reg);		// ["test", index: 0, input: "test123", groups: undefined]

let searchStr = 'st';
str.match(searchStr);	// ["st", index: 2, input: "test123", groups: undefined]

let str = '11te3st123';
let reg = /1/g;
str.match(reg); // ["1", "1", "1"]

matchAll

使用 matchAll ,就可以不必使用 while 循环加 exec 方式(且正则表达式需使用 /g 标志)。使用 matchAll 会得到一个迭代器的返回值,配合 for...of, array spread, 或者 Array.from() 可以更方便实现功能:

let str = '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}';
let reg = /\{\{(.+?)\}\}/g;

str.match(reg); // ['{{ day.value }}', '{{ day.weather }}']

reg.exec(str); // ['{{ day.value }}', ' day.value ', index: 11, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]
reg.exec(str); 	//  ['{{ day.weather }}', ' day.weather ', index: 29, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]

// matchAll,返回迭代器
Array.from(str.matchAll(reg)); // (2) [Array(2), Array(2)]
/*
0: (2) ['{{ day.value }}', ' day.value ', index: 11, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]
1: (2) ['{{ day.weather }}', ' day.weather ', index: 29, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]
length: 2
[[Prototype]]: Array(0)
*/

replace

www.w3school.com.cn/jsref/jsref…

replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。

stringObject.replace(regexp/substr,replacement)

replacement: 字符串或函数,函数参数类似reg.exec(str)返回的数组字符串,第一项是匹配正则的字符串,后续为匹配子表达式(分组)的内容

let str="Every man in the world! Every woman on earth!";

patt=/man/g;
str2=str.replace(patt,"person"); // Every person in the world! Every woperson on earth!

patt=/(wo)?man/g; // ?量词匹配0 || 1
patt.compile(patt);
str2=str.replace(patt,"person"); 	// Every person in the world! Every person on earth!

第二个参数使用特殊字符$:

字符替换文本
11、2、...、$99与 regexp 中的第 1 到第 99 个子表达式(分组)相匹配的文本。
$&与 regexp 相匹配的子串。
$`位于匹配子串左侧的文本。
$'位于匹配子串右侧的文本。
$$插入一个 "$"
let reg = /(\w+)\+(\w+)/;
let str = 'a+b';
str.replace(reg, '$2, $1'); // "b, a"
str.replace(reg, '$$ $2');	// $ b
RegExp.$1 // "a"
RegExp.$2 // "b"

let reg = /(\w+)\+(\w+)/;
let str = "--a+b==";
str.replace(reg, "$`, $'"); // 把匹配上正则的a+b替换为左侧,右侧
'----, ===='

第二个参数使用函数

函数参数:

  • 正则匹配的字符
  • 捕获的字符(分组的捕获)
  • 正则匹配字符的索引
  • 字符串主题
let reg = /\w+/g;	// 加上g修饰符才会执行多次
let str = 'a+b+c';
str.replace(reg,(match, char, index, str) => {
    console.log(match) // 分三次返回a、b、c
    return 1
}) // 1+1+1

replace和replaceAll

  1. replace不加g字符那么只会匹配一次
  2. replace和replaceAll匹配全局都必须加上g
  3. replaceAll不加上加上g会报错

结论:replace能实现replaceAll的所有效果,replaceAll只能用于全局匹配

image-20230222175531659

split

split() 方法用于把一个字符串分割成字符串数组。

stringObject.split(string|reg,howmany)

howmany: 可选。该参数可指定返回的数组的最大长度。如果设置了该参数,返回的子串不会多于这个参数指定的数组。如果没有设置该参数,整个字符串都会被分割,不考虑它的长度。

let str = '11te3st123';
let reg = /1/
str.split(reg, 3);	//  ["", "", "te3st"].length <= 3

正则案例

电话:

/^1[3-9]\d{9}$/

网址:

1、会排除特殊字符,也就是地址后面携带的url参数也不会被匹配到

/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/

image-20230224161151254

2、如果要匹配后面携带的参数这样简单匹配会更好,不会排除后续参数

/(ht|f)tps?:\/\/[^\s]*/gi

邮箱:

/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

问题:

用正则怎么解析出字符串内的模板变量

let str = '嗨,您好,今天是星期 {{ day.value }}';
let reg = /\{\{.+?\}\}/;
reg.test(str);
let str = '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}';
let reg = /\{\{\s*(.+?)\s*\}\}/g;

str.match(reg); // ['{{ day.value }}', '{{ day.weather }}']

reg.exec(str); // ['{{ day.value }}', 'day.value', index: 11, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]
reg.exec(str); 	//  ['{{ day.weather }}', 'day.weather', index: 29, input: '嗨,您好,今天是星期 {{ day.value }},天气{{ day.weather }}', groups: undefined]

用正则怎么解析富文本拿到纯文本

let str = `<div class="test">
	23456
	<span>1234</span>
</div>`
let reg = /<.*?>/g
str.replace(reg, '').replace(/\s/, '');

结语