JS基础系列之 —— 正则表达式

·  阅读 579

前言

正则表达式一直是困扰很多程序员的一门技术,当然也包括曾经的我。大多数时候我们在开发过程中要用到某些正则表达式的时候,都会打开谷歌或百度直接搜索然后拷贝粘贴。

作为一门用途很广的技术,我相信深入理解正则表达式并能融会贯通是值得的。所以,有了这次分享,希望通过这次分享,大家可以理解正则,并在遇到正则相关问题的时候可以不借助搜索引擎,自己就可以解决。

感受正则的魅力

1. 取到文本中的所有数字

let title = "现在是2021年08月22日12点10分";
// 普通方式拿到上面一段文字中的数字
let time = [...title].filter((item) => !Number.isNaN(Number.parseInt(item)));
// 拿到数字
console.log(time.join(""));

// 使用正则获取数字,正则比上面的要简洁的多
let numbs = title.match(/\d/g);
console.log(numbs.join(""));

// 打印结果完全相同
console.log(time.join("") === numbs.join("")); //true
复制代码

2. 如何判断输入的手机号是否是有效的手机号码?

/^1[3456789]\d{9}$/

/^1(3|4|5|6|7|8|9)\d{9}$/

^1\d{10}
复制代码

对比分析 与普通函数操作字符来比较,正则表达式可以写出更简洁、功能强大的代码。

0x01 是什么

正则表达式(Regular Expression)其实就是一门工具,目的是为了字符串模式匹配,从而实现搜索和替换功能。它起源于20世纪50年代科学家在数学领域做的一些研究工作,后来才被引入到计算机领域中。从它的命名我们可以知道,它是一种用来描述规则的表达式。而它的底层原理也十分简单,就是使用状态机的思想进行模式匹配。

简单地说,实现正则表达式引擎有两种方式:DFA 自动机(Deterministic Final Automata 确定型有穷自动机)和 NFA 自动机(Non deterministic Finite Automaton 不确定型有穷自动机)。

0x02 基础知识

1. 字符

单个字符

最简单的正则表达式可以由简单的数字和字母组成,没有特殊的语义,纯粹就是一一对应的关系。如想在'apple'这个单词里找到‘a'这个字符,就直接用/a/这个正则就可以了。

有时候我们想去找到 / 字符,但是它在正则中有着特殊的意义该怎么办呢?所以就需要用到转义符\

\ 反斜杠表示转义符
转义用于改变字符的含义,用来对某个字符有多种语义时的处理。

如果本来这个字符不是特殊字符,使用转义符号就会让它拥有特殊的含义。我们常常需要匹配一些特殊字符,比如空格,制表符,回车,换行等, 而这些就需要我们使用转义字符来匹配。

多个字符

单个字符的映射关系是一对一的,即正则表达式的被用来筛选匹配的字符只有一个。而这显然是不够的,只要引入集合区间和通配符的方式就可以实现一对多的匹配了。

2. 重复匹配修饰符

如果要重复匹配一些内容时我们要使用重复匹配修饰符,包括以下几种。

基本使用

let name = "sooooo";
// + :一个或者多个
console.log(name.match(/so+/)); // sooooo

// * :零个或者多个
console.log(name.match(/so*/)); // sooooo

// ? :有或者没有
console.log(name.match(/so?/)); // so

// {1,2} :一个到两个,最少一个,最多两个
console.log(name.match(/so{1,2}/)); // soo
复制代码

重复匹配对原子组影响

let name = "sososososososos";
// 连续匹配so,使用原子组包起来是里面的内容就变成了一个整体
console.log(name.match(/(so)+/g)); // ["sososososososo"]
复制代码

禁止贪婪(懒惰模式)

正则表达式在进行重复匹配时,默认是贪婪匹配模式,也就是说会尽量匹配更多内容,但是有的时候我们并不希望他匹配更多内容,这时可以通过 ? 进行修饰来禁止重复匹配

let name = "soooooo";

// *:零个或多个,加上问号表示匹配0个
let reg = /so*?/g;
console.log(name.match(reg)); // ["s"]

// +:一个或多个,加上问号表示只匹配1个
reg = /so+?/g;
console.log(name.match(reg)); // ["so"]

// ?:0个或者1个,再加上问号表示只匹配0个
reg = /so??/g;
console.log(name.match(reg)); // ["s"]

// {2,5} 表示匹配2到5个,加上问号表示匹配2个
reg = /so{2,5}?/g;
console.log(name.match(reg)); // ["soo"]
复制代码

独占模式

如果在表达式后加上一个加号(+),则会开启独占模式。同贪婪模式一样,独占模式一样会匹配最长。不过在独占模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯。

以下是对三种模式的表达式:

3. 边界符

单词是构成句子和文章的基本单位,一个常见的使用场景是把文章或句子中的特定单词找出来。如:

The cat scattered his food all over the room. 
复制代码

我想找到cat这个单词,但是如果只是使用/cat/这个正则,就会同时匹配到catscattered这两处文本。这时候我们就需要使用边界正则表达式\b,其中 b 是 boundary 的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。

上面的例子改写成/\bcat\b/这样就能匹配到cat这个单词了。

4. 模式修饰符

正则表达式在执行时会按他们的默认执行方式进行,但有时候默认的处理方式总不能满足我们的需求,所以可以使用模式修正符更改默认方式。

  • g 全局匹配

  • i 不区分大小写

  • m 多行匹配

  • s 将字符串视为单行匹配

  • y 模式表示匹配到不符合的就停掉,不会继续往后匹配,必须连续的符合条件的

5. 选择符

|用一个竖杠表示或者, 选择修饰符,

let title = "xiaoming";
// 一个竖表示或者,两边的表达式满足任意条件即可
console.log(/xiao|m/.test(title));

// 检测电话是否是上海或北京的坐机
//错误结果:只匹配 | 左右两边任一结果
console.log(tel.match(/010|020\-\d{7,8}/)); 

//正确结果:所以需要放在原子组中使用
let tel = "020-9999999";
console.log(/(010|020)\-\d{7,8}/.test(tel));
复制代码

0x03 高级用法

字符匹配我们介绍的差不多了,更加高级的用法就得用到子表达式了。通过嵌套递归和自身引用可以让正则发挥更强大的功能。

从简单到复杂的正则表达式演变通常要采用分组、回溯引用和逻辑处理的思想。利用这三种规则,可以推演出无限复杂的正则表达式

1. 原子组

分组

其中分组体现在:所有以()元字符所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。如果只是使用简单的(regex)匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。

分组匹配时没有添加 g 模式修正符时只匹配到第一个,匹配到的信息包含以下数据

match中使用原子组匹配,会将每个组数据返回到结果中

let nh = "nihaoya.com";
console.log(nh.match(/nihao(ya)\.(com)/)); 
复制代码

回溯引用

所谓回溯引用(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像\1,\2,....,其中\1表示引用的第一个子表达式,\2表示引用的第二个子表达式,以此类推。而\0则表示整个表达式。

假设现在要在下面 dom 中匹配["

标题一

", "

标题二

"],你要怎么做呢?

let dom = `
    <h1>标题一</h1>
    <h2>标题二</h2>
`;
// 一个小括号包起来的东西被称为原子组,\1表示与第一个原子组相同的内容
// /<(h[1-6])>[\s\S]*<\/(h[1-6])>/
let reg = /<(h[1-6])>[\s\S]*<\/\1>/g;
console.log(dom.match(reg)); // ["<h1>标题一</h1>", "<h2>标题二</h2>"]
复制代码

利用回溯引用,我们可以很容易地写出/<(h[1-6])>[\s\S]*<\/\1>/g这样的正则。

回溯引用在替换字符串中十分常用,语法上有些许区别,用$1,$2...来引用要被替换的字符串。

替换字符串可以插入下面的特殊变量名:

下面以js代码作演示:

  • 字符替换

    var str = 'abc abc 123'; str.replace(/(ab)c/g,'$1f'); // 得到结果 'abf abf 123'

  • 在你好前后添加三个=

    let hd = "=你好="; console.log(hd.replace(/你好/g, "``&'$'"));

  • 把电话号用 - 连接

    let hd = "(010)99999999 (020)8888888"; console.log(hd.replace(/((\d{3,4}))(\d{7,8})/g, "11-2"));

嵌套分组和不记录分组

嵌套分组就是有很多的/(1(2(3)))/,回溯引用时的状态如图所示

如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)这样就可以避免浪费内存。

原子组里面加上 ?: 表示不记录该原子组,但是原子组的功能仍然生效

var str = 'scq000'.
str.replace(/(scq00)(?:0)/, '$1,$2')
// 返回scq00,$2
// 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2
复制代码

分组别名

如果希望返回的组数据更清晰,可以为原子组编号,结果将保存在返回的 groups字段中

let hd = "<h1> nihaoya.com </h1>";
console.dir(hd.match(/<(?<tag>h[1-6])[\s\S]*<\/\1>/));
复制代码

  • 使用 ? 起原子组的别名

  • 使用 $ 读取别名

2. 断言匹配

有时,我们需要限制回溯引用的适用范围。那么通过断言匹配就可以达到这个目的。

断言虽然写在括号中但它不是组,所以不会在匹配结果中保存,可以将断言理解为正则中的条件。

断言用来声明一个应该为真的事实。正则表达式中只有当断言为真时才会继续进行匹配

零宽先行断言

(?=exp) 正向肯定查找

用来检查接下来出现的是不是某个特定的字符集。

张三先生和张三女士字体变颜色

// 张三先生和张三女士是天造地设的一对
<script>
  // 用小括号包括起来,前面加上?=表示该正则右侧等于某个值得元素
  let main = document.body;
  // 匹配张三 右侧 是先生的字段
  let men = /张三(?=先生)/g;
  // 匹配张三 右侧 是女士的字段
  let wumen = /张三(?=女士)/g;

  main.innerHTML = main.innerHTML.replace(
    men,
    `<span style="color:blue">$&</span>`
  );
  main.innerHTML = main.innerHTML.replace(
    wumen,
    `<span style="color:pink">$&</span>`
  );
</script>  
复制代码

下面是将价格后面 添加上 .00

<script>
  let lessons = `
    css,200元,300次
    js,300.00元,100次
    node.js,180元,260次
  `;
  let reg = /(\d+)(.00)?(?=元)/gi;
  lessons = lessons.replace(reg, (v, ...args) => {
    args[1] = args[1] || ".00";
    return args.splice(0, 2).join("");
  });
  console.log(lessons);
</script>
复制代码

零宽后行断言

?<=exp 反向肯定查找

用来检查前面出现的是不是某个特定的字符集。

匹配前面是shijei 的数字

let hd = "nihao789shijei666";
let reg = /(?<=shijei)\d+/i;
console.log(hd.match(reg)); // 666
复制代码

匹配前后都是数字的内容

let hd = "nihao789shijei666";
let reg = /(?<=\d)[a-z]+(?=\d{3})/i;
console.log(hd.match(reg)); // shijei
复制代码

获取标题中的内容

let hd = `<h1>nihaoya</h1>`;
let reg = /(?<=<h1>).*(?=<\/h1>)/g;
console.log(hd.match(reg));
复制代码

零宽负向先行断言

(?!exp) 正向否定查找

用来检查接下来不应该出现匹配内容的字符集。

使用 (?!exp)字母后面不能为两位数字

let reg = /abc(?!de)/;
reg.test('abcdefg');  // false;
reg.test('abcd');  // true;
reg.test('abcabc');   // true;
复制代码

零宽负向后行断言

(?<!exp) 反向否定查找

用来检查前面不应该出现的字符集。

0x04 应用

1. 利用正则替换字符

使用断言模糊电话号

有时候我们需要对手机号码进行模糊展示,中间四位用 * 代替

// 不使用断言
let tels = `15036999999`;
let reg = /(\d{3})(\d{4})(\d+)/g;
tels = tels.replace(reg, (v, ...args) => {
  args[1] = "*".repeat(4);
  return args.splice(0, 3).join("");
});
console.log(tels); // 150****9999

// 使用断言
let newtel = `15036999999`;
function hideTel(tel) {
  // 匹配前面是由3位数字组成,后面是由4位数字组成的字段
  let newReg = /(?<=\d{3})\d{4}(?=\d{4})/g;
  return  tel.replace(newReg, (v) => {
    // 将这个字段替换成4个*号
    return "*".repeat(4);
  });
}
console.log(hideTel(newtel)); // 150****9999
复制代码

时间字符串的格式化处理

有时候后端返回的时间字符串统一为 '2021-8-13 16:32:2' 这种格式的,想转换成 '2021年08月13日16时32分02秒'

/**
 * formatTime: 时间字符串的格式化处理
 * @param { string } templete 时间
 * @param { string } templete 期望获取日期格式的模板
 *  模板格式: {0}->年 {1~5}->月日时分秒
 * @returns { string } 格式化后的时间字符串
 */
function formatTime(time, templete = "{0}年{1}月{2}日{3}时{4}分{5}秒") {
  // 首先获取时间字符串中的年月日等信息
  let timeAry = time.match(/\d+/g);
   
  return templete.replace(/\{(\d+)\}/g, (...[, $1]) => {
    // =>content: 当前本次大正则匹配的信息 $1: 本次小分组单独匹配的信息
    // 以$1的值作为索引,到 timeAry 中找到对应的时间(如果没有则用"00"补)
    let time = timeAry[$1] || "00";
    return time.length < 2 ? "0" + time : time;
  });
}

let time = "2021-8-13 16:32:2";
// time = "2021/8/26";
console.log(formatTime(time)); // 2021年08月13日16时32分02秒
console.log(formatTime(time, "{0}年{1}月{2}日")); // 2021年08月13日
console.log(formatTime(time, "{0}/{1}/{2}日 {3}:{4}:{5}")); // 2021/08/13日 16:32:02
console.log(formatTime(time, "{1}-{2} {3}:{4}")); // 08-13 16:32

let time1 = "2021/8/26";
console.log(formatTime1(time1)); // 2021年08月26日00时00分00秒
console.log(formatTime1(time1, "{0}年{1}月{2}日")); // 2021年08月26日
console.log(formatTime1(time1, "{1}-{2} {3}:{4}")); // 08-26 00:00
复制代码

2. 表单验证

微信号正则

//微信号正则,6至20位,以字母开头,字母,数字,减号,下划线
var wxPattern = /^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$/;
//输出 true
console.log(wxPattern.test("nihaoya_com"));
复制代码

验证数字,可正,可负,可为0

^0$|^-?[1-9]\d*$
复制代码

思考:数字价格千分位分割

123456789 => 123,456,789

思路 - 步骤:

  1. 从后往前每三个数字前加一个逗号

  2. 开头不能加逗号(比如:123 最后不能变成,123)

是不是很符合(?=p)的规律呢?p可以表示每三个数字,要添加的逗号所处的位置正好是(?=p)匹配出来的位置。

步骤一:把最后一个逗号弄出来

let price = '123456789'
let priceReg = /(?=\d{3}$)/

console.log(price.replace(proceReg, ',')) // 123456,789
复制代码

步骤二:把所有逗号弄出来

let price = '123456789'
let priceReg = /(?=(\d{3})+$)/g

console.log(price.replace(priceReg, ',')) // ,123,456,789
复制代码

步骤三:去掉首位的逗号

let price = '123456789'
let priceReg = /(?!^)(?=(\d{3})+$)/g

console.log(price.replace(priceReg, ',')) // 123,456,789
复制代码

结果:

0x05 总结

对于正则来说,合理的使用正则的话可以大大的简化我们的代码,也可以帮助我们去实现更加强大的功能,希望通过这次分享, 让大家可以去熟悉正则,以后遇到可以很好的去使用正则

0x06 推荐正则可视化工具 :

1. regex101.com/

2. regexper.com/

3. jex.im/regulex

分类:
前端
收藏成功!
已添加到「」, 点击更改