正则的从入门到"入坑"(系统学习)

301 阅读11分钟

写在开头,扫一眼

讲个故事,常态化的加班,今天上面再和客户沟通的时候,客户说,你们咋这么能抗,随时打电话随时在(为什么这么能加班!),上面说因为你的项目着急,技术同事都在努力。本来到这里还正常,没想到客户说,那工资一定很高吧,上面说,“还行,市面上不算太低(diss:明明马上就到地。。),还有项目奖金(diss:这里应该是笑的,上面真能吹牛皮,可是一想到连加班。。咳咳,为什么鼻子一酸呢,好想哭)”。后来有同事咳嗽了两声,估计是听不下去了吧,上面背上电脑就走了。

你们也都一直在加班吗,别让我知道有人不是996,鸡哔你。
加班谁研究的,真是,劳人伤神。
文章7K字,系统梳理加案例解读辅佐你更好的一文学会正则。建议初中级同学能坚持把文章看下去。如果有发现错误的地方,请及时评论告诉小编,小编会及时做调整。
正文开始。

正则表达式简介

  • 正则表达式的定义:什么是正则边表达式,为什么要使用它。

    • 正则表达式(Regular Expression)是一种用于描述字符串模式的工具,允许进行复杂的字符串匹配和搜索。
    • 使用正则表达式可以简化数据验证、文本搜索和替换等任务。
    • 为什么要使用它,是个很好的问题,因为开发需要,面试更需要!
  • 正则表达式的应用场景:文本匹配、数据验证、字符串替换等。

    • ex1:文本匹配

      const phoneRegex = /\((\d{3})\) (\d{3})-(\d{4})/g;
      const text = "请拨打电话(123) 456-7890 联系我们。";
      const replacedText = text.replace(phoneRegex, "$1-$2-$3");
      
      console.log(replacrText); // 请拨打电话 123-456-7890 联系我们。
      

      开始拆解分析

      • 正则表达式

        • /:正则表达式的开始和结束标记。
        • \(\):匹配字面意义上的括号。由于括号在正则表达式中有特使意义,因此需要用\转义。
        • (\d{3}):匹配三位数字,并将其捕获为一个组。这里的\d代表数字。
        • :注意,中间有空格,匹配一个空格。
        • (\d{3}): 再次匹配并捕获3位数字,作为第二个组。
        • -:匹配字面意义上的连字符。
        • (\d{4}):匹配4位数字,并捕获为第三个组。
        • /g:表示全局搜索,即替换文本中所有匹配的部分。
      • text.replace()方法:

        • text:是待处理的字符串
        • replace(phoneRegex, "$1-$2-$3")
          • phoneRegex:是前面定义的正则表达式,用于匹配目标格式的电话号码。
          • "$1-$2-$3":这里的$1$2$3分别代表正则表达式中捕获的三个组(即区号、前缀和后缀)。通过这种方式,原来的格式(123) 456-7890被替换为123-456-7890
    • ex2:数据验证

       const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
      
       const vaildEmail = eamil => emailRegex.test(email);
       vaildEmail('pang_tt@163.com') // true
       vaildEmail('pang_tt@pang-tt.163.cn.com') // true
      

      开始拆解分析

      • 正则表达式

        • ^:表示字符串的开始
        • [\w-\.]+匹配一个或者多个字母、数字、下划线、连字符或点(即用户名称部分)
        • @: 匹配一个@符号
        • ([\w-]+\.)+:匹配一个或多个字母、数字或连字符,后跟一个点(即域名部分),整个部分可以出现多次,比如example.comwxample.co.cn
        • [\w-]{2,4}:匹配2到4个字符或数字(即顶级域名,如comorgnet等)。
        • $:表示字符串的结束。
      • vaildEmail:函数:

        • 接受一个email字符串作为参数。
        • 使用emailRegex.text(email)方法检查该字符串是否符合正则表达式的模式。
        • 返回布尔值,如果匹配成功,返回true,否则返回false。

重点:设计思路剖析

  • 输入格式的严格性:起始符^和结束符$ - 这些符号强制规定整个字符串必须完全匹配。这一设计确保整个字符串都符合邮箱格式,而不是检查部分字符串。它避免了潜在的安全漏洞或者错误输入,例如abv@domain.com text会被视为无效。

    • 用户部分[\w-\.]+:这一部分的设计考虑到了电子邮件用户名的灵活性,允许包含字母、数字、下划线、连字符和点号,也限制了其他字符避免了非法输入(如空格或特殊字符)。
    • 域名部分([\w-]+\.)+
      • 捕获组([\w-]+\.)+:该部分匹配邮箱域名,例如example.mail.,可以匹配一个或多个这样的域名段。
      • 可重复部分+:允许多层次的子域名,比如mail.example.com。这是通过+来实现的,即使邮箱域名有多个点分割的第二部分,也可以处理。
      • 顶级域名部分[\w-]{2,4}
        • [\w-]{2,4}:这个部分匹配顶级域名(TLD),例如comorg等。匹配长度为2-4的部分,确保只捕获常见的TLD形式。
        • 此设计较为灵活,可以覆盖大多数常见的顶级域名。这种设计确保避免过渡宽松的匹配,但也能适应大多数有效邮箱。
  • 为什么要这样设计

    • 精确控制输入范围:通过每个部分的细致分割,正则表达式能够有效地控制每个部分的输入。用户名、域名、顶级域名部分都有各自的限定符,确保不符合标准的输入被拒绝。
    • 安全性
      • 使用正则表达式来限定输入的格式是一种防御性编程的设计,确保不合法的输入被排除。这对于防止恶意输入或潜在的安全漏洞至关重要。
      • 尤其是在处理表单输入时,确保邮箱格式正确可以避免恶意用户注入不合法的字符串,进而可能触发数据库或者系统漏洞。
    • 灵活性和可扩展性
      • 该设计虽然比较严格,但也保留一定灵活性。正则表达式允许用户部分有较多样的字符选择,域名部分也能匹配复杂的域名结构。同时,顶级域名的长度设计可以通过简单的修改来适应新的需求,比如允许更长的顶级域名。
    • 简介性与可读性
      • 虽然正则表达式看起来复杂,但实际上它通过几个关键部分将邮箱格式进行了清晰的分解,最终生成一个简洁的验证工具。相比较于手动检查每一部分,这种方式更加直观和高效。
  • 设计模式总结

    • 分层匹配与捕获:通过分离用户名、域名、顶级域名的不同部分,使得正则表达式清晰且具有层次感。每一部分的设计都针对特定的需求进行了匹配和控制,确保只接受有效输入。
    • 灵活而有约束:在设计中既考虑了通用的输入请求(允许字母、数字、连字符、点号等),又通过边界控制、字符长度限制等约束提高了安全性和准确性。
    • 可扩展的匹配模式:域名部分设计的比较灵活,可以适应多级域名,顶级域名部分则可以根据需要修改匹配长度,从而支持更多新兴的TLD。

基本字符与符号

  • 字面字符:
    • 匹配普通字符(例如 abc 匹配字符串 "abc")

      // 示例
      const regex = /abc/;
      const text1 = "这是一个包含 abc 的字符串。";
      const text2 = "这个字符串不包含目标字符串。";
      
      // 检查 text1 是否包含 "abc"
      regex.test(text1) // true
      
      // 检查 text2 是否包含 "abc"
      regex.test(text2) // false
      

      相信有些小伙伴呢,从上文读下来会发现,为什么//^都可以称为正则的开始呢?答案来了

      /abc//^abc$/是两种不同的正则表达式,他们的区别在于匹配的范围和条件。

      • /abc/:这是一个简单的匹配模式,表示匹配任何包含“abc”的字符串。不限制“abc”出现在字符串的具体位置,只要字符串中包含这个字符序列,就可以匹配成功。
      const regex = /abc/;
      
      console.log(regex.test("abc")); // true
      console.log(regex.test("123abc456")); // true
      console.log(regex.test("def")) // false
      
      • /^abc$/:这是一个精确匹配模式,其中^表示匹配字符串的开头,$表示匹配字符串的结尾。这种模式要求字符串完全等于“abc”,不能有其他字符。
        const regex = /^abc$/;
        
        console.log(regex.test("abc")) // true
        console.log(regex.test("123abc456")); // false
        console.log(regex.test("def")) // false
      
    • 匹配数字、字母、特殊字符

      上案例,单独匹配数字、字母、特殊字符

      // 匹配数字
      const numberRegex = /\d/;
      // 匹配字母(包括大小写)
      const letterRegex = /[a-zA-Z]/;
      // 匹配特殊字符
      const specialRegex = /[!@#\$%\^\&*\)\(+=._-]/;
      /*
      这里需要打一个备注
      `[]`:用来定义字符类,即表示在这个位置可以匹配括号内任意一个字符。这个字符类中的每个字符都是单独的选项,不需要按照顺序匹配
      
      **为什么要这么设计**
      
      1、**覆盖常见特殊字符**:这个正则表达式捕捉了很多常见的特殊字符。这些字符经常在密码、标识符或者验证输入中使用,因此在表单验证、密码强度检测等应用场景中,这些字符的匹配是必要的。
      
      2、**避免歧义**:有些符号在正则表达式中具有特殊意义,例如`.`、`(`、`)`等,因此需要特别处理,确保他们按照 **字面** 意义匹配。所以需要适当的转义符号`\`来确保匹配的是符号本身。
      
      3、**灵活性与简洁性**:使用字符类`[]`的设计能够在一个字符位置同时匹配多个字符,简化了代码逻辑。如果需要匹配多个特殊字符,不需要多次调用正则或`|`(或运算符),而是将所有可能的字符组合在一起,提高了正则的简洁性和可读性。
      */
      
      const text1 = "Hello123!";
      const text2 = "OnlyText";
      const text3 = "12345";
      const text4 = "!!@@";
      
      console.log(numberRegex.test(text1)); // true(包含数字)
      console.log(letterRegex.test(text1)); // true(包含字母)
      console.log(specialRegex.test(text1)); // true(包含特殊字符)
      
      console.log(numberRegex.test(text2)); // false(不包含数字)
      console.log(letterRegex.test(text2)); // true(包含字母)
      console.log(specialRegex.test(text2)); // false(不包含特殊字符)
      
      console.log(numberRegex.test(text3)); // true(包含数字)
      console.log(letterRegex.test(text3)); // false(不包含字母)
      console.log(specialRegex.test(text3)); // false(不包含特殊字符)
      
      console.log(numberRegex.test(text4)); // false(不包含字母)
      console.log(letterRegex.test(text4)); // false(不包含字母)
      console.log(specialRegex.test(text4)); // true(包含特殊字符)
      

      继续上强度:匹配必须包含数字、字母和特殊字符的字符串

      const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#\$%\^\&*\)\(+=._-]).{8,}$/;
      
      const password1 = "Password123!";
      const password2 = "NoSpecial123";
      const password3 = "NoNumber!";
      const password4 = "short1!";
      
      console.log(passwordRegex.test(password1)); // true(包含字母、数字、特殊字符,且长度>=8)
      console.log(passwordRegex.test(password2)); // false(缺少特殊字符)
      console.log(passwordRegex.test(password3)); // false(缺少数字)
      console.log(passwordRegex.test(password4)); // false(长度不足8个字符)
      
      /*
      做个拆解
      `(?=.*[a-zA-Z])`: 确保字符串中包含至少一个字母
      `(?=.*\d)`: 确保字符串中包含至少一个数字
      `(?=.*[!@#\$%\^\&*\)\(+=._-])`: 确保字符串中包含至少一个特殊字符
      `.{8,}`: 确保字符串的最小长度为8个字符。
      */
      
  • 特殊字符
    • .(点号):匹配任意单个字符(除了换行符)

      注意:点号的行为必须引起特别的关注,需要小心对待。

      案例1:点号匹配任意字符。

      const regex = /.b./; // 匹配任意单个字符,后跟"b",再跟任意单个字符。
      
      regex.test("abc") // true,匹配"abc"
      regex.test("1b2") // true,匹配"1b2"
      regex.test("a_b") // true,匹配"a_b"
      regex.test("a\nb") // false,点号不匹配换行符
      

      案例2:多行文本中的点号行为

      const multilineText = "first line\nsecond line";
      
      // 正常的点号,不匹配换行符
      const regex = /first.*second/
      regex.test(multilineText) // false,点号不匹配换行符
      
      // 使用's'标志,点号可以匹配换行符
      const regexDotAll = /first.*second/s;
      
      regexDotAll.test(multilineText) // true,成功匹配
      
      /*
      - 使用`s`(dotall模式)标志后,点号就能匹配换行符,从而使整个表达式匹配成功
      - `.*`表示匹配任意字符,而在启用了 s 模式后,换行符也包含在其中
      */
      

      案例3:匹配带有点号的字符

      const regex = /a\.b/ // 严格匹配字母"a"后跟一个点号(字面意思)
      
      regex.test("a.b") // true
      regex.test("axb") // false
      regex.test("a2b") // false
      

      案例4:限制匹配的字符集

      const regex = /a.c/;
      const regex2 = /a.c/s;
      
      regex.test("a2c") // true,匹配"a2c"
      regex.test("abc") // true,匹配"abc"
      regex.test("a\nc") // false,点号不匹配换行符
      
      regex2.test("a2c") // true,匹配"a2c"
      regex2.test("abc") // true,匹配"abc"
      regex2.test("a\nc") // true,dotall模式下,点号匹配换行符
      
      /*
      - `a`开头
      - `[^b]`,表示除了 b 的任意字符
      - 然后是任意一个字符(点号)
      - 最后是 c
      */
      

      案例5:非贪婪匹配中的点号

      const regex = /<.*?>/;
      
      const text = "<div>Some content</div>"
      
      text.match(regex) // 输出:<div>
      
      // `.*?` 是非贪婪匹配模式,表示尽可能少地匹配字符。这里点号仍然表示任意字符,但通过`?`使其停止在第一个`>`处
      

      match 方法请移步下文 正则表达式在不同语言中的应用

  • 原字符
    • ^:匹配字符串的开始
    • $: 匹配字符串的结束

字符集与预定义字符类

  • 字符集

    • [](字符集):匹配方括号内的任意一个字符(例如[abc]匹配"a", "b" 或 "c")
    • 字符集的取反 [^]:匹配除了方括号内字符之外的任意字符 (如[^0-9]匹配非数字)
    // 案例一:匹配单个字母 [abc]
    const regexOne = /[abc]/g;
    const text = "apple ball cat dog";
    const matches = text.match(regex);
    console.log(matches) // ['a', 'b', 'a', 'c', 'a']
    const test = regexOne.test(text)
    console.log(test) // true
    
    // 案例二:匹配字母范围[a-z]
    const regexTwo = /[a-z]/g;
    const text = "Dog 123!";
    const matches = text.match(regexTwo);
    console.log(matches); // ['o', 'g']
    const test = regexTwo.test(text);
    console.log(test); // true
    
    // 案例三:匹配数字[0-9]
    const regexThr = /[0-9]/g;
    const text = "Room 101 is ready";
    const matches = text.match(regexThr);
    console.log(matches); // ['1', '0', '1']
    const test = regexThr.test(text);
    console.log(test); // true
    
    // 案例四:匹配大小写字母
    const regexFour = /[A-Za-z]/g;
    const text = "Welcome 2024!";
    const matches = text.match(regexFour);
    console.log(matches); // ['W', 'e', 'l', 'c', 'o', 'm', 'e']
    const test = regexFour.test(text)
    console.log(test); // true
    
    // 案例五:匹配特殊字符[!@#]
    const regexFive = /[!@#]/g;
    const text = "@method is post in $Api";
    const matches = text.match(regexFive);
    console.log(matches); // ['@']
    const test = regexFive.test(text);
    console.log(test) // true
    
    // 案例六:匹配非数字[^0-9]
    const regexSix = /[^0-9]/g;
    const text = "abc123";
    const matches = text.match(regexSix);
    console.log(matches); // ['a', 'b', 'c']
    const test = regexSix.test(text);
    console.log(true) // true
    
  • 预定义字符类:

    • \d:匹配数字字符(相当于[0-9]
    • \D:匹配非数字字符
    • \w: 匹配字母、数字或下划线(相当于[a-zA-Z0-9_]
    • \W: 匹配非字母、数字或下划线的字符
    • \s: 匹配空白字符(空格、制表符等)
    • \S: 匹配非空白字符
    ## 上案例。匹配复杂文本中的不同字符类型
    
    const text = "User_123 login in 10:45 AM, IP: 198.162.1.1";
    
    // 1、匹配所有数字字符
    const digits = text.match(/\d/g);
    console.log("匹配到的所有数字字符:", digits)
    // 匹配到的所有数字字符:['1', '2', '3', '1', '0', '4', '5', '1', '9', '8', '1', '6', '2', '1', '1']
    // 2、匹配所有非数字字符
    const notDigits = text.match(/\D/g);
    console.log("匹配到的所有非数字字符:", notDigits)
    // 匹配到的所有非数字字符:['U', 's', 'e', 'r', '_',' ', 'l', 'o', 'g', 'i', 'n', ' ', 'i', 'n', ' ', ':', ' ', 'A', 'M', ',', ' ', 'I', 'P', ':', ' ', '.', '.', '.']
    // 3、匹配所有数字、字母或下划线
    const wordChars = text.match(/\w/g);
    console.log("匹配到的所有数字、字母或下划线字符:", wordChars)
    // 匹配到的所有数字、字母或下划线字符:['U', 's', 'e', 'r', '_', '1', '2', '3', 'l', 'o', 'g', 'i', 'n', 'i', 'n', '1', '0', '4', '5', 'A', 'M', 'I', 'P', '1', '9', '8', '1', '6', '2', '1', '1']
    // 4、匹配所有非数字、字母或下划线
    const notWordChars = text.match(/\W/g);
    console.log("匹配到的所有非数字、字母或下划线字符:", notWordChars)
    // 匹配到的所有非数字、字母或下划线字符:[' ', ' ', ' ', ':', ' ', ',', ' ', ':', ' ', '.', '.', '.']
    // 5、匹配所有空白字符
    const spaces = text.match(/\s/g);
    console.log("匹配到的所有空白字符:", spaces)
    // 匹配到的所有空白字符:[' ', ' ', ' ', ' ', ' ', ' ']
    // 6、匹配所有非空白字符
    const notSpaces = text.match(/\S/g);
    console.log("匹配到的所有非空白字符:", notSpaces)
    // 匹配到的所有非空白字符:['U', 's', 'e', 'r', '_', '1', '2', '3', 'l', 'o', 'g', 'i', 'n', 'i', 'n', '1', '0', ':', '4', '5', 'A', 'M', ',', 'I', 'P', ':', '1', '9', '8', '.', '1', '6', '2', '.', '1', '.', '1']
    

重复与量词

  • 基本量词

    • *: 匹配0次或多次(例如a*匹配"a","aa"或无"a")
    • +: 匹配1次或多次(例如a+匹配"a", "aa")
    • ?: 匹配0次或一次(例如a?匹配"a"或无"a")
  • 限定重复次数的量词

    • {n}: 匹配恰好n次(如a{3}匹配"aaa")
    • {n,}: 匹配至少n次(如a{2,}匹配"aa", "aaa")
    • {n,m}: 匹配至少n次,至多m次(如a{2,5}匹配2至5个"a")
  • 懒惰量词

    • *?+???:使量词尽可能少匹配(例如a+?匹配最少数量的"a")
  • 代码示例

    // 基本量词
    let text1 = 'aaa';
    console.log(text1.metch(/a*/g)); // 匹配0次或多次"a",返回['aaa', '']
    console.log(text1.match(/a+/g)); // 匹配1次或多次"a",返回['aaa']
    console.log(text1.match(/a?/g)); // 匹配0次或1次"a",返回['a', 'a', 'a', '']
    
    // 限定重复次数的量词
    let text2 = "aaaaa";
    console.log(text2.match(/a{3}/g)); // 匹配恰好3次"a",返回['aaa']
    console.log(text2.match(/a{2,}/g)); // 匹配至少2次"a",返回['aaaaa']
    console.log(text2.match(/a{2,4}/g)); // 匹配至少2次,至多4次"a",返回['aaaa']
    
    // 懒惰量词
    let text3 = "aaaab";
    console.log(text3.match(/a*?b/g)); // ['aaaab']
    console.log(text3.match(/a+?b/g)); // ['aaaab']
    console.log(text3.match(/a??b/g)); // ['ab']
    

重点注意:一定以控制台为准!

对于懒惰量词来说,复杂场景的AI正则和控制台的输出完全不一样,因为对于懒惰量词来说,直面意义上是尽可能少地匹配,但是控制台的输出与直面意思确实有所不同,这里小编调研了两天,没有找到比较合适的解答,所以暂时也就先这样了。

分组与捕获

  • 括号

    • ()(捕获组):将部分正则表达式分组并捕获(如(abc)匹配"abc")
    • \1(反向引用):引用前面捕获的组(如(a)\1匹配重复字符,如"aa")
  • 非捕获组

    • (?:):定义非捕获组(如(?:abc)匹配"abc",但不捕获)
  • 代码示例

    // () 匹配组
    let text = "Hello, my name is John.";
    let regex = /(name is) (\w+)/;
    let match = text.match(regex);
    
    console.log(match[0]) // 'name is John'
    console.log(match[1]) // 'name is'
    console.log(match[2]) // 'John'
    
    
    // \1 反向引用
    let text = "He said said.";
    let regex = "/(\b\w+\b)\1/"; // \b 匹配单词边界
    let match = text.match(regex);
    
    console.log(match[0]); // "said said"
    console.log(match[1]); // "said"
    
        
    // (?:) 非捕获组
    let text = "abc123 xyz456";
    let regex = /(?:abc|xyz)(\d+)/;
    let match = text.match(regex);
    
    console.log(match[0]); // "abc123"
    console.log(match[1]); // "123"
    

    再引入一点, g 和 没有 g 的差别是蛮大的

    • 没有g:正则表达式会在字符串中查找第一个匹配,并返回一个数组,包含匹配的整个字符串以及捕获组内容。
    • g时:正则表达式会在整个字符串中查找所有匹配项,但只返回每个完整匹配的字符串,而不包含匹配组。
    • 如果要获取捕获组,就需要使用matchAll
    let text = "abc123 xyz456";
    let regex1 = /(?:abc|xyz)(\d+)/;       // 不带 g 标志
    let regex2 = /(?:abc|xyz)(\d+)/g;      // 带 g 标志
    
    let match1 = text.match(regex1);
    let match2 = text.match(regex2);
    
    console.log(match1); // ["abc123", "123"]
    console.log(match2); // ["abc123", "xyz456"]
    
    let groups = [...text.matchAll(regex2)];
    console.log(groups)
    /*
    [
        [
            "abc123",
            "123"
        ],
        [
            "xyz456",
            "456"
        ]
    ]
    */
    

位置匹配(锚点)

  • 开始与结束

    • ^:匹配字符串的开始(如^abc匹配以"abc"开头的字符串)
    • $:匹配字符串的结束(如abc$匹配以"abc"结尾的字符串)
  • 单词边界

    • \b:匹配单词边界(如\bword\b匹配独立的单词 "word")
    • \B:匹配非单词边界

逻辑或选择

  • 管道符 |:表示"或"操作(如 a|b 匹配"a"或"b")

零宽断言(Lookahead 和 Lookbehind)

  • 正向零宽断言(Lookahead)
    • (?=...):检查某个模式是否出现在当前位置的后面,但不会将此模式包含在匹配结果中。(如\d(?=px)匹配后面跟着"px"的数字)

    • 代码示例

      let text = "100px 200px 300em";
      let regex = /\d+(?=px)/g; // 匹配后面跟着一个"px"的数字
      let match = text.match(regex); // ["100", "200"]
      
  • 负向零宽断言(Negative Lookahead)
    • (?!...):检查当前位置后面不存在某个模式的情况。(如\d(?!px)匹配后面不跟"px"的数字)

    • 代码案例

      let text = "100px 200em 300px";
      let regex = /\d+(?!px)/g; // 匹配后面不跟"px"的数字
      let match = text.match(regex); // ["10", "200", "30"]
      
  • 正向零宽后发断言(Lookbehind)
    • (?<=...):检查当前位置前面是否存在某个模式,但不会将此模式包含在匹配结果中。(如(?<=\$)\d+匹配前面没有"$"的数字)

    • 代码示例

      let text = "$100 a200 ¥300";
      let regex = /(?<=\$)\d+/g; // 匹配前面有"$"的数字
      let match = text.match(regex); // ["100"]
      
  • 负向零宽后发断言(Negative Lookbehind)
    • (?<!):检查当前位置前面不存在某个模式的情况。(如(?<!\$)\d+匹配前面没有"$"的数字)

    • 代码示例

      let text = "$100 200 ¥300";
      let regex = /(?<!\$)\d+/g;
      let match = text.match(regex); // ["00", "200", "300"]
      

贪婪与惰性匹配

  • 贪婪模式:默认情况下,正则表达式是贪婪的

    • 贪婪模式会尽可能地匹配满足条件的字符,直到无法继续为止。这是正则表达式的默认行为。
  • 惰性模式:通过在量词后面添加?,使正则变为惰性匹配

    • 惰性模式会尽可能少地匹配字符。可以确保匹配的字符串是符合条件的最短子串。
  • 代码示例

    • 示例1:匹配 HTML 标签内容
    // ## 贪婪模式
    // 贪婪匹配会匹配 <p> 和 </p> 标签之间的所有内容,包含第二个 <p> 标签及其内容。
    const text = '<p>This is a paragraph.</p><p>This is another paragraph.</p>';
    const regex = /<p>.*<\/p>/g;
    const match = text.match(regex);
    console.log(match)
    // ['<p>This is a paragraph.</p><p>This is another paragraph.</p>']
    
    // ## 惰性匹配
    // 惰性匹配会分别匹配每队 <p> 和 </p> 标签之间的内容。
    const regex2 = /<p>.*?<\/p>/g;
    const match2 = text.match(regex2);
    console.log(match2)
    // [ '<p>This is a paragraph.</p>', '<p>This is another paragraph.</p>' ]
    
    • 示例2:提取网址中的协议
    // ## 贪婪模式
    // 贪婪模式会捕获所有符合条件的字符,导致输出不完整的URL
    const text = "http://example.com and https://example.com";
    const regex = /http.*:\/\//g;
    const match = text.match(regex);
    console.log(match)
    // ['http://example.com and https://']
    
    // ## 惰性匹配
    // 惰性匹配会分别匹配协议部分
    const regex2 = /http.*?:\/\//g;
    const match2 = text.match(regex2);
    console.log(match2)
    // ['http://', 'https://']
    
    • 示例3:提取括号中的内容
    // ## 贪婪模式
    // 贪婪模式会捕获从第一个 ( 到最后一个 ) 之间的所有内容
    const text = "here is (some text) and (another one).";
    const regex = /\(.*\)/g;
    const match = text.match(regex);
    console.log(match)
    // ['(some text) and (another one)']
    
    // ## 惰性匹配
    // 惰性匹配会逐一匹配每对括号内的内容
    const regex2 = /\(.*?\)/g;
    const match2 = text.match(regex2);
    console.log(match2)
    // ['(some text)', '(another one)']
    
  • 注意事项

    • **贪婪与惰性匹配的选择:**在匹配精确内容时,优先考虑使用惰性匹配,尤其在HTML或多段内容的处理上,避免贪婪匹配带来的过多捕获。
    • 性能考量:贪婪匹配由于一次性匹配所有内容,通常效率更高;惰性匹配需要逐步确认符合条件的最短字符串,可能在大文本中影响性能。

高级技巧与优化

  • 回溯问题

    • JavaScript的正则引擎也会经历回溯,特别实在处理复杂的正则表达式时,避免性能瓶颈。
    • 处理回溯问题的技巧:
      • 使用非贪婪量词:在JavaScript中,可以通过在量词后面添加 ? 来指定非贪婪匹配。.*? 会尽量少地匹配字符。
      • 明确的边界:使用 ^$ 来限制匹配的开始和结束位置,减少不必要的匹配尝试。
      • **简化子模式:**避免使用可选的模式和重复的模式。例如,将(a|b|c|d)替换为([a-d])
      • 减少交替选择:避免在正则表达式中使用过多的交替选择 |
  • 正则表达式的效率优化

    • 优化 JavaScript 中的正则表达式可以通过简化模式和调整方式来实现。
    • 效率优化技巧
      • 使用原子组: 使用原子组(?:)不创建捕获组,可以提高效率,减少回溯。
      • 避免重复计算:使用命名组(?<name>)来提高可读性,避免在复杂模式中出现重复。
      • **限制字符集:**使用具体字符集(如[0-9])而非通配符(如.),可以提高匹配速度。
      • **使用固定长度模式:**尽量使用固定长度的模式,避免可变长度的匹配,以提高性能。

正则表达式在不同语言中的应用

  • JavaScrip

    • 使用RegExp对象和方法(如.test().match().matchAll()

    1、test()

    方法的书面名称是正则表达式匹配测试方法,它是JavaScript中用于检测字符串是否与正则表达式匹配的一个方法。结果返回一个布尔值(true、false),表示是否匹配成功。

    注意:简单快速的匹配测试,它不返回具体内容匹配,只是用于测试是否匹配。


    2、match()

    match()是JavaScript字符串对象的一个方法,用来根据提供的正则表达式在字符串中搜索匹配,并返回匹配到的结果。

    基本用法

    str.match(regex);
    
    • str是要进行搜索的字符串。
    • regex是要使用的正则表达式。

    返回值

    • 如果正则表达式带有全局标志g:
      • 返回一个包含所有匹配结果的数组,如果没有匹配到任何内容,则返回null
    • 如果正则表达式没有全局标志g:
      • 返回一个包含第一个匹配项和捕获组的数据,或者没有匹配到任何内容,则返回null

    示例用法

    • 单次匹配(无全局标志g
    const text = "hello world!";
    const result = text.match(/o/);
    // 输出: ['o', index:4, input: 'hello world!', groups: undefined]
    
    /*
    返回的数组包含匹配到的`o`及其位置信息
    	索引0:匹配到的字符"o"
    	`index:4`: 表示匹配的位置是索引4
    	input: 表示原始字符串"hello world!"
    	如果有命名捕获组,则 groups 会包含对应的值,这里是 undefined
    */
    
    • 全局匹配(带g标识)
    const text = "hello world!";
    const result = text.match(/o/g)
    // ['o', 'o']
    
    /*
    正则表达式 `/o/g` ,会匹配所有出现的"o"
    返回一个数组,包含所有匹配到的字符
    */
    
    • 带有捕获组的匹配
    const text = "hello 12345!";
    const result = text.match(/(\d+)/);
    // ['12345', '12345', index: 6, input: 'hello 12345!', groups: undefined]
    
    /*
    正则表达式 `(\d+)` 中使用了捕获组来匹配数字。
    返回的数组中,除了匹配的结果 12345 外,第二个 12345 是捕获组的内容
    */
    
    • 全局匹配所有的数字
    const text = "phone: 12345, ZIP: 67890";
    const result = text.match(/\d+/g);
    // ['12345', '67890']
    
    /*
    使用 `/\d+/g` 正则表达式全局匹配所有数字序列。
    返回的数组包含所有匹配到的数字序列。
    */
    

    使用 match()matchAll()

    match()只能在全局模式下返回所有匹配项的数组,而matchAll()可以返回所有匹配项的迭代器,其中包括每个匹配项的详细信息(例如捕获组)。

    如果需要捕获组合全局匹配,可以使用matchAll()

    const text = "phone: 12345, ZIP: 67890";
    const matches = text.matchAll(/\d+/g);
    
    for(const matcj of matches)(
    	console.log(match)
    )
    
    /*
    // [ '12345', index: 7, input: 'Phone: 12345, ZIP: 67890', groups: undefined ]
    // [ '67890', index: 21, input: 'Phone: 12345, ZIP: 67890', groups: undefined ]
    */
    

    总结

    • text.match()是用来在字符串中匹配符合正则表达式的内容
    • 它可以返回单个匹配结果(无g)或所有匹配结果(带g
    • 通过结合捕获组,match()可以提取特定部分的信息。如果需要处理更复杂的全局匹配,可以使用matchAll()