title: 正则表达式语法详解
date: 2021-08-20 17:36:04
update: 2021-8-20 17:43:51
categories: 前端
tags:
- 正则表达式
top: false
一、前言
正则表达式很有魅力,熟练使用正则表达式会让开发更高效,更优雅,同时一些开发工具在搜索和替换时也支持正则表达式,如idea, vscode等等。
在你学会正则之前,你只能看着那些正则大师们,写了一串外星文似的字符串,替代了你用一大篇幅的if else代码来做一些数据校验
这篇文章主要参考知乎文章: 你是如何学会正则表达式的? 和 b站:后盾人正则表达式系列课程,本文可以作为一种文档来查阅,里面基本每个知识点都有例子支撑,所以内容可能有点多
推荐两个网站,正则可视化 和 Github很火的一个正则学习网站
二、 领略正则的魅力
举一个例子来说明正则表达式的魅力
一个字符串 asjdhfalsjkdf1521521asldkfjalj454
,想从这个字符串中取出所有的数字,怎么做?如果是通过js,那可以通过字符串操作,判断每一个字符是不是数字或者转为数组通过filter进行过滤
let str = 'asjdhfalsjkdf1521521asldkfjalj454';
// 字符串操作
let result = "";
for (let x of str) {
if (!Number.isNaN(parseInt(x))) {
result += x;
}
}
// 或者
let result = [...str].filter(item =>
!Number.isNaN(parseInt(item))
)
console.log(result); // 1521521454
如果使用正则表达式呢?代码将非常简单
let str = 'asjdhfalsjkdf1521521asldkfjalj454';
let result = str.match(/\d/g).join("");
console.log(result); // 1521521454
是不是代码整洁了很多,学会正则表达式就不需要每次需要正则的时候都要去网上找了!!!
三、正文
1. 创建正则表达式方式
① 构造函数创建
var reg = new RegExp("abc|def", "g");
② 字面量创建(常用)
var reg = /abc|def/;
2. 字符匹配规则
字符 | 说明 |
---|---|
\ | 转义字符,如 \( 匹配左括号 |
\d | 匹配一位数字 [0-9] ,\D则取反 |
\w | 匹配字符数字下划线 [0-9a-zA-Z_],\W则取反 |
\s | 匹配空格、水平制表符、垂直制表符、换行符、分页符、回车符 [\t\v\n\r\f],\S则取反 |
. | 匹配除了换行、回车、行分隔符、段分隔符外的任意字符 【^\n\r\u2028\u2029】 |
\uxxxx | 匹配十六进制 |
\f | 换页符 |
\n | 换行符 |
\r | 匹配回车符 |
\t | 水平制表符 |
\v | 竖直制表符 |
\o | 匹配NULL字符 |
[abc] | abc任意一个 |
[^abc] | abc中任何一个都不匹配,取反 |
[a-z] | 匹配从a-z的任意字符 |
3. 运算符及优先级
优先级 | 运算符 | 描述 |
---|---|---|
1 | \ | 转义字符 |
2 | ()、(?:)、(?=)、[] | 断言和原子组 |
3 | *、+、?、{n}、{n,}、{n,m} | 匹配量词 |
4 | ^、$,\任何元字符(如\d,\w),任何字符 | |
5 | | | 逻辑 “或” 操作 |
很多时候匹配结果和自己预想不一样,可以检查一下这个运算优先级,比如匹配量词(+ * {m,n})或者 |
这个运算符,因为他们的优先级比较低,举两个例子
let str = "abbbbbababab";
let reg = /ab+/g;
console.log(str.match(reg)); // abbbbb,ab,ab,ab
可能这段代码你的本意是要整体匹配 ab
,+
代表ab可以出现多次,但是第一组匹配结果显然不是自己想要的,原因是因为字符匹配 ab
优先级低于 +
运算符,所以 ab
不能作为一个整体,因此 +
运算符只作用在 b
字符上,也就会匹配abbbb
这种
同理还有一个例子
let str = "abc,ac";
let reg = /a|bc/g;
console.log(str.match(reg)); // a,bc,a
因为字符匹配优先级比较高,因此正则理解的这个表达式应该是匹配字符 a
或者字符 bc
。而不是匹配 ab
或者 ac
4. 匹配量词
如果要重复匹配一些内容时我们要使用重复匹配修饰符,包括以下几种
字符 | 说明 |
---|---|
* | 零到多个 |
+ | 一到多个 |
? | 零到一个 |
{ n } | 匹配 n 个 |
{ n,m } | 匹配 n 到 m 个 |
{ n,} | 匹配 n 到多个 |
在这些量词中也分为
贪婪
和懒惰(非贪婪)
两种,贪婪就是尽可能的多匹配,非贪婪就是懒,一个满足条件就停止
正则默认是贪婪的(仿佛讽刺了人性),如果要取消贪婪,可以在表达式后面加一个 ?
比如 a*?
则最少匹配0个 a
let str = "http://www.suhaoblog.cn";
let reg = /w+/g
let lessReg = /w+?/
console.log(str.match(reg)); // www
console.log(str.match(lessReg)); // w
再举一个例子,如果我想将main标签中所有span标签换成h1标签,对replace方法不太清楚的可以看看mdn中replace的api
<!DOCTYPE html>
<html>
<body>
<main>
<span>suhaoblog,cn</span>
<span>this is a test</span>
</main>
<script>
const main = document.querySelector("main");
let reg = /<span>([\s\S]+?)<\/span>/g;
main.innerHTML = main.innerHTML.replace(reg, (match, p0) => {
return `<h1 style="color:red">${p0}</h1>`;
})
</script>
</body>
</html>
此处的正则就需要使用贪婪模式,因为一次只匹配一对span标签,如果不用贪婪,则会从第一个span的开始匹配到第二个span的结束,整体换成h1标签,这样虽然看起来一样,但是通过审查元素发现只有一个h1标签,在h1内部还有一个span。而通过贪婪会精确匹配到每一对span,逐一替换
5. 位置匹配
① 开始结束位置
限定匹配和结束的边界,举个例子
var str = "123";var reg = /\d/g;console.log(reg.test(str)); // true
如果加上字符边界限定
var str = "123";var reg = /^\d$/g;console.log(reg.test(str)); // false
为什么结果是false呢,因为加上开始和结束限定,相当于整个字符串都要满足条件才返回true,str包含三个数字,而正则只匹配了一个字符,所以匹配失败
而不加 ^
和 $
限定就表示从字符串开头开始匹配,如果匹配到一个数字就返回true,显然满足
如果上面例子想返回true,可以加上 {} 限定匹配位数
var reg = /^\d{3}$/
匹配三位,返回true
② 边界匹配符
边界匹配符主要包括\b
,\B
两种,都是匹配位置而不是匹配字符,常用来获取字符
\b
单词边界,是指单词与符号之间的边界,是一个位置,不是空格或字符。(这里单词可以是中文字符,英文字符,数字。符号可以是中文符号,英文符号,空格,制表符,换行)。不能与匹配量词?+*{1}{2,5}等连用
\B
非单词边界,是指符号与符号,单词与单词的边界,不能与量词连用
let str = "https://www.suhaoblog.cn";let reg = /\b/g;console.log(str.replace(reg, "-")); // -https-://-www-.-suhaoblog-.-cn-
可以看到匹配到八个位置,都是单词和符号的边界,反之如果使用 \B
let str = "https://www.suhaoblog.cn";let reg = /\B/g;console.log(str.replace(reg, "-")); // h-t-t-p-s:-/-/w-w-w.s-u-h-a-o-b-l-o-g.c-n
两个对比应该就能清楚二者的区别和用法
6. 匹配模式
正则表达式中主要包含 i
,g
,m
,u
,s
,y
几种匹配模式
① ‘i’ 模式
忽略大小写,在该模式下所有字符会按照小写匹配
let str = "suHao1,SUHAO,Suhao,su1hao";let res = str.match(/suhao/i, "test")console.log(res); // suHao
② ‘g’ 模式
全局匹配,不加 g
修饰,第一个满足条件就会停止,而 g
修饰后不会停止,会继续匹配到字符串的结尾
还是上面的例子
let str = "suHao1,SUHAO,Suhao,su1hao";let res = str.match(/suhao/ig, "test")console.log(res); // suHao,SUHAO,Suhao
③ ‘u’ 模式
我知道的在 u
模式下有三种用法,匹配宽字节,语言系统匹配和字符属性匹配
首先是宽字节匹配,使用 u
模式可以正确处理四个字符的 UTF-16
字节编码
第二个用法时匹配Unicode类别,如 /\p{L}/
,这种用法我见的比较少,初学不建议深入
在Unicode编码中,每个字符都有对应的类别(Unicode Categories),可以理解成属性,比如 L
表示单词(不全指英文单词),P
表示标点符号,N
表示数字,这种属性也需要在 u
模式下生效,Unicode Categories 文档地址
// 匹配数字let str = "test,1234+?";console.log(str.match(/\p{N}/ug)); // 1,2,3,4// 使用L匹配单词let str = "中文test,1234+?";console.log(str.match(/\p{L}/ug)); // 中,文,t,e,s,t
这里有点不太确定,L文档中标注的是
any kind of letter from any language
,中文这两个字符之所以被匹配到可能是因为中文被认为是中文的单词
,如果要匹配字母还是建议/^[a-zA-Z]$/
这种匹配方法
第三种用法是匹配Unicode中支持的语言系统,比如/\p{sc=Han}/
这种写法匹配汉字
let str = "中1文test,1234+?";console.log(str.match(/\p{sc=Han}/ug)); // 中,文
具体语言对应表可以查看这里 【配合chrome浏览器的翻译成中文功能效果更佳,不过chrome把汉语的Han翻译成了韩。。】
④ ‘y’ 模式
y
模式和 g
模式有点相似,也是全局匹配,但是 y
比较懒,如果第一个匹配失败,遇到困难了,匹配就停止了。且后一次匹配都是从上一次的匹配成功的下一个位置开始的,举个例子
let str = "苏浩的个人博客地址是:http://www.suhaoblog.cn,http://music.suhaoblog.cn"let index = str.indexOf(":") + 1;const reg = /(https?:\/\/\w+\.\w+\.\w+),?/y;reg.lastIndex = index;console.log(reg.lastIndex); // 11console.log(reg.exec(str)[1]); // http://www.suhaoblog.cnconsole.log(reg.lastIndex); // 35console.log(reg.exec(str)[1]); // http://music.suhaoblog.cnconsole.log(reg.lastIndex); // 60
通过设定正则表达式对象的
lastIndex
让匹配从指定位置开始,第一次匹配成功后lastIndex
等于上一次匹配成功的位置,通过这种方式可以加快匹配速度,特别是大文本
⑤ ‘m’ 模式
多行匹配,会对每一行进行单独处理,举个例子
给定一个字符串,将这个字符串转换为[ {name: "js",price: "200元"} ]
的格式
let str = ` #1 js,200元 % #2 php,300元 % #3 java,500元 % `// 将匹配结果的每一行通过map函数进行处理let result = str.match(/^\s*#\d\s*\w+\s*.*$/gm).map(item => { // 去掉前后空格,replace返回结果是个字符串,所以可以链式调用 let result = item.replace(/^\s*#\d\s*/, "").replace(/\s*%$/, ""); let [name, price] = result.split(","); // es6 解构语法,精简处理 return { name: name, price: price }})console.log(result);
⑥ ‘s’ 模式
单行匹配,将目标字符串当成单行处理, .
可以匹配所有字符,包括换行
let str = ` sd, 1212 dsf ,.`;let reg = /.+/sconsole.log(str.match(reg))// ↵ sd,↵ 1212↵ dsf↵ ,.↵
可以匹配到回车
7. 组
组分为三类,分别是 捕获组
、分组引用
、非捕获组
,这部分开发中用的比较多,需要认真理解
① 捕获组
首先是捕获组,通过括号 ()
包裹起来的部分就是捕获组,比如
let str = "http://www.suhaoblog.cn";let reg = /https?:\/\/(\w+\.\w+\.\w+)/console.dir(str.match(reg))
结果为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dptUGoKT-1631583183934)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210627212503460.png)]
其中索引为0表示匹配的结果,1为匹配的原子组,也就是所说的捕获组了
② 分组引用
举个例子,日期格式一般是2020-12-12,也有2020/12/12这种格式,这两种都是可以的,但是不能混着用,比如2020-12/12,这显然不合适,最开始我的正则是这么写的
let reg = /\d{4}[\/-]\d{2}[\/-]\d{2}/
这个正则虽然能匹配到 2020-12-12
和 2020/12/12
但是同时 2020-12/12
也能匹配成功,这显示是不对的,因此需要用到分组引用(捕获组的引用),也就是 \n
这种形式,n代表第几次匹配结果,也就是前面通过捕获组捕获的结果,改写正则
let reg = /\d{4}([\/-])\d{2}\1\d{2}/
再次测试,发现 2020-12/12
这种无法通过测试,如果包含多个原子组,只需要数左括号,从左边数第n个左括号和对应闭合括号中包含的内容就是第n个引用组
还有个替换的例子
// 将(010)99999999转化为电话格式,即010-99999999let str = "(010)99999999 (020)99999999";let reg = /\((\d+)\)(\d+)/g;let res = str.replace(reg, '$1-$2'); // 010-99999999 020-99999999
replace方法中的 $1
和 $2
也是分组引用,当然也可以使用回调函数,不过不如这个简单,也贴一下代码,方便对比
let str = "(010)99999999 (020)99999999";let reg = /\((\d+)\)(\d+)/glet res = str.replace(reg, (match, p0, p1) => { return `${p0}-${p1}`})
③ 非捕获组
使用括号包裹的部分会被匹配到捕获组,如果不想匹配,可以使用 ?:
取消捕获,还是捕获组的例子,如果我不想匹配三级域名 www
,可以使用 ?:
取消捕获
let str = "http://www.suhaoblog.cn";let reg = /https?:\/\/((\w+)\.\w+\.\w+)/console.dir(str.match(reg))
不取消捕获,则捕获组中包括 www.suhaoblog.cn
和 www
两部分,如果修改正则
let reg = /https?:\/\/((?:\w+)\.\w+\.\w+)/
则此时匹配组只有 www.suhaoblog.cn
8. 断言
① 零宽先行断言
?=exp
匹配后面为exp
的内容
let str = "苏浩的个人网站地址:。";let reg = /:(?=。)/gconsole.log(str.replace(reg, (match) => { return match + "http://www.suhaoblog.cn"}));// 苏浩的个人网站地址:http://www.suhaoblog.cn。
在正则中匹配是从前向后匹配,因此这里的前和逻辑前是反过来的,先行断言也就是向后匹配。匹配到后面的句号,然后替换成你想替换的部分。
② 零宽后行断言
和先行断言效果相反,向前匹配,例子
let str = "苏浩的个人网站地址:。";let reg = /(?<=:)/gconsole.log(str.replace(reg, "http://www.suhaoblog.cn"));// 苏浩的个人网站地址:http://www.suhaoblog.cn。
③ 零宽负向先行断言
(?!exp)
后面不能出现exp指定的内容,举个例子
比如匹配号码,号码可以是134,139,198,178等开头,但是以139开头后下一位不能是1,就可以使用这个零宽负向先行断言
let str = "19824578939";let str1 = "13911111111";let reg = /(134\d|139(?!1)|198\d|178\d)\d{7}/g;console.log(reg.test(str)); // trueconsole.log(reg.test(str1)); // false
思路就是匹配前4位,如果是134,198和178开头,则多匹配任意一位数字,如果是139开头,则下一位数字通过 零宽负向先行断言
控制不为1,最后同意控制剩余部分为7位数字即可
④ 零宽负向后行断言
和③相似,不过是向前匹配。例子:网址前面不能出现空格
let str = "苏浩个人博客地址为 http://www.suhaoblog.cn";let str1 = "苏浩个人博客地址为http://www.suhaoblog.cn";let reg = /(?<!\s+)https?:\/\/\w+\.\w+\.\w+/g;console.log(reg.test(str)); // falseconsole.log(reg.test(str1)); // true
四、总结
本文重点内容还是基本的匹配字符、匹配量词、匹配模式以及分组部分,正则的语法其实不是很难,重要的是多练习,灵活使用各种原子组和捕获组完成对字符串的验证和替换。这篇文章是边学习边总结的,可能有些地方不是特别准确,有问题欢迎大家指出