1 基本概念
正则表达式中的元素由字符和元字符组成。字符又叫做普通字符即普通的计算机编码,如我们常用的英文字符、数字、中文等等;元字符又被称为特殊字符即在正则表达式中存在特殊意义的字符,如\、*、$等等,详情可见MDN 文档。本文主要介绍正则表达式的基本使用,以及一些常见正则问题的处理。
2 一对一匹配
正则表达式与目标字符一对一匹配。
当我们想匹配a字符,正则表达式可以直接写成/a/。
但是匹配的是元字符时,如
$,直接写成/$/是无法匹配成功的,因为$在正则中存在特殊意义。
此时我们需要使用
\对元字符进行转义,即消除元字符的特殊意义。
个别普通字符在
\的修饰下也会拥有特殊含义,如\n(换行)、\r(回车)等等。
小结:匹配普通字符:/普通字符/;匹配元字符:/\元字符/。
| 正则表达式 | 描述&举例 | 关联 |
|---|---|---|
/普通字符/ | 匹配普通字符 | - |
/\元字符/ | 匹配元字符 | - |
/\n/ | 匹配换行符(LF) | 换行和回车 |
/\r/ | 匹配回车符(CR) | 换行和回车 |
/\f/ | 匹配换页符 | - |
/\t/ | 匹配制表符(Tab) | - |
/\v/ | 匹配垂直制表符 | - |
3 一对多匹配
正则表达式之所以强大,就是其能进行模糊匹配。使用集合、区间和通配符的方式实现一对多的匹配,实现字符模糊。涉及:
[]、-、通配符。
3.1 集合
使用[]或者[^]将字符一一列举,[]:代表匹配列举的内容;[^]:代表匹配除列举以外的内容。
假如想匹配 1 到 9 之间的任意一个奇数,[一对一匹配](#2 一对一匹配)肯定无法满足当前的要求。此时我们可以使用集合的方式来处理。
3.2 区间
需要匹配的字符连贯时,可以使用一个-区间来代替一一列举
当需要匹配任意大于 4 的数字时,我们可以直接写成/[56789]/,但是更简单的方法是写成/[5-9]/。
/[a-z]/:匹配任意小写字母;/[\u4e00-\u9fa5]/:匹配任意汉字;
3.3 通配符
通配符用于匹配一类字符。
在开发中时常会遇到仅允许用户输入数字的场景,用区间方式可写成/[0-9]/,如果用通配符写法可以写成:\d。懒惰才是人类文明进步的真正动力呀;
集合、区间和通配符可以合并使用的,当他们同时存在时会取它们的并集。
小结:集合写法:/[列举字符]/;区间写法:/[开始-结束]/;通配符写法:/通配符/。
| 正则表达式 | 描述&举例 | 关联 |
|---|---|---|
/[13579]/ | []列举的全部字符 | - |
/[5-9]/ | []内区间的全部字符 | - |
/\d/ | 匹配全部数字 | 等价于[0-9] |
/\D/ | 匹配除数字以外的任意字符 | 等价于 [^\d] |
/\w/ | 0 至 9、a 至 z、A 至 Z 以及_ | 等价于 [a-zA-Z_\d] |
/\W/ | 0 至 9、a 至 z、A 至 Z 以及_以外字符 | 等价于 [^\w] |
/\s/ | 全部空白字符,如: 空字符、\n、\r、\f、\t、\v | 等价于[\n\r\f\t\v ] |
/\S/ | 全部非空白字符 | 等价于[^\s] |
/./ | 除\n 和\r 以外的全部字符 | 等价于[^\n\r] |
4 次数限定
本节就来讲讲设置字符重复的次数,实现长度模糊。涉及:
?、{}、*、+
4.1 ?元字符
使用?修饰的字符或者分组可匹配 0 次或者 1 次。如正则:/https?/,可以同时匹配http和https。
4.2 {}元字符
如果想同时匹配http、https和httpss三种字符串,按照前面说的?元字符可以写成/https?s?/,但是这样的写法显然很冗长,这时我们就可以用到{}元字符了,正则可写成/https{0,2}/,{n,m}修饰的字符或者分组可匹配n-m次;{}有四种写法:
-
{count}:精确count 次; -
{min,max}:min 次到 max 次; -
{min,}:至少 min 次; -
{0,max}:至多 max 次。
4.3 *元字符
使用*修饰的字符或者分组可匹配0 次或者多次。等价于/https{0,}/;
4.4 +元字符
和*类似代表一个区间,使用+修饰的字符或者分组至少匹配一次,等价于/https{1,};
小结:?:匹配 0 至 1 次;{}:匹配指定次数或者指定区间;*:匹配大于等于 0 次;+:匹配大于等于 1 次。
| 正则表达式 | 描述&举例 | 关联 |
|---|---|---|
/https?/ | 出现 0 次或者 1 次 如:http、https | 等价于/https{0,1}/ |
/https{0,2}/ | 重复 0 次至 2 次 如:http、https、httpss | - {count}:count 次;- {min,max}:min 次到 max 次;- {min,}:至少 min 次 ;- {0,max}:至多 max 次 |
/https*/ | 重复大于等于 0 次 如:http、https 等 | 等价于/https{0,}/ |
/https+/ | 重复大于等于 1 次 如:https、httpss 等 | 等价于/https{1,}/ |
5 边界限制
用于限制查询过程中的单词边界限制和句子边界限制。涉及:
\b、\B、$、^。
5.1 单词边界\b
我们经常需要去判断某字符串是否存在某个单词,比如day ,如果按照往常写法写成/day/,测试后会发现含有day的单词如today也会匹配上;要想解决这个问题就需要用到单词边界的功能了。
当一个区域的左右位置有且仅有一个\w([0-9a-zA-Z_])时该区域就存在单词边界,如下图所示每一个\b就是一个单词边界;
此时想要匹配day但是不想匹配包含day的单词可以写出/\bday\b/
5.2 非单词边界\B
单词边界以外的区域均为非单词边界,即一个区域内前后位置都为\w或者前后位置不存在\w,如下图每个_就是非单词边界。
调整一下上面的例子,假如只想匹配以day结尾的单词,那么正则可以写成/\Bday\b/,当然也能写成/\w+day\b/。
5.3 句子边界^与$
现在有一段字符串:http://https.com.hellochange.cn,我们想判断它是否是以https开头以com结尾。如果不使用正则在JavaScript中可以直接使用startsWith和endsWith来判断。
如果使用正则来处理就必须用到^和$来限制匹配的开始位置和结束位置。^表示以什么开头;$表示以什么结尾。
小结:\b:单词边界;\B:非单词边界;^:内容以什么开头;$:内容以什么结尾。
| 正则表达式 | 描述&举例 | 关联 |
|---|---|---|
/\bday\b/ | 匹配day单词 | - |
/\Bday\b | 匹配以day结尾的单词如:today | - |
/^我/ | 匹配以我开头的句子如:我是谁 | - |
/你$/ | 匹配以你结尾的句子如:看见你 | - |
/^我.+你$/ | 匹配以我开头你结尾的句子如:我和你 | - |
6 修饰符
修饰符也叫标志,用于指定匹配策略。涉及:
g、m、i、s
g:全局匹配(global),默认情况下正则在找到第一个匹配的内容后就会停止继续查找,如果想要找到全部符合规则的内容就可以使用全局策略。
m:多行匹配(multiline),以回车符和换行符做为句子分割点。
i:忽略大小写(insensitive),正则匹配默认是区分大小写的,如果想匹配过程中不区分大小写可添加该修饰符。
s:.元字符可匹配回车符\r和换行符\n(single line)。
| 正则表达式 | 描述&举例 | 关联 |
|---|---|---|
/^https.+com$/m | 多行匹配以 https 开头 com 结尾的句子 | - |
/^https.+com$/g | 匹配全部以 https 开头 com 结尾的句子 | - |
/^https.+com$/i | 匹配以 https 开头 com 结尾的句子且不区分大小写 | - |
/^.+$/s | 匹配任何字符串 | - |
7 逻辑关系
正则中可用的逻辑关系,即「或
|」和「非^」。涉及:|、^
7.1 逻辑或
A|B:A或者B均满足匹配规则。
7.2 逻辑非
[^AB]:匹配除A、B以外的任何字符。
8 分组
即
()的一系列特征与使用,如分组、提取转换、回溯。
8.1 分组基础
使用()元字符将正则表达式进行分组,每一个分组都是一个子表达式,每一个子表达式是一个整体,类比为1个“字符”。
假如有一段字符串,我们想知道该字符串是否全由ABC组成,此时正则应该如何书写呢?首先我们可以把ABC类比成一个字符X,由题可知ABC至少需要出现一次所以需要使用+,因为是判断整个字符串所以需要使用到^和$来限制匹配的开始位置和结束位置,此时正则可以写成/^X+$/,根据()的分组功能,最终正则可以写成/^(ABC)+$/
8.2 提取转换
使用()进行分组,默认情况下各分组匹配的内容会被提取,使用这一特性可以很轻松的实现内容的提取和转换,如下图可以看出,除了被匹配的内容被提取,每一个分组的内容也被提取出来了。
假如现在需要将YYYY-MM-DD的数据转换为YYYY年MM月DD日,有了分组提取的特性该需求就很好处理了,实现如下:
// 方式一
function fn1(str) {
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;
const result = reg.exec(str);
if (result) {
return `${result[1]}年${result[2]}月${result[3]}日`;
// 也可以使用构造函数属性,该属性只要执行正则操作就有有值,如使用reg.test(str)、str.match(reg)等
// return `${RegExp.$1}年${RegExp.$2}月${RegExp.$3}日`;
}
return str;
}
// 方式二
function fn2(str) {
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;
return str.replace(reg, (match, year, month, day) => {
return `${year}年${month}月${day}日`;
// 当然这里也可以使用前面提到的构造函数全局属性
});
}
// 方式三
function fn2(str) {
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;
// $1等价于RegExp.$1
return str.replace(reg, `$1年$2月$3日`);
}
8.3 非捕获分组
分组但是不捕获分组匹配的内容,涉及:(?:)。提取转换小节提到过,使用()进行分组,各分组匹配的内容也会被提取,但是有时我们并不会用到分组匹配的内容,此时我们可以使用非捕获分组(?:);
8.4 回溯引用
在正则表达式中可使用\1、\2、\3等等表示前面第一个分组、第二个分组、第三个分组匹配的内容。
有时我们需要根据前面匹配的内容来决定后面的内容就需要用到回溯功能了。如有YYYY-MM-DD和YYYY/MM/DD两种时间格式,如果想用一个正则表达式把2种时间都匹配应该如何书写?如果直接写成/^(\d{4})(-|\/)(\d{2})(-|\/)(\d{2})$/,测试后会发现YYYY-MM/DD和YYYY/MM-DD的日期也会匹配。
如果想要控制分割符前后一致,就可以用到分组的回溯引用功能了,正则可写成:/^(\d{4})(-|\/)(\d{2})\2(\d{2})$/,\2代表第二个分组匹配的内容(也等价于RegExp.$2 ),即(-|\/)匹配到的内容。
8.5 零宽先行断言
零宽先行断言分为零宽正向先行断言(?=pattern)和零宽负向先行断言(?!pattern)。
零宽正向先行断言:(?=pattern)代表字符串中一个零宽度的位置,该位置后面字符串能与pattern匹配。举个例子:字符串为:people,正则为:/peo(?=ple)/,(?=ple) 其实就是代表peo和ple之间的一个零宽的位置,为了方便理解我们假定这个位置为#,字符串就变成:peo#ple,正则则变成:/peo#/,因为#并不存在,最终得到的结果就是peo。
零宽负向先行断言:(?!pattern)也代表字符串中一个零宽度的位置,但是与正向相反,该位置后面字符串不能与pattern匹配。
8.6 零宽后行断言
零宽后行断言分为零宽正向后行断言(?<=pattern)和零宽负向后行断言(?<!pattern)。
零宽正向后行断言:(?<=pattern)代表字符串中一个零宽度的位置,该位置前面字符串能与pattern匹配。
零宽负向后行断言:(?<!pattern)代表字符串中一个零宽度的位置,该位置前面字符串不能与pattern匹配。
注意 ⚠️:零宽后行断言存在兼容性问题,在 safari 浏览器和一些老版本浏览器中无法识别,所以一般不会直接使用后行断言,而是使用[替代方案](#8.7 零宽后行断言替代方案)
8.7 零宽后行断言替代方案
- 将
/(?<=A)B/转化为/(?=A(B))A\1/或者/(?=AB)A(B)/【并非全部情景都适用】
假如想匹配全部含config.后缀名的文件,如果使用零宽后行断言可以直接写成:/(?<=config)\.(\w+)$/
转化为前行断言为:/(?=config(\.(\w+)$))config\1/
如果我们只想获取文件的后缀名,我们可以使用分组的提取功能,当然也可以把结果中的config使用replace进行移除。
- 将
/(?<!A)B/转化为^(?!.*AB$).+(B)【并非全部情景都适用】
假如想匹配全部不含config.后缀名的文件,如果使用零宽后行断言可以直接写成:/(?<!config)\.(\w+)$/
转化为前行断言为:/^(?!.*config\.(\w+)$).+\.(\w+)$/
如果想获取到后缀名和上面一样使用分组的提取功能
- 将字符串翻转然后使用前行断言匹配
const str = "apple and people";
const reg = /(?<=app)le/;
// 将字符串翻转
const reverseStr = str.split("").reverse().join(""); // 'elpoep dna elppa'
// 将正则由后行断言转化为前行断言
const reverseReg = /el(?=ppa)/;
// 翻转后的匹配结果
const reverseResult = reverseStr.match(reverseReg); //['el', index: 11, input: 'elpoep dna elppa', groups: undefined]
// 转化为真实结果
const result = reverseResult
? reverseResult.map((item) => item.split("").reverse().join(""))
: null; // ['le']
小结:(?=pattern):后面是pattern;(?!pattern)后面不是pattern;(?<=pattern)前面是pattern;(?<!partterb)前面不是pattern。
9 后记
第一次认真的写一篇博客,其中肯定存在很多纰漏,各位如果有发现什么错误之处或者觉得哪块表述的不清楚或者在写作上和表述上有什么建议的请留言告诉我,Thanks♪(・ω・)ノ!