正则表达式-进阶篇

272 阅读2分钟

分组

() 分组

在正则表达式中还提供了一种将表达式分组的机制,当使用分组时,除了获得整个匹配。还能够在匹配中选择每一个分组

🌰:
image.png

这段正则表达式将文本分成了两组,第一组为0731,第二组为88293892

分组有一个非常重要的功——捕获数据。所以()也被称为捕获分组,用来捕获数据,当我们想要从匹配好的数据中提取关键数据的时候可以使用分组

下面我们来看几组🌰:

  1. 提取p标签中的数据
let elementArr = [
    '<p>Kayce</p>',
    '<p>Beth</p>',
    '<p>Jamie</p>',
    '<p>Rip</p>'
]

let pattern = /<p>(\w+)</p>/
const result = []
for (let i of elementArr) {
    result.push(pattern.exec(i)[1])
}
console.log(result)
// ['Kayce', 'Beth', 'Jamie', 'Rip']
  1. 提取学号 有些学校的学号是由多个关键信息组成的,例如:2019-5013-08 2019表示入学年份,5013表示班级代码,08表示班级中的排序,接下来用正则表达式匹配不同格式的学号,并将其中的关键信息用分组提取出来。
let numberArr = [
    '2019-5013-08',
    '2020 5566 09',
    '2022 0103 07',
]

let pattern = /(\d{4}).*?(\d{4}).*?(\d{2})/
const result = []
for (let i of numberArr) {
    result.push([...pattern.exec(i)])
}
console.log(result)
/*
* ['2019-5013-08', '2019', '5013', '08']
* ['2020 5566 09', '2020', '5566', '09']
* ['2022 0103 07', '2022', '0103', '07']*/

| 或者条件

使用分组的同时还可以使用或者(or)条件

例如我们要从一堆文件中找出属于图片的文件:

我们这里把jpg,png,gif,jpeg归类为图片文件,那么凡是以这四个类型结尾的文件,我们都可以认为是图片文件

let fileArr = [
    'image.jpg',
    'image.png',
    'image.jpeg',
    'image.gif',
    'not_image.txt',
    'not_image.doc',
    'not_image.xls',
    'not_image.ppt'
]

let pattern = /(.jpg|.png|.gif|.jpeg)$/
const result = []
for (let i of fileArr) {
    if (pattern.test(i)) {
        result.push(i)
    }
}
console.log(result)
// ['image.jpg', 'image.png', 'image.jpeg', 'image.gif']

非捕获分组

有时候,我们并不需要捕获某个分组的内容,但是又想使用分组的特性,这个时候我们就可以使用(?:表达式),从而不捕获数据,还能使用分组的功能

举个🌰:
现在有几组电话号码,我们想用两个分组来匹配数据,但是我们只需要提取-或者:后面的这一串电话数字

01-75855,0731-99384,0512-39402,tel:039485

let phoneArr = [
    '01-75855',
    '0731-99384',
    '0512-39402',
    'tel:039485',
]

let pattern18 = /(?:\w+)[-:](\d{5})/
const result18 = []
for (let i of phoneArr) {
    result18.push([...pattern18.exec(i)])
}
console.log(result18)
/*
* 0: (2) ['01-75855', '75855']
* 1: (2) ['0731-99384', '99384']
* 2: (2) ['0512-39402', '39402']
* 3: (2) ['tel:03948', '03948']*/

\N 分组的回溯引用

正则表达式还提供了一种引用之前匹配分组的机制,有时候,我们或许会寻找一个子匹配,该匹配接下来会再次出现。

例如,我们要匹配一段HTML代码,比如:0123<font>hello world</font>abcd
我们编写的正则表达式可能是这样的:/<\w+>.*<\/\w+>/g

乍一看匹配的似乎没有问题 image.png

But

当我们把结束标签中的内容换成bar的时候,依然可以匹配。但这显然不是我们想要的效果,这不是一对正确的标签 image.png

那么想让结束标签的正则和开始标签的正则匹配相同的数据该如何做呢?

先上结果:/<(\w+)>.*<\/\1>/g

可以看到,这是我们想要的结果。这里\1其实就代表了前面分组括号中的内容(\w+) image.png

如果看到这里还有些云里雾里的话,下面再用一个例子来对回溯引用有一个进一步的了解:

现在有这样一组数据:

mbaabm,lonnol,ldccbk,iooiss

我们要匹配前面两组数据,先来分析,他们共有的格式,可以得出是123321这种格式的数据

再来看下面这张图

image.png

我们看到表达式中我们写了三个分组(\w),这时候就可以用回溯引用来表示前面三个分组,分别是\1,\2,\3,根据我们上面得出的数据格式123321,我们的正则表达式就可以表示成 /(\w)(\w)(\w)\3\2\1/ image.png

如果回溯引用表示成\N,那么N就是分组的顺序编号(从左至右)

零宽断言

很多人也称零宽先行断言和零宽后行断言为环视,也有人叫预搜索

基本概念:零宽断言正如它的名字一样,是一种零宽度的匹配,它匹配到的内容不会保存到匹配结果中去,最终匹配结果只是一个位置而已

作用:给指定位置添加一个限定条件,用来规定此位置之前或者之后的字符必须满足限定条件才能使正则中的子表达式匹配成功。

零宽先行断言和零宽后行断言总共有四种:

  1. 正向零宽先行断言
  2. 反向零宽现行断言
  3. 正向零宽后行断言
  4. 反向零宽后行断言

零宽先行断言

(?=表达式) 正向零宽先行断言

指在某个位置向右看,表示所在位置右侧必须能匹配表达式

🌰:
我喜欢你,我喜欢,我喜欢我,喜欢,喜欢你,现在要从这几个字符中取出喜欢两个字,并且要求喜欢后面有你,这个时候我们就可以这么写:/喜欢(?=你)/,这就是正向零宽先行断言 image.png

分析:

image.png

零宽先行断言可以用来判断字符串是否符合特定的规则,下面我们来看一个密码强度验证的🌰

规则如下:

  • 至少一个大写字母
  • 至少一个小写字母
  • 至少一个数字
  • 至少8个字符

现在有这样几组密码:Admin123456,oYHMDdHCK9,wdfqe#wefDdf444,123456,admin123,ADMIN123(),编号89757

匹配符合要求的密码。

let passwordArr = [
    'Admin123456',
    'oYHMDdHCK9',
    'wdfqe#wefDdf444',
    '123456',
    'admin123',
    'ADMIN123()',
    '编号89757'
]

let pattern = /(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}/
const result = []
for (let i of passwordArr) {
    if (pattern.test(i)) {
        result.push(i)
    }
}
console.log(result)
// ['Admin123456', 'oYHMDdHCK9', 'wdfqe#wefDdf444']

(?!表达式) 反向零宽先行断言

反向零宽先行断言可以理解为与正向零宽先行断言的作用相反,即所在位置右侧不能出现某字符,有点=!=的意思

匹配不是qq邮箱的数据

let emailArr = [
    'abc@sina.com',
    'qq@163.com',
    'a@google.com',
    'qq@123.com',
    'test@qq.com',
    'qq@qq.com',
    'gc@qq.com',
    '163@qq.com'
]

let pattern20 = /\w+@(?!qq)\w+.com/
const result20 = []
for (let i of emailArr) {
    if (pattern20.test(i)) {
        result20.push(i)
    }
}
console.log(result20)
// ['abc@sina.com', 'qq@163.com', 'a@google.com', 'qq@123.com']

分析: image.png

匹配除<p></p>之外的所有标签

let elementsArr = [
    '<div></div>',
    '<h1></h1>',
    '<h2></h2>',
    '<h3></h3>',
    '<h4></h4>',
    '<h5></h5>',
    '<tr></tr>',
    '<td></td>',
    '<p></p>',
    '<p>code</p>',
    '<p></p><p></p>',
    'p></p><p></p>',
    'p></p><p></p>',
    'p></p><p></p>',
    'p></p><p></p>',
    'p></p><p>',
]

let pattern21 = /<(?!p|/p).+>/
const result21 = []
for (let i of elementsArr) {
    if (pattern21.test(i)) {
        result21.push(i)
    }
}
console.log(result21)
// ['<div></div>', '<h1></h1>', '<h2></h2>', '<h3></h3>', '<h4></h4>', '<h5></h5>', '<tr></tr>', '<td></td>']

零宽后行断言

零宽后行断言只需要你记住一句话:零宽先行断言和零宽后行断言只有一个区别,即先行断言从左往右看,后行断言从右往左看

(?<=表达式) 正向零宽后行断言

在某个位置向左看,表示所在位置左侧必须能匹配表达式

还是拿我喜欢你来举🌰:

我喜欢你,我喜欢,我喜欢我,喜欢,喜欢你,现在要从这几个字符中取出喜欢两个字,并且要求喜欢前面有我,后面有你,这个时候就要这么写:/(?<=我)喜欢(?=你)/

image.png

匹配王姓同学的名字

let nameArr = [
    '王芳',
    '王芳芳',
    '芳芳 王菲',
    '菲菲 王菲菲',
    '王五',
    '张三',
    '李四 小王',
    '富贵',
    '二麻子',
    '大王',
]

let pattern22 = /.(?<=王).+/
const result22 = []
for (let i of nameArr) {
    if (pattern22.test(i)) {
        result22.push(pattern22.exec(i)[0])
    }
}
console.log(result22)
// ['王芳', '王芳芳', '王菲', '王菲菲', '王五']

(?<!表达式) 反向零宽后行断言

在某个位置向左看,表示所在位置左侧不能匹配表达式

匹配一个美元符号中的数据

要求:以$开头,以$结束,并且匹配两个$中的数据

let strArr = [
    '$ a = a^2 $',
    '$123$',
    '$ A = a / b + b $',
    '$ a = a^2 $',
    '$123$',
    '$ A = a / b + b $',
    '$ a^{2}+ 3\frac{2}{3} $',
    '$ a**N&123% $',
    'abc$asddadd$$',
    '$$$ a=b^2 $$',
    'abc$asddadd$$',
    'abc$ a=b^2 $$abc',
    'abc$$ a = a**2 $$',
    '$$ a=b^2 $$',
    'abc$asddadd$$',
    '$$$ a=b^2 $$$$',
    '$$$ a=b^2bruce $',
    '$a=b^2bruce $$$',
    '$a=b^2bruc$$e $',
]

let pattern = /^\$(?!\$)([^$]*)(?<!\$)\$$/
const result = []
for (let i of strArr) {
    if (pattern.test(i)) {
        result.push(pattern.exec(i)[1])
    }
}
console.log(result)