JS正则表达式
什么是正则
概念:
正则表达式是对字符串(包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为“元字符”))操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。正则表达式是一种文本模式,该模式描述在搜索文本时要匹配的一个或多个字符串。
语法:
/正则表达式内容/模式修正符(可有可无),由两个斜线包裹起来的检索匹配模式。
使用正则的优势
我们在了解了正则的概念之后,通过一个例子来看一下使用正则的优势:
//1.使用正则表达式的优势
//问题描述,找出一串字符中的数字
let hd = "sdsxcakdkl123sdshdk456";
//不使用正则表达式
let nums = [...hd].filter(search => !Number.isNaN(parseInt(search)));
console.log(nums.join(""));
//使用正则表达式
console.log(hd.match(/\d/g).join(""));
不使用正则表示式的情况,看似只有一句代码,实际上它做了很多工作,将hd字符串展开并变为数组,再使用数组的过滤方法对每一个字符进行转换成int的操作并加以判断,最后将过滤好的数组用join方法连接成一个字符串,反观我们的正则表达式,强大。
创建正则表达式的方法
创建正则表达式一般有两种方法:
1.字面量创建正则表达式
2.使用对象创建正则表达式
不多说,我们看代码:
//字面量创建方式
let kk = "https://lockme.gitee.io";
let a = "m";
console.log(/m/.test(kk));
//字面量形式不能直接使用变量a,但是可以间接使用如下,eval里面为字符串模板的符号(esc键下面的键)
console.log(eval(`/${a}/`).test(kk));
//使用对象创建正则表达式,后面的g是模式修正符,后面会讲到
let reg = new RegExp('m','g'); //可以直接使用变量 等同于reg = new RegExp(a,"g")
console.log(reg.test(kk));
介绍一些正则中常用的方法
正则中常使用的方法有:
1.正则表达式的方法(由正则表达式使用)test和exec
1.1 正则表达式.test(string),表示使用前面的正则表达式检索参数中传入的字符串,匹配成功返回true,
匹配失败返回false。
1.2 正则表达式.exec(string),表示使用前面的正则表达式检索参数中传入的字符串,但是匹配成功返回的是匹配的值,单次匹配与test方法差不多,但是多次匹配的话,它会记录一个lastIndex(后面会讲)的值,下次匹配从lastIndex的下一位置继续匹配。
2.字符串的方法search、match、matchAll、split、replace
2.1 字符串.search(string | RegExp),表示搜素指定字符串,返回索引,失败则返回-1,支持传入正则表达式作为参数。
2.2 字符串.match(string | RegExp),搜索指定字符串,返回一个数组,里面装的所有匹配的值,支持传入正则表达式作为参数。
2.3 字符串.matchAll(string | RegExp),全局匹配, 方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。
2.4 字符串.split(string),拆分,以传入的参数字符串拆分,返回拆分后元素的数组,支持传入正则,返回拆分成的数组。
2.4 字符串.replace(string | RegExp,“xxx” | function),将指定的字符串替换,可以传入替换的字符串或者是实现替换的回调函数,返回替换后的字符串。
选择符的使用
选择符 | 表示or的关系,/表达式1 | 表达式2/ 表示只匹配其中的一个即可
元字符的使用
只列举一些常用的,完整的可以自行百度查看。
元字符 | 内容 |
---|---|
\d | 匹配数字字符 |
\D | 匹配除了数字的字符 |
\s | 匹配空白字符(回车、空格、制表符等) |
\S | 匹配除了空白字符的字符 |
\w | 匹配字母、数字、下划线 |
\W | 匹配除了字母、数字、下划线的字符 |
. | 除了换行符\n的所有字符 |
^ | 表示以什么字符开始 |
$ | 表示以什么字符结束 |
量词的使用
同样只列举一些
量词 | 内容 |
---|---|
{n} | 表示出现的字符串的长度为n |
{m,n} | 表示出现的字符串的长度为m~n之间 |
{m,} | 至少m个 |
? | {0,1},0个或者1个 |
* | {0,},0个或者多个 |
+ | {1,},1个或者多个 |
关于量词还多其他知识,比如量词的分类,贪婪量词,惰性量词和支配性量词,想了解可自行百度。
重复匹配
元字符后面跟量词达到重复匹配的效果,比如:
/d+ 表示匹配一个或者多个数字
/w* 表示匹配0个或者多个字母数字下划线
/s? 表示匹配0个或者一个空白字符
[123]{2,3} 表示匹配2到3个’1’、‘2’、'3’字符中的其中一个
(123){5} 表示匹配5个’123’串
最后两个是下面要讲的原子表和原子组
原子表和原子组
1.原子表
用中括号包括起来的,表示或的关系,只匹配其中的一个字符。
举例:[1234] 表示匹配1,2,3,4四个字符中的一个字符,相当于1 | 2 | 3 | 4。
1.1 区间匹配
[a-z]表示所有26个小写字母字符。
[0-9]表示所有数字字符。
[A-Z]表示所有大写字母字符。
1.2 排除匹配
[^ue]表示除了ue两个字符均匹配。
1.3 原子表字符不解析
() 和.+ 放入原子表中不会有正则表达式的含义,只保留字符串的本义。
[()]表示匹配括号。
[.+]表示匹配字符’.‘或者’+’
1.4使用原子表匹配任意内容
尽管有元字符.,但也不能匹配任意字符,因为它是除了换行符,那么如何匹配任意内容?我们通常使用
[\s\S],意思是匹配空白字符或者除了空白字符的其他字符,就是任意字符,同样也可以使用[\d\D]等。
2.原子组
用括号包括起来的,表示一个整体,匹配相同整体的内容。
举例:(1234),表示匹配’1234’这样一个整体的字符串
2.1邮箱验证中原子组的使用
<body>
<input type="text" name="email">
<span></span>
</body>
<script>
document.querySelector(`[name="email"]`).addEventListener('keyup',function() {
//一些邮箱的格式 10807396@qq.com 100@sina.com.cn
let reg = /^[\w-]+@([\w-]+\.)+(com|org|cn|net|cc)$/i;
document.querySelector(`span`).innerHTML = reg.test(this.value)?"正确":"错误";
});
</script>
算是文章的真正意义上的第一个正则的例子,我细讲一下表达式的匹配吧,后面的例子就不赘述了,得你自己去看。一般邮箱这样的字符串都得严格规定开始和结束的字符,所以使用^和$,第一个原子组中表示匹配字母数字下划线和-,后跟量词+(这个+是量词,千万不要看成其它意义上的加号了)表示匹配一个或者多个,第一部分^[\w-]+就表示匹配一个或者多个字母、数字、下划线和-并以其开头,@符就是表示匹配@符,下一个[\w+.]表示匹配一个或者多个字母数字下划线和点,因为点在正则中是有含义的,所以使用反斜杠进行转义,用原子组包起来是因为像这样模式的字符串可能不止一组,像100@sina.com.cn这样的就有两组sina.和com.,所以包括起来使用量词一个或者多个,最后匹配以com、org、cn、net、cc结尾的其中一个字符,i表示不区分大小写,是下面要讲的模式修正符。
2.2 原子组引用完成替换操作
<script>
let hd = `
<h1>smallbridge</h1>
<span>前端者</span>
<h2>lockme</h2>
`;
//将h标签变成p标签
let reg = /<(h[1-6])>([\s\S]*)<\/\1>/gi;
//$2表示取第二个原子组中的内容
//console.log(hd.replace(reg,`<p>$2</p>`));
//也可以使用函数的方式,其中传入的参数,p0表示匹配的所有字符串,
//p1表示第一个原子组,p2表示第二个原子组,依次类推
let res = hd.replace(reg,(p0,p1,p2)=>`<p>${p2}</p>`);
// hd.replace(reg,(p0,p1,p2) =>{
// console.log(p0);
// });
console.log(res);
</script>
首先提一下正则中的\1,它表示这里匹配的模式与前面第一个原子组中的模式相同,前面匹配好h1后面也会跟着匹配h1,这个数字是怎么来的,从左往右数,第一个就是\1,第二个就是\2,有人要问为什么不是从0开始?因为0表示匹配的整个字符串。
这个要完成替换就要将标签中的内容提取出来,一般情况下,需要被提取的内容在正则表达式中都使用原子组包起来,然后使用$加序号就可以提取了。
嵌套原子组的序号怎么看?从左往右,从外往里
2.3 不记录组
有时候会出现一个正则表达式中原子组很多的情况,那么其的序号势必会增大,这样会造成不好的后果,不记录组的作用就出来了,将不需要提取值的原子组标记为不记录组。在原子组中使用(?:)表示此组不记录编号,无法使用$编号提取,这样序号就会小很多。
<script>
let hd = `
https://www.lockme.gitee.io
http://github.com
https://hdcms.com
`;
//?表示{0,1}可有可无,s不是必须的,有s没有s均匹配
let reg = /https?:\/\/(\w+\.)+(com|org|io|cc|cn)/ig;
console.dir(hd.match(reg));
//在原子组中使用?:,表示此组不记录编号,无法使用$加编号访问,用于忽略那些不用取值的组
let regg = /https?:\/\/(?:\w+\.)+(com|org|io|cc|cn)/ig;
//第一个原子组忽略记录编号
</script>
2.4 原子组在替换中的使用技巧
<body>
<main>
<a style="color:red" href="http://www.hdcms.com">
开源系统
</a>
<a id="l1" href="http://houdunren.com">后盾人</a>
<a href="http://yahoo.com">雅虎</a>
<h4>http://www.hdcms.com</h4>
</main>
</body>
<script>
//要求 将便签的herf属性中的地址加s和www
const main = document.querySelector('body main');
const reg = /(<a.*href=["'])(http)(:\/\/)(www\.)?(hdcms|houdunren)/g;
//args是es6的语法,剩余参数,允许我们将不定数量的一个参数表示为一个数组
main.innerHTML = main.innerHTML.replace(reg,(v,...args)=>{
console.log(args);
args[1] += 's';
args[3] = args[3] || 'www.';
//修改之后,将修改后的重新连接起来
return args.splice(0,5).join('');
})
</script>
模式修正符
上面已经提到了,在正则表达式的末尾添加模式修正符,常用的有:
模式 | 内容 |
---|---|
i | 不区分大小写 |
g | 全局匹配 |
m | 多行匹配,每一行单独处理 |
u | 宽字节匹配模式 |
s | 单行匹配模式 |
关于多行匹配,我们看一个例子:
<script>
//多行匹配
let hd = `
#1 js,200元 #
#2 php,300元 #
#9 lockme.gitee.io #
#3 node.js,180元 #
`;
//将上述字符串输出成[{"name":"js","price":"200元"}]格式
//m表示多行匹配,就是每一行单独处理,不这样的话,你去写匹配很麻烦的,不信你可以试试
//我中间写的是 .+ 表示匹配除换行的任意字符,如果不多行匹配的话,以第一个为例
//.+就会匹配到 js,200元 # ,然后才是最后的若干空白字符#结尾,整个表达式匹配的内容就会变成
/*
#1 js,200元 #
#
*/
//显然这样是不行的
let lessons = hd.match(/^\s*#\d+\s+.+\s+#$/gm).map(v=>{
//删除不必要的字符
v = v.replace(/\s*#\d+\s*/,"").replace(/\s+#/,"");
//解构赋值
[name,price] = v.split(",");
return {name,price};
});
//以JSON格式打印
console.log(JSON.stringify(lessons,null,2))
</script>
转义字符
上面也已经提到,若要在正则表达式中匹配一些拥有正则含义的字符,应当使用反斜杠进行转义。比如点、$、+、*、?等。
关于转义需要注意一点:
let a = 12.34;
console.log(/\d+\.\d+/.test(a)); //转义.这个符号
//但如果使用字符串形式
let reggg = new RegExp('\d+\.\d+','g');
//将会被解析成d+.d+,表示的意思是若干d任意字符若干d,因为在字符串中 \d就等于d \.就等于 .
//你可以试着输出一下 'd' === '\d',看是否为true
//无法匹配
console.log(reggg.test(a));
//所以需要再次转义
let re = new RegExp('\\d+\\.\\d+','g');
console.log(re.test(a))
汉字与字符属性
/\p{xxx}/u 这样的正则表达式表示只匹配某类字符,根据字符属性确定,比如:
/\p{L}/u 只匹配字母
/\p{P}/u 只匹配标点符号
/\p{sc=Han}/u 只匹配中文
其他的字符属性可查看百度
lastIndex的作用
正则表达式在不加模式修正符g的情况下是只会匹配一次的,后面再有符合条件的内容也不会再匹配了。
在正则表达式的方法中有一个exec()方法,它会记录上一次匹配的位置(lastIndex),下一次匹配的时候紧接着上一次匹配的位置开始匹配,仅在全局模式下,非全局模式始终匹配第一个。
使用exec完成全局匹配:
<script>
//在全局模式下(必要条件),正则表达式的exec方法会记录上一次匹配的位置(lastIndex)
let hd = "lockme.gitee.io";
let reg = /\w/g;
console.log(reg.lastIndex);
console.log(reg.exec(hd));
console.log(reg.lastIndex);
console.log(reg.exec(hd));
// while(res = reg.exec(hd)){
// console.log(res);
// }
</script>
批量使用正则匹配同一字符串
有时候会有非常复杂的字符串,想要写出匹配它的正则表达式十分困难,这时我们就可以将其进行拆分,拆分成满足若干条件的字符串,这样或许就会简单许多。不多说,我们看例子:
<body>
<input type="text" name="password">
</body>
<script>
const input = document.querySelector(`[name="password"]`);
input.addEventListener("keyup", e=>{
const value = e.target.value;
//批量使用正则表达式
const regs = [
/^[a-z0-9]{5,9}$/i,
/[A-Z]/,
/[0-9]/
]
//使用数组的every方法,里面的每一个均返回true时,结果才为true,否则就为false
let state = regs.every(e => e.test(value));
console.log(state ? "正确" : "错误");
// let regg = /^[0-9a-zA-Z]{5,9}$/;
// console.log(regg.test(value));
})
</script>
禁止贪婪
我们使用的量词通常都是贪婪的,什么是贪婪?比如\d+表示匹配一个或者多个字符,在匹配检索的时候它都会尽可能多的匹配满足条件的字符,这就是贪婪。在有些情况下,我们是需要禁止这种贪婪的,禁止贪婪简单来说就是使量词偏向少的一方。我们通过在量词的后面添加一个?来禁止贪婪。比如:
+? 本来是一个或者多个,禁止贪婪后就是一个
*? 0个
?? 0个
{3,5}? 3个
{1,} 1个
使用,我们看一个例子:
<body>
<main>
<span>houdunwang</span>
<span>hdcms.com</span>
<span>houdunren.com</span>
</main>
</body>
<script>
//使用问号禁止贪婪,让量词偏向少的一边
//例子,将span标签全部替换成h4标签在其前面添上内容"后盾人-"并描红
const main = document.querySelector(`main`);
//若不禁止贪婪的话,因为我们写的是[\s\S]匹配所有字符,同样包括了</span>
//所以最终会匹配成一个这样的内容
/* <span> 第一个<span>匹配
houdunwang</span>
<span>hdcms.com</span>
<span>houdunren.com
这一大块[\s\S]+匹配
</span> <\/span>匹配
*/
//这样不好。禁止贪婪后便会匹配成一个个独立的span标签
let reg = /<span>([\s\S]+?)<\/span>/ig;
main.innerHTML = main.innerHTML.replace(reg,(v,p1) => {
return `<h4 style="color:red">后盾人-${p1}</h4>`;
});
</script>
全局匹配
上面已经提到了全局匹配,我们可以使用exec配合g来实现全局匹配,同样match加g也可以。还有一种方法matchAll,下面两个例子体会一下它的用法:
<body>
<h1>houdunren.com</h1>
<h2>hdcms.com</h2>
<h3>后盾人</h3>
</body>
<script>
//只取出内容
let reg =/<(h[1-6])>([\s\S]+?)<\/\1>/ig;
const body = document.body;
//matchAll函数返回一个迭代器
const hd = body.innerHTML.matchAll(reg);
//console.log(body.innerHTML.matchAll(reg));
let contents = [];
for (const iterator of hd) {
//console.dir(iterator); //打印输出,你看一下控制台的内容,你就知道下面为什么要写iterator[2]
//因为我们要提取的内容在第二组中
contents.push(iterator[2]);
}
console.table(contents);
</script>
自定义matchAll方法
<body>
<h1>houdunren.com</h1>
<h2>hdcms.com</h2>
<h3>后盾人</h3>
</body>
<script>
String.prototype.matchAll = function(reg) {
let res = this.match(reg);
if(res) {
let str = this.replace(res[0], "^".repeat(res[0].length));
let match = str.matchAll(reg) || [];
return [res,...match];
}
};
let body = document.querySelector("body").innerHTML;
let search = body.matchAll(/<(h[1-6])>([\s\S]+?)<\/\1>/ig);
console.log(search);
</script>
$符号的使用
$可以通过$加序号来提取原子组中的内容,我们前面已经知道了,除此之外,它还可以使用($’)来表示匹配内容右边的内容,用($`)来表示匹配内容左边的内容,用$&表示匹配的所有内容。
<body>
<main>
教育是人人都必须接受的一门艺术,从古至今接受教育的人往往都能巴拉巴拉!
</main>
</body>
<script>
//在字符串前后各再加一个=
let hd = '=后盾人=';
let reg = /后盾人/;
//$`表示匹配内容左边的内容 $'表示匹配内容右边的内容 $&表示匹配的内容
//第一个是esc下面的那个键 第二个是单引号
//你这样记,左边的内容就是你左手esc下边的键,右边的内容就是右手的单引号键
console.log(hd.replace(reg,"$`$&$'"));
//$&的使用
const main = document.querySelector('body main');
main.innerHTML = main.innerHTML.replace(/教育/g,`<a href="https://lockme.gitee.io">$&</a>`);
</script>
原子组别名
为了简化原子组的使用方式,有时候我们会为原子组取别名,在组中使用?<别名>来给这个原子组取一个别名,并使用$<别名>引用此原子组,我们看一个例子:
<body>
<main>
<a id="hd" href="https://www.houdunren.com">后盾人</a>
<a href="https://www.hdcms.com">hdcms</a>
<a href="https://www.sina.com.cn">新浪</a>
</main>
</body>
<script>
//将上述标签中的内容,转换成[link:'',title:'']这样数组的形式
const main = document.querySelector('body main');
//给组取别名,这样做了之后,iterator中的groups属性,将会生成这样一个对象
//groups: {link: "https://www.houdunren.com", title: "后盾人"}
//这样其实就已经到达目的了,用一个容器提取就行了
const reg = /<a.*?href=(["'])(?<link>.*?)\1>(?<title>.*?)<\/a>/ig;
//console.log(main.innerHTML.match(reg));
const link = [];
for (const iterator of main.innerHTML.matchAll(reg)) {
//将对象压入数组中即可
link.push(iterator["groups"]);
}
console.log(link);
let hd = `smallbridge`;
//取别名
let r = /s(?<con>ma)llbridge/i;
//取别名后可以使用$<con>取值
console.log(hd.replace(r,"<h4>$<con></h4>"));
//也可以使用原先的$1取值
console.log(hd.replace(r,"<h4>$1</h4>"));
</script>
断言匹配
断言匹配可以理解成正则表达式自身的条件,用来优化检索的。
一共有四种断言,两两匹配,很好记忆,他们分别是:
前断言
(?<=xxx) 表示匹配前面是xxx的字符
(?<!xxx) 表示匹配前面不是xxx的字符
后断言
(?=xxx) 表示匹配后面是xxx的字符
(?!xxx) 表示匹配后面不是xxx的字符
注意:前后断言可以一起使用,断言仅仅是用作条件限制,并不会匹配其中的内容用作返回。
看几个例子:
使用后断言匹配规范价格
<script>
let lessons = `
js,200元,300次
php,300.00元,100次
node.js,180元,260次
`;
let reg = /(\d+)(.00)?(?=元)/ig;
lessons = lessons.replace(reg,(v,...args)=>{
//console.log(args);
args[1] = args[1] || '.00';
return args.slice(0,2).join("");
});
console.log(lessons);
</script>
使用前断言模糊电话号码
<script>
let users = `
陈小桥电话: 17398890700
后盾人电话: 13508157296
`;
let reg = /(?<=\d{3})\d{4}(?=\d{4})/ig;
users = users.replace(reg,v =>{
return '*'.repeat(4);
});
console.log(users);
</script>
使用断言限制用户关键字
<body>
<main>
<input type="text" name="username">
</main>
</body>
<script>
const input = document.querySelector(`[name = "username"]`);
input.addEventListener("keyup",function(){
//不能包含小桥的5到6位字母,字母开头,字母结尾
const reg = /^(?!.*小桥.*)[a-z]{5,6}$/ig;
console.log(this.value.match(reg));
})
</script>
使用断言排除法统一数据
<body>
<main>
<a href="https://www.houdunren.com/1.jpg">1.jpg</a>
<a href="https://oss.houdunren.com/2.jpg">2.jpg</a>
<a href="https://cdn.houdunren.com/3.jpg">3.jpg</a>
<a href="https://houdunren.com/4.jpg">后盾人</a>
</main>
</body>
<script>
const main = document.querySelector("main");
const reg = /https:\/\/([a-z]+)?(?<!oss)\..+?(?=\/)/ig;
//console.log(main.innerHTML.match(reg));
main.innerHTML = main.innerHTML.replace(reg,()=>{
return "https://oss.houdunren.com";
});
</script>
好了,我的分享就到这里了,谢谢大家!