《JS 正则迷你书》笔记2 - 位置匹配

Posted by cody1991 on July 21, 2020

from https://github.com/qdlaoyao/js-regex-mini-book

正则要么匹配字符,要么匹配位置

位置匹配

什么是位置?

位置 (锚)就是每个字符之间的位置 _h_e_l_l_o_ 比如这个字符中的 _ 代表的就是一个个的位置

ES5 中一共有 6 个 锚

^ $ \b \B (?=p) (?!p),把它们弄成一个正则

/^$\b\B(?=p)(?!p)/

https://regexper.com/#%2F%5E%24%5Cb%5CB%28%3F%3Dp%29%28%3F!p%29%2F

^ $

^ 匹配开头,在多行里面匹配多个行开头

$ 匹配结尾,在多行里面匹配多个行结尾

比如我们把字符串开头结尾替换成 #,这里也可以看到位置是可以替换成字符的

1
2
3
4
5
6
7
`hello`.replace(/^|$/g, "#");
// "#hello#"

"I\nlove\njavascript".replace(/^|$/gm, "#");
// "#I#
// #love#
// #javascript#"

\b \B

\b 代表的是单词的边界 (其实就是 \w$ ^ \W 的边界),简单来说就是单词和单词之间(包括开始和结束)的位置

我们考察下下面的情况

1
2
"[JS] Lesson_01.mp4".replace(/\b/g, "#");
// [#JS#] #Lesson_01#.#mp4#

\B\b 的反面,所以上面的例子其他剩下的位置都是 \B,就是 \w\w, \W\W,\W$ ^简单来说就是单词和单词,非单词和非单词(包括开始和结束)之间的位置

1
2
"[JS] Lesson_01.mp4".replace(/\B/g, "#");
// #[J#S]# L#e#s#s#o#n#_#0#1.m#p#4

(?=p) (?!p)

(?=p) 代表的是 p 模式前面的 位置,或者说 该位置 后面需要匹配 p,正向先行断言

比如 (?=l) 表示 l 字符前面的位置

1
2
"hello".replace(/(?=l)/g, "#");
// he#l#lo

(?!p) 代表的是非 p 模式前面的位置,或者说这个位置后面不是 p,负向先行断言

1
2
"hello".replace(/(?!l)/g, "#");
// #h#ell#o#

位置的特性

对于位置我们可以理解成空字符

比如 hello 等价于 "hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "" + "o" + "";

也等价于 "hello" == "" + "" + "hello"

所以我们把 /^hello$/ 写成了 /^^^hello$$$/ 完全没有问题

设置写成了 /(?=he)^^he(?=\w)llo$\b\b$/ 也是没有问题

案例分析

不匹配任何东西的正则

/.^/ ,匹配某个在开始前的字符,不存在

数字的千位分隔符表示法

先匹配一个,使用 /(?=\d{3}$)/,代表查找某个位置,它后面是 \d{3}$ 三个数组跟着结束符

1
2
"12345678".replace(/(?=\d{3}$)/, ",");
// "12345,678"

https://regexper.com/#%2F%28%3F%3D%5Cd%7B3%7D%24%29%2F

接下来匹配所有的位置,要求 \d{3} 至少出现一次,所以使用 +

这里表示的是 可以是某个位置,它后面跟着 3 个数字和结束符,6 个数字和结束符,9 个数字和结束符 等等的情况 (一开始自己理解错了)

1
2
"12345678".replace(/(?=(\d{3})+$)/g, ",");
// "12,345,678"

https://regexper.com/#%2F%28%3F%3D%28%5Cd%7B3%7D%29%2B%24%29%2Fg

不过发现下面的出错情况,所以继续优化

1
2
3
4
5
"123456789".replace(/(?=(\d{3})+$)/g, ",");
// ",123,456,789"

"123456789 123456789".replace(/(?=(\d{3})+$)/g, ",");
// 123456789 ,123,456,789"

下面是优化后的,表示要替换成 , 的位置,这个位置前面的位置不能是 非单词之间的边界(因为 ^\w 之间是 \b,这个位置是不能放 , 替换的,所以我们用 \B)。最后也替换成了 \b 代表从 单词边界 的位置倒叙算起

1
2
3
4
5
"123456789".replace(/\B(?=(\d{3})+\b)/g, ",");
// ",123,456,789"

"123456789 123456789".replace(/\B(?=(\d{3})+\b)/g, ",");
// "123,456,789 123,456,789"

https://regexper.com/#%2F%5CB%28%3F%3D%28%5Cd%7B3%7D%29%2B%5Cb%29%2Fg

格式化

千分符表示法一个常见的应用就是货币格式化

1888 格式化成:$ 1888.00

1
2
3
4
5
6
7
8
9
function format(num) {
  return num
    .toFixed(2)
    .replace(/\B(?=(\d{3})+\b)/g, ",")
    .replace(/^/, "$$ ");
}

format(1888);
// "$ 1,888.00"

验证密码问题

密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符

简化

不考虑至少两个字符,我们简单写出来

/^[0-9a-zA-Z]{6,12}$/

如果要求必须有数字,我们可以写出下面的正则

/(?=.*[0-9])/

所以整个正则变成了 /(?=.*[0-9])^[0-9a-zA-Z]{6,12}$/

那要求同时包含数字或者小写字母,那就变成了

/(?=.*[0-9])(?=.*[a-z])^[0-9a-zA-Z]{6,12}$/

解答

  • 同时包含数字和小写
  • 同时包含数字和大写
  • 同时包含小写和大写

最终的解答就是

/((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[A-Z])(?=.*[a-z]))^[0-9a-zA-Z]{6,12}$/

https://regexper.com/#%2F%28%28%3F%3D.*%5B0-9%5D%29%28%3F%3D.*%5Ba-z%5D%29%7C%28%3F%3D.*%5B0-9%5D%29%28%3F%3D.*%5BA-Z%5D%29%7C%28%3F%3D.*%5BA-Z%5D%29%28%3F%3D.*%5Ba-z%5D%29%29%5E%5B0-9a-zA-Z%5D%7B6%2C12%7D%24%2F

另外一种方法

必须有两个字符,那就是后面不能全是一种字符,所以我们可以写出另外一种解法

首先不能全是数字的写法是

/(?![0-9]{6,12}$)^[0-9a-zA-Z]{6,12}$/

那三个都写上的话就是

/(?![0-9]{6,12}$)(?![a-z]{6,12}$)(?![A-Z]{6,12}$)^[0-9a-zA-Z]{6,12}$/

https://regexper.com/#%2F%28%3F!%5B0-9%5D%7B6%2C12%7D%24%29%28%3F!%5Ba-z%5D%7B6%2C12%7D%24%29%28%3F!%5BA-Z%5D%7B6%2C12%7D%24%29%5E%5B0-9a-zA-Z%5D%7B6%2C12%7D%24%2F