前言

距离上一次更新已经是五个月以前了,时光荏苒,已经到2024年了。由于自己会的是Python,所以不太清楚自己能否找到合适的工作,思来想去还是准备学习一下前端的知识,希望能在当前艰难的大环境下生存下来。

正则表达式其实就是字符串的一个匹配规则,能方便匹配一系列符合规则的字符串,让开发效率变高,这次也是趁着学完了来做个笔记记录一下,毕竟如果后续不常用还是容易忘记。以下部分均以Javascript作为示范语言,Python语言实际也大差不差。

以下是一个匹配24小时时间的例子:

1
2
3
4
5
const reg4 = /^(([01][0-9])|([2][0123])):[0-5][0-9]$/;
console.log(reg4.test("23:59")); // true
console.log(reg4.test("00:00")); // true
console.log(reg4.test("24:00")); // false
console.log(reg4.test("23:60")); // false

其中/^(([01][0-9])|([2][0123])):[0-5][0-9]$/即为一个正则表达式。由//包裹,在第二个/之后可添加修饰符来约束正则中执行的某些细节,如//g,表示全局匹配。

常见的字符串匹配函数

在学习正则表达式之前,我们需要了解js中一些常用的字符串匹配方法,这样才能方便后续学习时的测试。

本段落使用的测试字符串为:"前端开发,必须掌握正则表达式,前端就业前景好,前端工资高",固定的正则表达式为:/前端/g,即在字符串中匹配前端这两个字符,g的含义为全局匹配,这里不必深究。

test

正则表达式的一个方法(并非字符串的方法,后续学习记得区分一下)。用来检测字符串是否符合正则表达式的规则,如果符合返回true,否则返回false。

1
2
3
4
const reg = /前端/g;
const str = "前端开发,必须掌握正则表达式,前端就业前景好,前端工资高";
console.log(reg.test("前端开发")); // true
console.log(reg.test("Java开发")); // false

这边有一个注意点。

test 方法在被连续调用时会从上一次匹配的下一个位置开始查找,而不是每次都从第一个位置开始匹配,如果不注意可能会出现反常的结果,如下的一个例子:

1
2
3
4
reg.test('前端开发,用js')
// 能匹配到可以返回true,否则返回false
console.log(reg.test("前端开发")); // false
console.log(reg.test("Java开发")); // false

这是因为test方法在被连续调用时会从上一次匹配的下一个位置开始查找,当reg.test('前端开发,用js')执行后,第二次调用 test 方法时将从字符串的第3个字符开始匹配,而不是从第 0 个字符开始,因此出现了反常的错误。

exec

是正则表达式的一个方法,作用是查找符合规则的字符串。

1
2
3
4
const reg = /前端/g;
const str = "前端开发,必须掌握正则表达式,前端就业前景好,前端工资高";
const reg = /前端/g;
console.log(reg.exec("前端开发"));

输出为:[ '前端', index: 0, input: '前端开发', groups: undefined ]

返回值为数组,如果匹配失败则返回NULL,匹配成功的返回值有三个参数:

  • 第一项为匹配到的字符串 ‘前端’
  • 第二项为匹配到的字符串的索引 index:0
  • 第三项为原字符串 input: ‘前端开发’

需要注意的是exec方法也会从上一次匹配的下一个位置开始查找,这点和text一样。

replace

是字符串的方法(不是正则表达式的方法),替换字符串中符合规则的字符串。

1
2
3
4
const reg = /前端/g;
const str = "前端开发,必须掌握正则表达式,前端就业前景好,前端工资高";
str.replace(reg, "后端");
console.log(str.replace(reg, "后端"));

输出为后端开发,必须掌握正则表达式,后端就业前景好,后端工资高

match

是字符串的方法,查找符合规则的字符串

1
2
3
const reg = /前端/g;
const str = "前端开发,必须掌握正则表达式,前端就业前景好,前端工资高"
console.log(str.match(reg));

输出为:[ '前端', '前端', '前端' ]

匹配成功返回一个数组,匹配失败返回None。

更多关注返回值是否为None,与test的区别在于方法调用对象不同,且match方法明显比test安全,也更符合一般代码逻辑。

修饰符

放在第二个/之后,作用是约束正则中执行的某些细节,常见的有三个取值。

sign function
i 执行对大小写不敏感的匹配
g 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)
m 执行多行匹配(应用场景较少)
  1. 大小写脱敏

    1
    2
    3
    4
    5
    const reg1 = /a/i;
    const reg2 = /a/;
    const str = "A";
    console.log(reg1.test(str)); // true
    console.log(reg2.test(str)); // false
  2. 全局匹配

    1
    2
    3
    4
    5
    const reg1 = /前端/;
    const reg2 = /前端/g;
    const str = "前端开发,必须掌握正则表达式,前端就业前景好,前端工资高";
    console.log(str.match(reg1));
    console.log(str.match(reg2));
    • reg1:[ '前端', index: 0, input: '前端开发,必须掌握正则表达式,前端就业前景好,前端工资高', groups: undefined ]
    • reg2:[ '前端', '前端', '前端' ]
  3. 使用多个修饰符

    1
    2
    3
    const reg3 = /Java/gi;
    const message = "学Java,java工资高,java前景好";
    console.log(message.replace(reg3, "Python"));

    输出为:学Python,Python工资高,Python前景好

元字符

即具有特殊含义的字符,给与正则表达式更加多样灵活的组合。

边界符

即为规定表达式的边界部分,常见有单词边界、字符串边界、精确匹配三种情况。

  1. 单词边界

    在字符串头尾加上\b 构成单词的部分,如下面这个例子:

    1
    2
    3
    4
    5
    6
    7
    // 加上\b 构成单词的部分
    const reg1 = /cat/g;
    const reg2 = /\bcat\b/g;
    const message = "The cat scattered his food all over the room.";

    console.log(message.replace(reg1, "dog"));
    console.log(message.replace(reg2, "dog"));
    • reg1:The dog sdogtered his food all over the room.
    • reg2:The dog scattered his food all over the room.

    可以看到由于reg1并未添加单词边界,导致scattered中的“cat”部分也会被匹配上,出现了大相径庭的结果。

  2. 字符串边界

    使用^作为字符串的开头,$作为字符串的结尾。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const reg5 = /^a/i;
    console.log(reg5.test("a")); // true
    console.log(reg5.test("abc")); // true
    console.log(reg5.test("bcd")); // false

    const reg6 = /c$/i;
    console.log(reg6.test("c")); // true
    console.log(reg6.test("cb")); // false
    console.log(reg6.test("C")); // true
  3. 精确匹配

    如果同时在开头与结尾加上^ $在一块,那么就编程了精确匹配,即字符串必须完全满足正则的全部要求。

    1
    2
    3
    const reg7 = /^a$/;
    console.log(reg7.test("a")); // true
    console.log(reg7.test("aaa")); // false

量词

顾名思义,即规定字符出现的频率。

sign function
* 表示0次或者多次
+ 表示一次或者多次
? 表示零次与一次
{n} 只能有n次
{n,m} 表示从n到m次(包括n与m)
    1
    2
    3
    const reg8 = /^a*$/i
    console.log(reg8.test("aaa")); // true
    console.log(reg8.test("")); // true
    1
    2
    3
    const reg8 = /^a+$/i
    console.log(reg8.test("aaa")); // true
    console.log(reg8.test("")); // false
  1. ?

    1
    2
    3
    const reg_1 = /^a?$/i
    console.log(reg_1.test("")); //true
    console.log(reg_1.test("aaa")); //false
  2. {n}

    1
    2
    3
    4
    const reg9 = /^a{3}$/
    console.log(reg9.test("a")); //false
    console.log(reg9.test("aaa")); // true
    console.log(reg9.test("")); // false
  3. {n,m}

    1
    2
    3
    4
    const reg9 = /^a{1,3}$/
    console.log(reg9.test("a")); //true
    console.log(reg9.test("aaa")); // true
    console.log(reg9.test("")); // false

字符类

日常生活中经常出现一些字符串不同但是等效的情况,如Pythonpythoncolorcolour,广义来说即为多个字符都符合匹配要求,可以出现替换的情况。

如:/[abc]/,意味着我要匹配的字符串其中有a或b或c;/[Pp]thon/为匹配Pythonpython

常见的字符类情况为:

sign function
[a-z] 表示a到z,26个小写的英文字母
[A-Z] 表示A到Z,26个大写的英文字母
[0-9] 表示0-9十个数字
. 匹配除了换行符(\n与\r)外的任意一个字符
[^] 表示取反,与[]合并使用,注意要写在[]里面

以下是一些例子:

  1. 密码匹配(6-16位字母、数字或者下划线组成)

    1
    2
    3
    4
    const reg = /[a-zA-Z0-9_]{6,16}/;
    console.log(reg.test("123456")); // true
    console.log(reg.test("1234567")); // true
    console.log(reg.test("12345")); // false
  2. 替换Python为Java

    1
    2
    3
    4
    5
    const reg = /[Pp]ython/;
    const str1 = "人生苦短,我学Python"
    const str2 = "人生苦短,我学Python"
    console.log(str1.replace(reg,"Java"));
    console.log(str2.replace(reg,"Java"));

    输出都为:人生苦短,我学Java

  3. 匹配爱后面没有你的数据

    1
    2
    3
    4
    const reg = /爱[^你]/;
    console.log(reg.test("不爱你")); // false
    console.log(reg.test("爱你一万年")); // false
    console.log(reg.test("我爱我")); // true
  4. .匹配的例子

    1
    2
    3
    4
    const reg14 = /./;
    console.log(reg14.test("")); //false
    console.log(reg14.test("\n")) // false
    console.log(reg14.test("\r")) // false

预定义

对于一些既定的字符类来说,还有简化的写法,这些简化的写法就是预定义。

sign Same meaning function
\d [0-9] 小写数字0-9
\D [^0-9] 除了数字的部分
\w [a-zA-Z0-9_] 匹配任意的字母、数字、下划线
\W [^a-zA-Z0-9_] 匹配任意非字母、数字、下划线
\s [\t\r\n\v\f] 匹配空格(包含换行、空格、制表符)
\S [^\t\r\n\v\f] 匹配非空格

大写其实就是小写的取反效果

分组与选择

分组

分组就是将数据的不同部分分开以适应不同的规则,使用(),每一个小括号就是一个分组。

一个经典的案例为:将日期2020-10-10格式转化为10/10/2020的格式。

此时我们就需要将匹配成功的年、月、日部分单独拿出来使用,这时候就可以使用分组的方法,匹配代码如下:

1
2
3
const reg = /^(\d{4})-(\d{2})-(\d{2})$/
const str = "2020-10-10"
console.log(str.match(reg))

输出为:[ '2020-10-10', '2020', '10', '10', index: 0, input: '2020-10-10', groups: undefined ]

可以看到实际上match按照正则表达式分组的想法(一个小括号一组)将匹配结果分为了三组(年、月、日),其中将整个日期作为一个group匹配起来,我们称其为group0。

2020-10-10 group0

2020 group1

10 group2

10 group3

在replace中支持使用$n来获取其中的分组数据

1
2
3
const reg = /^(\d{4})-(\d{2})-(\d{2})$/
const str = "2020-10-10"
console.log(str.replace(reg,"$1年$2月$3日"))

输出为:2020年10月10日

分支

使用|来表示或者,如:

1
2
3
const reg17 = /^good|nice$/
console.log(reg17.test("good")) // true
console.log(reg17.test("nice")) // true

[\bnice\b\bgood\b]/[(nice)(good)]/效果相同。

分支与可选[]效果实际相同,但可选一般用于两者之间,而选择一般用于多者之间

非分组匹配

在正则表达式中用于将多个表达式组合成一个单元,但是与普通的括号不同,它不会保存匹配的内容用于后续的引用。这意味着你可以使用非捕获组来应用量词(如 +*)到整个组合的模式上,而不会捕获该组的匹配结果。

我的理解是帮助你找出成对或组合的模式出现的次数,但不需要具体的捕获内容。

以下是一个捕获HTML标签的例子:

1
2
3
const text = "This is a <b>bold<b> word and this is an <i>italic</i> word."
const reg = /(?:<.>)/g
console.log(text.match(reg))

输出为[ '<b>', '<b>', '<i>' ]

个人感觉这个东西没什么用

一些零碎知识

  1. 可选字符

    有时,我们可能想要匹配一个单词的不同写法,比如color和colour,或者honor与honour

    1
    2
    3
    const reg19 = /colou?r/
    console.log(reg19.test("color")) // true
    console.log(reg19.test("colour")) // true

    这个正则表达式中?代表u可有可无。

  2. 重复区间

    算是一些细节的补充,因为正则表达式默认是贪婪模式,即尽可能的匹配更多字符,于是会出现以下情况:

    {M,} - 表示至少M次

    {,N} - 表示至多N次

    {M} - 表示恰好M次

    而要使用非贪婪模式,我们要在表达式后面加上?号。

案例

密码匹配

密码匹配(6-16位字母、数字或者下划线组成)

1
2
3
4
5
const reg = /[a-zA-Z0-9_]{6,16}/;
const reg2 = /^\w{6,16}$/;
console.log(reg2.test("123456")); // true
console.log(reg2.test("1234567")); // true
console.log(reg2.test("12345")); // false

匹配16进制颜色值

#ffffff#fff

可以看出数字的个数为6个或者3个,可以使用分支结构,另外,每个字符的取值范围在0-9与a-f(包括大写)之间,这可以使用选择[]。

1
2
3
4
const reg3 = /^#([0-9a-fA-F]{3}|[0-9A-Fa-f]{6})$/;
console.log(reg3.test("#ccc")); // true
console.log(reg3.test("#cccccc")); // true
console.log(reg3.test("#cc")); // false

匹配24小时时间

这个分为小时与分钟两部分进行分析。

小时部分:

  1. 当第一个数字为0或1的时候,第二个数字为0-9
  2. 当第一个数字为2时,第二个数字为0-3

分钟部分:

  1. 第一个数字为0-5
  2. 第二个数字为0-9
1
2
3
4
5
const reg4 = /^(([01][0-9])|([2][0123])):[0-5][0-9]$/;
console.log(reg4.test("23:59")); // true
console.log(reg4.test("00:00")); // true
console.log(reg4.test("24:00")); // false
console.log(reg4.test("23:60")); // false

手机号脱敏

将11位手机号中间4位替换为*,例如13812341234替换为*138****1234*,这里就可以把头三个数与尾四个数各分为一组,后续就可以使用$获取数据。

1
2
3
4
const str = "13812341234";
console.log(str.substring(0, 3) + "****" + str.substring(7,11));
const reg6 = /^(1[3-9]\d{1})\d{4}(\d{4})$/
console.log(str.replace(reg6, "$1****$2"));

匹配电话号码

假设电话号码可以有下列两种方式:

  • 0XX-XXXXXXX,例如020-8810456;

  • 0XXXXXXXXX,例如0208810456

020 代表区号,8810456是电话号码,区号第一个数字必须是0,电话号码的第一个数字必须大于等于1。

1
2
3
4
5
const reg9 = /^0[0-9]{2}-?[1-9]\d{6}$/
console.log(reg9.test("020-8810456")); // true
console.log(reg9.test("0208810456")); // true
console.log(reg9.test("020-88104XX")); // false
console.log(reg9.test("3208810456")); // false

总结

正则表达式功能强大,虽然在平时大部分的时候都可以直接靠搜索引擎解决需求(如:手机号,身份证等),VsCode中也有名为any-rule的正则插件,几乎涵盖了开发中大部分的需求,但是如果遇到一些特殊情况要求重新编写一个正则表达匹配,那就需要自己动手实现了,比如匹配特定的网站链接是否合法。

最后找到两个非常好用的网站:

  1. 编程胶囊:编程胶囊-打造学习编程的最好系统 (codejiaonang.com)
  2. 正则可视化:Regexper