关于前端:就因为这三个知识点我彻底学废了正则表达式

36次阅读

共计 12554 个字符,预计需要花费 32 分钟才能阅读完成。

讲个小笑话

昨天下完班,良久才回到家,就因为公司楼下的停车场设计的和迷宫一样,每次都要找良久,能力发现,我没有车o(╥﹏╥)o。

欢送关注”前端胖头鱼“公众号,兴许你为素未谋面,但很可能相见恨晚噢。

前言

已经我一度对正则表达式有种恐怖和厌恶感,为啥?因为总感觉这玩意很难,很干燥,看到他人写出 贼牛逼的正则,我想啥时候我能像他们一样优良。直到我看到了这三个知识点。。。

只须要花 10 分钟 工夫,你能够播种

  1. 正则表达式中的 地位匹配原理与常识
  2. 正则表达式中的 字符串匹配原理与常识
  3. 正则表达式中的 括号的妙用
  4. 14 个常见正则表达式解析帮忙了解知识点

置信我,看完这篇文章,对于工作中 90% 以上的正则问题你都能找到解决思路和计划。

置信我,看完这篇文章,对于工作中 90% 以上的正则问题你都能找到解决思路和计划。

置信我,看完这篇文章,对于工作中 90% 以上的正则问题你都能找到解决思路和计划。

默念三声

正则表达式是 匹配模式 ,要么 匹配字符 ,要么 匹配地位

正则表达式是 匹配模式 ,要么 匹配字符 ,要么 匹配地位

正则表达式是 匹配模式 ,要么 匹配字符 ,要么 匹配地位

1. 搞懂地位无能啥?

题目 1:数字的千分位宰割法

将 123456789 转化为 123,456,789

题目 2:手机号 3 -4- 4 宰割

将手机号 18379836654 转化为 183-7983-6654

题目 3:验证明码的合法性

明码长度是 6 -12 位,由数字、小写字符和大写字母组成,但必须至多包含 2 种字符

这些题时常呈现在面试中,日常业务也少不了它的身影。搞懂地位,不仅能搞定面试,业务你也将写的飞起

啥是地位?

正则表达式是匹配模式,要么匹配字符,要么匹配地位。那什么是 地位 呢?

如下图箭头所指,地位能够了解为 相邻字符之间的地位

咱们能够和 空字符串 进行类比, 字符的首尾、间隙都能够用空字符串进行连贯。

'hello' === ''+'h'+'' + 'e' + ''+'l'+'' +  'l' + ''+'o'+'' // true

有哪些地位?

正则中罕用来示意地位的符号次要有:

^、$、\b、\B、?=p、(?!p)、(?<=p)、(?<!p)

接下来咱们就一个个把他们全整明确。

^

脱字符,匹配行的结尾

例如要在 hello 的结尾塞一个笑脸 (😄) 怎么搞, 这个必定难不倒你


let string = 'hello'

console.log(string.replace(/^/, '😄')) // 😄hello

$

美元符号,匹配行的结尾

同现实在 hello 的结尾塞一个笑脸 (😄) 呢?


let string = 'hello'

console.log(string.replace(/$/, '😄')) // hello😄

这两个示意首尾地位的符号,置信大家肯定都很相熟。

\b

单词的边界,具体讲有三点规定。

① \w 和 \W 之间的地位

② ^ 与 \w 之间的地位

③ \w 与 $ 之间的地位

比方藏在你们电脑上 学习教程 文件夹中的某一集种子长这样 xxx_love_study_1.mp4,想要把他变成❤️xxx_love_study_1❤️.❤️mp4❤️ 怎么搞呢?

其实只须要执行一行代码就行


'xxx_love_study_1.mp4'.replace(/\b/g, '❤️') // ❤️xxx_love_study_1❤️.❤️mp4❤️

画图了解就是

\B

非单词的边界,也就是 \b 反着来的意思,它的规定如下:

① \w 与 \w 之间的地位

② \W 与 \W 之间的地位

③^ 与 \W 之间的地位

④\W 与 $ 之间的地位

同样还是用 学习教程 文件夹中的种子,稍稍革新一下,当执行这行代码之后,会输入啥?

'[[xxx_love_study_1.mp4]]'.replace(/\B/g, '❤️')

….

没错,满满的都是爱啊!!!,都快看不清名字了。


❤️[❤️[x❤️x❤️x❤️_❤️l❤️o❤️v❤️e❤️_❤️s❤️t❤️u❤️d❤️y❤️_❤️1.m❤️p❤️4]❤️]❤️

画图解释如下

(?=p)

合乎 p 子模式后面的那个地位。换句话说是,有一个地位,紧跟其后须要满足 p 子模式。也有一个学名叫正向后行断言。

还是这个例子 xxx_love_study_1.mp4,要在 xxx(xxx 能够指代任何你喜爱的那个 TA) 后面塞一个❤️, 怎么写呢?

是这样吗?不是的,这样会导致你的 xxx 都不见了,那还要❤️做什么呢?


'xxx_love_study_1.mp4'.replace('xxx', '❤️') // ❤️_love_study_1.mp4

利用 (?=p) 就能够很不便这这件事(能够想想和下面有什么不同?)


'xxx_love_study_1.mp4'.replace(/(?=xxx)/g, '❤️') // ❤️xxx_love_study_1.mp4

画图了解

(?!p)

(?=p)反过来的意思,能够了解为 (?=p) 匹配到的地位之外的地位都是属于 (?!p) 的,它也有一个学名叫负向后行断言。


'xxx_love_study_1.mp4'.replace(/(?!xxx)/g, '❤️') 

// (?=xxx)的输入
❤️xxx_love_study_1.mp4
// (?!xxx)的输入
x❤️x❤️x❤️_❤️l❤️o❤️v❤️e❤️_❤️s❤️t❤️u❤️d❤️y❤️_❤️1❤️.❤️m❤️p❤️4❤️

认真比照一下,是不是除了 (?=xxx) 匹配到最后面那个地位,其余地位都是 (?!xxx) 匹配到的啦。

(?<=p)

合乎 p 子模式前面 (留神(?=p) 示意的是后面)的那个地位。换句话说是,有一个地位,其后面的局部须要满足 p 子模式。

仍然是这个例子:咱们要在 xxx(xxx 能够指代任何你喜爱的那个 TA)的前面塞一个❤️, 怎么写呢?

'xxx_love_study_1.mp4'.replace(/(?<=xxx)/g, '❤️') //xxx❤️_love_study_1.mp4

画图解释

(?<!p)

(?<=p)反过来的意思,能够了解为 (?<=p) 匹配到的地位之外的地位都是属于 (?<!p) 的,


'xxx_love_study_1.mp4'.replace(/(?<!xxx)/g, '❤️') 

// (?<=xxx)的输入
xxx❤️_love_study_1.mp4
// (?<!xxx)的输入
❤️x❤️x❤️x_❤️l❤️o❤️v❤️e❤️_❤️s❤️t❤️u❤️d❤️y❤️_❤️1❤️.❤️m❤️p❤️4❤️

认真比照一下,是不是除了 (?<=xxx) 匹配到前面那个地位,其余地位都是 (?<!xxx) 匹配到的啦。

栗子详解

学习完地位相干的常识,咱们来做一下结尾的几个题目试试

题目 1:数字的千分位宰割法

将 123456789 转化为 123,456,789

察看题目的法则就是从后往前,每三个数字前加一个逗号,(须要留神的是结尾不须要加逗号,)。是不是很合乎
(?=p) 的法则呢?p 能够示意每三个数字,要增加的逗号所处的地位正好是 (?=p) 匹配进去的地位。

第一步,尝试先把前面第一个逗号弄出来



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

console.log(price.replace(priceReg, ',')) // 123456,789

第二步,把所有的逗号都弄出来

要把所有的逗号都弄出来,次要要解决的问题是怎么示意 三个数字一组, 也就是 3 的倍数。咱们晓得正则中括号能够把一个 p 模式变成一个小整体,所以利用括号的性质,能够这样写



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

console.log(price.replace(priceReg, ',')) // ,123,456,789

第三步,去掉首位的逗号,

下面曾经基本上实现需求了,然而还不够,首位会呈现, 那怎么把首位的逗号去除呢?想想后面是不是有一个常识正好满足这个场景?没错(?!p),就是他了,两者联合就是从后往前每三个数字的地位前增加逗号,然而这个地位不能是 ^ 首位。


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

console.log(price.replace(priceReg, ',')) // 123,456,789

题目 2:手机号 3 -4- 4 宰割

将手机号 18379836654 转化为 183-7983-6654

有了下面数字的千分位宰割法,做这个题置信会简略很多,也就是从后往前找到这样的地位:

每四个数字前的地位,并把这个地位替换为 -


let mobile = '18379836654'
let mobileReg = /(?=(\d{4})+$)/g

console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654

题目 3:手机号 3 -4- 4 宰割扩大

将手机号 11 位以内的数字转化为 3 -4- 4 格局

回忆一下这样的场景,有一个表单须要收集用户的手机号,用户是一个个数字输出的,咱们须要在用户输出 11 位手机号的过程中把其转化为 3 -3- 4 格局。即

123 => 123
1234 => 123-4
12345 => 123-45
123456 => 123-456
1234567 => 123-4567
12345678 => 123-4567-8
123456789 => 123-4567-89
12345678911 => 123-4567-8911

这样用 (?=p) 就不太适合了,例如 1234 就会变成 -1234。
想想后面的知识点有适宜解决这种场景的吗?是的(?<=p)

第一步, 将第一个 - 弄出来

const formatMobile = (mobile) => {return String(mobile).replace(/(?<=\d{3})\d+/, '-')      
}

console.log(formatMobile(123)) // 123
console.log(formatMobile(1234)) // 123-4

将第二个 - 弄出来

将第一个 - 弄出来之后,字符的长度多了一位,本来 1234567(这个地位插入 -)8,要变成往后移一位

const formatMobile = (mobile) => {return String(mobile).slice(0,11)
      .replace(/(?<=\d{3})\d+/, ($0) => '-' + $0)
      .replace(/(?<=[\d-]{8})\d{1,4}/, ($0) => '-' + $0)
}

console.log(formatMobile(123)) // 123
console.log(formatMobile(1234)) // 123-4
console.log(formatMobile(12345)) // 123-45
console.log(formatMobile(123456)) // 123-456
console.log(formatMobile(1234567)) // 123-4567
console.log(formatMobile(12345678)) // 123-4567-8
console.log(formatMobile(123456789)) // 123-4567-89
console.log(formatMobile(12345678911)) // 123-4567-8911

题目 4:验证明码的合法性

明码长度是 6 -12 位,由数字、小写字符和大写字母组成,但必须至多包含 2 种字符

题目由三个条件组成

① 明码长度是 6 -12 位

② 由数字、小写字符和大写字母组成

③ 必须至多包含 2 种字符

第一步写出条件①和②和正则

let reg = /^[a-zA-Z\d]{6,12}$/

第二步,必须蕴含某种字符(数字、小写字母、大写字母)

let reg = /(?=.*\d)/
// 这个正则的意思是,匹配的是一个地位,这个地位须要满足 ` 任意数量的符号,紧跟着是个数字 `,留神它最终失去的是个地位,而不是数字或者是数字后面有任意的货色

console.log(reg.test('hello')) // false
console.log(reg.test('hello1')) // true
console.log(reg.test('hel2lo')) // true

// 其余类型同理

第三步,写出残缺的正则

必须蕴含两种字符,有上面四种排列组合形式

① 数字和小写字母组合

② 数字和大写字母组合

③ 小写字母与大写字母组合

④ 数字、小写字母、大写字母一起组合(但其实后面三种曾经笼罩了第四种了)

// 示意条件①和②
// let reg = /((?=.*\d)((?=.*[a-z])|(?=.*[A-Z])))/
// 示意条件条件③
// let reg = /(?=.*[a-z])(?=.*[A-Z])/
// 示意条件①②③
// let reg = /((?=.*\d)((?=.*[a-z])|(?=.*[A-Z])))|(?=.*[a-z])(?=.*[A-Z])/
// 示意题目所有条件
let reg = /((?=.*\d)((?=.*[a-z])|(?=.*[A-Z])))|(?=.*[a-z])(?=.*[A-Z])^[a-zA-Z\d]{6,12}$/


console.log(reg.test('123456')) // false
console.log(reg.test('aaaaaa')) // false
console.log(reg.test('AAAAAAA')) // false
console.log(reg.test('1a1a1a')) // true
console.log(reg.test('1A1A1A')) // true
console.log(reg.test('aAaAaA')) // true
console.log(reg.test('1aA1aA1aA')) // true

2. 字符串匹配原来这么简略

两种含糊匹配

正则如果只有准确匹配,那么便齐全没有了意义

横向

一个正则可匹配的字符串的长度不是固定的,能够是多种状况,通过量词 +、*、?、{m,n},可实现横向匹配

let reg = /ab{2,5}c/
let str = 'abc abbc abbbc abbbbc abbbbbc abbbbbbc'

str.match(reg) // ['abbc', 'abbbc', 'abbbbc', 'abbbbbc']

纵向

一个正则匹配的字符串,具体到某一位字符时,能够不是某个确定的字符串,能够有多种可能,实现形式是字符组(其实多选分支 | 也能够实现)

let reg = /a[123]c/
let str = 'a0b a1b a2b a3b a4b'

str.match(reg) // ['a1b', 'a2b', 'a3b']

字符组

不要被名字给糊弄了,尽管他叫做字符组,但其实只是代表一个字符的可能性

范畴表示法

[123456abcdefABCDEF] => [1-6a-fA-F]

排除字符组

某位字符能够是任何货色,然而就是不能是 xxx, 应用 ^ 符号

问题:如何要示意除了某个单词之外的任意货色呢?

[^abc]

常见简写模式

\d // 数字
\D // 非数字
\w // [0-9a-zA-Z_]
\W // [^0-9a-zA-Z_]
\s // [\t\v\n\r\f]
\S // [^\t\v\n\r\f]
.

量词

量词 & 简写

1. {m,} // 至多呈现 m 次
2. {m} // 呈现 m 次
3. ? // 呈现 0 次或者 1 次,等价于{0,1}    
4. + // 至多呈现 1 次, 等价于{1,} 
5. * // 呈现人一次, 等价于{0,}  

贪心匹配 VS 惰性匹配

正则自身是贪心的,会尽可能的多匹配合乎模式的字符

let regex = /\d{2,5}/g
let string = '123 1234 12345 123456'
// 贪心匹配
// string.match(regex) // [123, 1234, 12345, 12345]

// 惰性匹配
let regex2 = /\d{2,5}?/g
// string.match(regex) // [12, 12, 34, 12, 34, 12, 34, 56]

量词前面加一个?,即变成了惰性匹配

贪心量词        惰性量词
{m,n}            {m,n}?
{m,}             {m,}?
?                       ??
+                       +?
*                   *?  

多选分支

一个模式能够实现横向和纵向的含糊匹配,而多选分支能够反对多个子模式任选其一,模式是(p1|p2|p3)

let regex = /good|nice/
let string = 'good idea, nice try.'

// string.match(regex) // ['good', 'nice']

// 留神,用 /good|goodbye/ 去匹配 'goodbye' 匹配到的是 good
// 因为分支构造是惰性的,后面的匹配上了,前面的就不再尝试了

案例剖析

1. 匹配 id

// 1
let regex = /id=".*?"/ // 想想为什么要加? 不加的话 连前面的 class 都会匹配到
let string = '<div id="container"class="main"></div>';
console.log(string.match(regex)[0]);
// 2
let regex = /id="[^"]*"/ 
let string = '<div id="container"class="main"></div>'; 
console.log(string.match(regex)[0]); 

2. 匹配 16 进制的色彩值

// 要求匹配如下色彩
/*
#ffbbad
#Fc01DF
#FFF
#ffE
*/

let regex = /#([a-fA-F\d]{6}|[a-fA-F\d]{3})/g
let string = "#ffbbad #Fc01DF #FFF #ffE";

console.log(string.match(regex))
//  ["#ffbbad", "#Fc01DF", "#FFF", "#ffE"]

3. 匹配 24 小时制工夫

/*
    要求匹配
  23:59
  02:07
*/
// 解析:// 第一位:能够是 0、1、2
// 第二位:当第一位位 0 或者 1 的时候,能够是 0 到 9、第一位是 2 的时候,只能够是 0 到 3
// 第三位:固定是冒号:// 第四位:能够是 0 到 5
// 第五位:0 到 9

let regex = /^([01]\d|2[0-3]):[0-5]\d$/

console.log(regex.test('23:59')) // true
console.log(regex.test('02:07'))// true

// 衍生题,能够是非 0
let regex = /^(0?\d|1\d|2[0-3]):(0?|[1-5])\d/

console.log(regex.test("23:59") ) // true
console.log(regex.test("02:07") ) // true
console.log(regex.test("7:09") ) // true

4. 匹配日期

/*
    要求匹配
  yyyy-mm-dd 格局的日期
  留神月份、和日的匹配
*/

let regex = /\d{4}-(0\d|1[0-2])-(0[1-9]|[12]\d|3[01])/

console.log(regex.test("2017-06-10") ) // true
console.log(regex.test("2017-11-10") ) // true

3. 括号的神奇作用

括号的作用是提供了分组(括号内的正则是一个整体,即提供子表达式),便于咱们援用它

分组

如何让量词作用于一个整体?

let reg = /(ab)+/g
let string = 'ababa abbb ababab'

console.log(string.match(reg)) // ["abab", "ab", "ababab"]

分支构造

分支构造有点像编程外面或的概念 ||

/*
匹配 
I love JavaScript
I love Regular Expression
*/

let reg = /I love (JavaScript|Regular Expression)/

console.log(reg.test('I love JavaScript')) // true
console.log(reg.test('I love Regular Expression')) // true

分组援用

通过括号创立子表达式,能够进行数据提取和弱小的替换操作,也能够通过 js 来援用分组内容

提取数据

/*
提取年月日
2021-08-14
*/

let reg = /(\d{4})-(\d{2})-(\d{2})/

console.log('2021-08-14'.match(reg))
//  ["2021-08-14", "2021", "08", "14", index: 0, input: "2021-08-14", groups: undefined]

// 第二种解法, 通过全局的 $1...$9 读取 援用的括号数据
let reg = /(\d{4})-(\d{2})-(\d{2})/
let string = '2021-08-14'

reg.test(string)

console.log(RegExp.$1) // 2021
console.log(RegExp.$2) // 08
console.log(RegExp.$3) // 14

数据替换

/*
将以下格局替换为 mm/dd/yyy
2021-08-14
*/
// 第一种解法
let reg = /(\d{4})-(\d{2})-(\d{2})/
let string = '2021-08-14'
// 第一种写法
let result1 = string.replace(reg, '$2/$3/$1')
console.log(result1) // 08/14/2021
// 第二种写法
let result2 = string.replace(reg, () => {return RegExp.$2 + '/' + RegExp.$3 + '/' + RegExp.$1})
console.log(result2) // 08/14/2021
// 第三种写法
let result3 = string.replace(reg, ($0, $1, $2, $3) => {return $2 + '/' + $3 + '/' + $1})
console.log(result3) // 08/14/2021

反向援用(很重要)

除了通过 js 援用分组的内容,也能够通过正则来援用分组内容

/*
    写一个正则反对以下三种格局
  2016-06-12
  2016/06/12
  2016.06-12
*/
let regex = /(\d{4})([-/.])\d{2}\1\d{2}/

var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";

console.log(regex.test(string1) ); // true
console.log(regex.test(string2) ); // true
console.log(regex.test(string3) ); // true
console.log(regex.test(string4) ); // false

留神

  1. 援用不存在的分组会怎么?

    1. 即匹配的就是 \1 \2 自身
  2. 分组前面有量词会怎么?

    1. 分组前面如果有量词,分组最终 (留神是分组,不是说整体) 捕捉的数据是最初一次的匹配
'12345'.match(/(\d)+/) // ["12345", "5", index: 0, input: "12345", groups: undefined]

/(\d)+ \1/.test('12345 1') // false
/(\d)+ \1/.test('12345 5') // true

非捕捉性括号

下面应用的括号都会匹配他们匹配到的数据,以便后续援用,所以也能够称为捕捉型分组和捕捉型分支。

如果想要括号最原始的性能,但不会援用它,也就是既不会呈现在 API 援用里,也不会呈现在正则援用里,能够应用

非捕捉性括号(?:p)

// 非捕捉型援用
let reg = /(?:ab)+/g
console.log('ababa abbb ababab'.match(reg)) // ["abab", "ab", "ababab"]
// 留神这里,因为是非捕捉型分组,所以应用 match 办法时,不会呈现在数组的 1 地位了
let reg = /(?:ab)+/
console.log('ababa abbb ababab'.match(reg)) // ["abab", index: 0, input: "ababa abbb ababab", groups: undefined]
let reg = /(ab)+/
console.log('ababa abbb ababab'.match(reg)) // ["abab", "ab", index: 0, input: "ababa abbb ababab", groups: undefined]

案例

1.trim 办法模仿

// 1. 提取两头要害字符, 应用的分组援用
const trim1 = (str) => {return str.replace(/^\s*(.*?)\s*$/, '$1')
}
// 2. 去掉结尾和结尾的空字符
const trim2 = (str) => {return str.replace(/^\s*|\s*$/g, '')
}

2. 将每个单词的首字母大写

要害是要找到每个单词的首字母

// my name is epeli

const titleize = (str) => {return str.toLowerCase().replace(/(?:^|\s)\w/g, (c) => c.toUpperCase())
}  

console.log(titleize('my name is epeli')) // My Name Is Epeli

// 拓展,横向转驼峰,例如 base-act-tab => BaseActTab
'base-act-tab'.replace(/(?:^|-)(\w)/g, ($0, $1) => $1.toUpperCase()) // BaseActTab

3. 驼峰化

// -moz-transform => MozTransform
const camelize = (str) => {return str.replace(/[-_\s]+(\w)/g, (_, $1) => $1.toUpperCase())     
}

console.log(camelize('-moz-transform')) // MozTransform

4. 中划线化

// MozTransform => -moz-transform
const dasherize = (str) => {return str.replace(/[A-Z]/g, ($0) => ('-' + $0).toLowerCase())
}

console.log(dasherize('MozTransform')) // -moz-transform

5.HTML 本义和反本义

// html 本义规定见 https://blog.wpjam.com/m/character-entity/

const escapeHTML = (str) => {
    const escapeChars = {
    '<': 'lt',
    '>': 'gt',
    '"':'quot',''': '#39',
    '&': 'amp'
  }
  
  let regexp = new RegExp(`[${Object.keys(escapeChars).join('')}]`,'g') // 为了失去字符组[<>"'&]
    
    return str.replace(regexp, (c) => `&${escapeChars[ c]};`)
}

console.log(escapeHTML('<div>Blah blah blah</div>')) // &lt;div&gt;Blah blah blah&lt;/div&gt;


// 反本义
const unescapseHTML = (str) => {
    const htmlEntities = {
    nbsp: ' ',
    lt: '<',
    gt: '>',
    quot: '"',
    amp: '&',
    apos: '''
  }
  
  return str.replace(/&([^;]+);/g, ($0, $1) => {return htmlEntities[ $1] || ''
    })
}

console.log(unescapseHTML('&lt;div&gt;Blah blah blah&lt;/div&gt;')) // <div>Blah blah blah</div>

6. 匹配成对的标签

/*
    匹配
      <title>regular expression</title>
        <p>laoyao bye bye</p>
  不匹配
    <title>wrong!</p>
*/
let reg = /<([^>]+)>.*?</\1>/g

console.log(reg.test('<title>regular expression</title>')) // true
console.log(reg.test('<p>laoyao bye bye</div>')) // false

相约再见

强烈推荐 老姚 的正则表达式迷你小书,笔者也是读了这本书之后缓缓开始对正则有些了解,不再抗拒它,这篇文章次要也是基于这本书的内容做了总结。

参考

  1. JS 正则表达式残缺教程(略长)
  2. 三十分钟包会——正则表达式
  3. 聊聊让人头疼的正则表达式

正文完
 0