无法避免的正则表达式

前言

什么是正则表达式?

我们在日常开发中总是能遇到一些需要校验的功能,例如校验邮箱、手机号等等,或者对一些字符串做替换处理等,正则表达式总是避不开的。个人理解正则就是用来匹配字符,校验字符规则,或者是提取一段字符中的内容。本文只介绍正则的一些基本使用。

匹配字符

匹配模糊长度

1
2
3
const regexp = /ab{1,5}c/g
const str = "abc abbc abbbc abbbbbbbc"
console.log(str.match(regexp)) // [ 'abc', 'abbc', 'abbbc' ]

通过 {m,n} 的形式来设置匹配的长度范围,以上代码匹配的就是 b 出现 1 - 5 次。而且 {m,n}中,可以省略,代表匹配出现至少 m 次,或者只写 m,{m} 代表匹配只出现 m 次。

匹配模糊字符

1
2
3
const regexp = /a[bcde]a/g
const str = "aba aca ada aaa"
console.log(str.match(regexp)) // [ 'aba', 'aca', 'ada' ]

通过 [] 的形式来设置可能出现的字符,以上代码匹配的就是 bcde 中出现任意一个。

排除某些字符用 ^

1
2
3
const regexp = /a[^bcde]c/g
const str = "abc acc adc aac"
console.log(str.match(regexp)) // [ 'aac' ]

通过 ^ 来设置不匹配哪些字符,以上代码匹配的就是 除了 bcde 字符外任意字符

多分支匹配

1
2
3
const regexp = /a|b|cd|efg/g
const str = "adbdcdefg"
console.log(str.match(regexp)) // [ 'a', 'b', 'cd', 'efg' ]

多分支匹配通过 | 实现,意思是匹配其中任意一个即可,和 JS 中的 类似,另外需要注意当前面的分支匹配上后就不会继续进行匹配了。

匹配位置

锚字符

  • ^ 匹配开头
  • $ 匹配结尾
  • \b 单词字符和非单词字符之间,也就是 \w 和 \W 之间
  • \B 把 \b 取反
  • (?=p) p 前的位置
  • (?=p) 除了p 前的位置

开头结尾 ^ $

1
2
3
const regexp = /^ab$/g
var string = "ab";
console.log(string.match(regexp)) // [ 'ab' ]

以上代码就是匹配以 a 开头,b 结尾的字符串,另外字符串有一个方法 replace 可以通过匹配位置进行替换。

1
2
var string = "abdefg";
console.log(string.replace(/^|$/g, '#')) // #abdefg#

通过匹配字符串的开头和结尾位置,替换成 #。

1
2
3
4
5
6
7
8
9
10
var string = "a\nb\nd\ne\nf\ng";
console.log(string.replace(/^|$/gm, '#'))
/*
#a#
#b#
#d#
#e#
#f#
#g#
*/

需要注意的是,在多行模式匹配下,开头和结尾是每行的开头结尾。

单词边界 \b \B

1
2
var string = "Hello,[JS] Hello World";
console.log(string.replace(/\b/g, '#')) // #Hello#,[#JS#] #Hello# #World#

单词边界就是 \w 和 \W 之间,也就是所有数字、字符串以及下划线 和 其他字符之间,也就是下图箭头所指的位置。注意看箭头两边的字符分别是\w 和 \W,另外要注意的是开头和结尾也是 \W。
单词边界
那 \B 就很好理解了,就是除了 \w 所指位置的其他位置。

先行断言

(?=p)(正向先行断言)指 p 前面的位置,这个 p 就是一个子模式,先用 p 进行匹配,然后再取他前面的位置

1
2
const string = "Hello,[JS] Hello World";
console.log(string.replace(/(?=\w)/g, '#')) // #H#e#l#l#o,[#J#S] #H#e#l#l#o #W#o#r#l#d

(?!p)(负向先行断言)就是和 (?=p) 相反,就是先匹配非 p 然后取前面的位置

1
console.log(string.replace(/(?!\w)/g, '#')) // Hello#,#[JS#]# Hello# World#

(?<=p) 指 p 后面的位置

1
console.log(string.replace(/(?<=\w)/g, '#')) // H#e#l#l#o#,[J#S#] H#e#l#l#o# W#o#r#l#d#

(?>!p)(?<=p) 相反

1
console.log(string.replace(/(?<!\w)/g, '#')) // #Hello,#[#JS]# #Hello #World

实践

给金额加上千分位逗号 例如 123456789 -> 123,456,789
解题思路:先选中倒数第三位前的位置替换成逗号,然后出现多次,最后排除开头的位置。
需要匹配的位置
我们要做的就是选中图中箭头的位置

第一步

1
2
3
const string = "123456789";
const regexp = /(?=\d{3}$)/g
console.log(string.replace(regexp, ",")); // 123456,789

第一步
上面的正则匹配的就是以三个数字结尾前的位置,然后替换成逗号。
第二步

1
2
const regexp = /(?=(\d{3})+$)/g
console.log(string.replace(regexp, ",")); // ,123,456,789

第二步
通过 + 让三个数字匹配多次,达到匹配上每三位数字前的位置,然后替换成逗号,这时发现开头的位置多了一个逗号,我们需要排除掉开始的位置
第三步

1
2
const regexp = /(?!^)(?=(\d{3})+$)/g
console.log(string.replace(regexp, ",")); // 123,456,789

通过(?!^)匹配除了开始位置的其他位置,这种方法就排除掉了开头,达到了预期的效果。

影响量词的匹配行为

贪婪匹配

正则在进行量词匹配时,会尽可能地多匹配。

1
2
3
const regexp = /a{1,3}/g
const str = "aaa aaaa aaaaa"
console.log(str.match(regexp)) // [ 'aaa', 'aaa', 'a', 'aaa', 'aa' ]

惰性匹配(非贪婪匹配)

我们使用 ? 来声明惰性匹配,惰性匹配每次只满足最低要求即可。

1
2
3
const regexp = /a{1,3}?/g
const str = "aaa"
console.log(str.match(regexp)) // [ 'a', 'a', 'a' ]

分组

我们可以通过()来对正则表达式进行分组,如

1
2
3
const regexp = /(abc)+/g
const str = "abcabc abccc"
console.log(str.match(regexp)) // [ 'abcabc', 'abc' ]

abc 括起来使他们成为一个分组,+的就是这个分组。

提取分组的数据

我们对正则进行分组匹配后还可以拿到分组匹配到的数据,

1
2
3
4
const regexp = /(\d+)([a-z]+)/
const str = "1231abc"
console.log(str.match(regexp))
console.log(regexp.exec(str))

两个输出结果相同,输出结果:
输出结果

需要注意的是,在捕获分组的时候,使用全部匹配 /g 就不返回分组信息了。

1
2
3
const regexp = /(\d+)([a-z]+)/g
const str = "1231abc"
console.log(str.match(regexp)) // [ '1231abc' ]

反向引用

在正则表达式中,如果有一个不确定的字符,后面还需要用到这个相同的字符,就可以用反向引用的方式

1
2
3
4
5
6
7
8
const regexp = /\d{3}([,\-])\d{3}\1\d{3}/
const str = "123,456,789";
const str2 = "123-456-789";
const str3 = "123-456,789";

console.log(regexp.test(str)) // true
console.log(regexp.test(str2)) // true
console.log(regexp.test(str3)) // false

这里我们用 \1 来引用第一个分组的字符,所以当前面是 , 后面也只能是 ,,这样就能做到前后一致。
另外如果出现嵌套括号的情况,我们以左括号出现的顺序为引用的顺序

1
2
3
4
5
6
7
const regexp = /^((\d)(\d(\d)))\1\2\3\4$/;
/*
* \1 => ((\d)(\d(\d)))
* \2 => (\d)
* \3 => (\d(\d))
* \4 => (\d)
* */

红色是 \1 ,绿色是 \2 ,橙色是 \3 ,蓝色是 \4

如果不希望分组被提取引用,使用非捕获分组 (?:p),我们把上面的代码修改一下看结果。

1
2
3
const regexp = /(\d+)([a-z]+)/
const str = "1231abc"
console.log(str.match(regexp))

输出结果:
非捕获分组输出

附录

详细查阅: 正则表达式

匹配数量简写

  • {m,} 至少出现 m 次数
  • {m} 只出现 m 次数
  • ? 不出现或者出现一次 等价于 {0,1}
  • + 至少出现一次
  • * 有没有都行

匹配范围简写

  • \d 表示数字
  • \D 除了数字外任意字符
  • \w 数字+字母+下划线
  • \W 除了 \w
  • \s 空白字符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符
  • \S 非空白符
  • . 任意字符

修饰符

  • i 不区分大小写
  • g 全局匹配
  • m 多行匹配
  • s 允许 . 匹配换行符。
  • u 使用 unicode 码的模式进行匹配。
  • y 粘性 匹配从目标字符串的当前位置开始。

无法避免的正则表达式
https://l1ushun.github.io/2023/12/13/regexp/
作者
liu shun
发布于
2023年12月13日