死磕 - JavaScript正则表达式

感谢《JavaScript 正则表达式迷你书》作者

1. 字符匹配

1.1 两种模糊匹配

  • 横向:字符串的长度不是固定的

    示例:/ ab{2,5}c/
    在这里插入图片描述

  • 纵向:字符串的内容不是确定的

    示例:/a[123]b/
    在这里插入图片描述

1.2 字符组

  • 范围

    示例:/[1-4d-g]/
    在这里插入图片描述

  • 排除

    示例:/[\^abc]/
    在这里插入图片描述

  • 常见简写
    +\d:等价于[0-9],表示一位数字
    • \D:等价于[^0-9],表示一位非数字的任意字符

    • \w:等价于[0-9a-zA-z_],表示一位数字或大小写字母或下划线

    • \W:等价于[^0-9a-zA-z_],表示一位非单词的任意字符

    • \s:等价于[ \t\v\n\r\f],表示一位空白符

    • \S:等价于[^ \t\v\n\r\f],表示一位非空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符

    • .:等价于 [^\n\r\u2028\u2029],表示通配符。换行符、回车符、行分隔符和段分隔符
      除外

      任意字符: [\d\D]、[\w\W]、[\s\S] 和 [^] 中任何的一个
      [] :只匹配一位字符

1.3 量词

  • ?:等价于{0,1},贪婪模式

  • +:等价于{1,},

  • *:等价于{0,},

  • ??:等价于{0,1}?,惰性模式

  • +?:等价于{1,}?,

  • *?:等价于{0,}?,

    贪婪模式:会匹配尽可能的子串(默认
    惰性模式:会匹配尽可能的子串(使用 ? 将贪婪匹配模式转化为惰性匹配模式)

1.4 多选分支

  • 形式:p1|p2|p3

    • 其中 p1、p2 和 p3 是子模式
    • |(管道符)分隔,表示其中任何之一
  • 分支结构是惰性的,即当前面的匹配上了,后面的就不再尝试

    示例:/a|b|c/
    在这里插入图片描述

1.5 案例

  • 16 进制颜色

    /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/
    在这里插入图片描述

  • 匹配时间(24小时)

    \/^([01][0-9]|2[0-3]):[0-5][0-9]$\/
    在这里插入图片描述

    /^(0?\d|1\d|2[0-3]):(0?\d|[1-5]\d$)/
    在这里插入图片描述

  • 日期

    /\^[0-9]{1,4}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])$/
    在这里插入图片描述

2. 位置匹配

2.0 修饰符

  • g:全局匹配(global)

  • i:忽略大小写(insensitive)

  • m:多行模式(multi line)

    1.^ $ 变成行开头行结尾****
    2.没有该修饰符会把多行文本当成一整个字符串

2.1 如何匹配位置

  • ^ 和 $

    • ^(脱字符)匹配开头

    • $(美元符号)匹配结尾

    • 示例

      var result = "hello".replace(/^|$/g, '#');
      console.log(result);
      // => "#hello#"
      
  • \b 和 \B

    • \b 是单词边界

      1.具体就是 \w 与 \W 之间的位置
      2.包括 \w 与 ^或$ 之间的位置

    • \B 就是 非单词边界

      • 在字符串中所有位置中,扣掉 \b,剩下的都是 \B 的。
    • 示例

      var result1 = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
      console.log(result1);
      // => "[#JS#] #Lesson_01#.#mp4#"
      
      var result2 = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
      console.log(result2);
      // => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
      
  • (?=p) 和 (?!p)

    • (?=p)

      • 其中 p 是一个子模式
      • 即 p 前面的位置
      • 或者说该位置后面的字符要匹配 p
      • 正向先行断言(positive lookahead)
    • (?!p)

      • 就是 (?=p) 的反面意思
      • 表示该位置后面的字符不能匹配 p
      • 负向先行断言(negative lookahead)
    • 示例

      var result1 = "hello".replace(/(?=l)/g, '#');
      console.log(result1);
      // => "he#l#lo"
      
      var result2 = "hello".replace(/(?!l)/g, '#');
      console.log(result2);
      // => "#h#ell#o#"
      

2.2 理解位置

  • 把位置当作空字符

  • 任意多个空字符与一个空字符效果等价

  • 示例

    var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello");
    console.log(result);
    // => true
    

    TIP :把位置理解空字符,是对位置非常有效的理解方式

2.3 案例

  • 数字千分位表示法

    分析
    1. 把对应位置替换为,
    2. 当刚好有3倍数位时,不需要开头的逗号

    实现
    1. /(?=\d{3}$)/g:匹配以三个数字结尾的位置
    2. /(?=(\d{3})+$)/g:匹配以三n数字结尾的位置
    3. /(?!^)(?=(\d{3})+$)/g :不匹配开头的位置
    4. /\B(?=(\d{3})+\b)/g :支持非连续的数字字符串

     function format (num) {
        return num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g, ",").replace(/^/, "$$ ");
    };
    console.log( format(1888) );
    // => "$ 1,888.00"
    
  • 验证密码

    密码规则
    1.长度 6-12 位
    2.由数字、小写字符和大写字母组成
    3.至少包括 2 种字符

    • 分析:
      • 不考虑第三条规则:/^[0-9A-Za-z]{6,12}$/
      • 必须包含数字或字母:/(?=.*[0-9])|(?=.*[a-z])|(?=.*[A-Z])/
      • 同时包含三种字符:/(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])/
      • 实现方法一:考虑所有情况 /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9A-Za-z]{6,12}$/
        在这里插入图片描述
    • 第二种思路:至少两种字符即不能全为数字,也不能全是小写字母或全是大写字母
      • 不能全是数字:/(?!^[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}$/
        在这里插入图片描述

3. 括号的作用

3.1 分组与分支结构

  • 分组
    var regex = /(ab)+/g;
    var string = "ababa abbb ababab";
    console.log( string.match(regex) );
    // => ["abab", "ab", "ababab"]
    
  • 分支结构
    在这里插入图片描述

3.2 分组引用

  • 提取数据

    • 字符串match方法

      • 使用g标志:返回所有匹配项,但不捕获组(不具有额外属性)
      • 未使用g标志:仅返回第一个完整匹配及其相关的捕获组(具有额外属性)
      • 额外属性

        groups: 一个捕获组数组 或 undefined(如果没有定义命名捕获组)。
        index: 匹配的结果的开始位置
        input: 搜索的字符串

    • 正则实例对象的exec方法

    • RegExp构造函数的全局属性 $1-$9 来获取

  • 替换

    • 字符串的replace方法第二参数可以使用 $1-$9 指代相应的分组
  • 代码

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var string = "2017-06-12";
    console.log( string.match(regex) );
    console.log( regex.exec(string) );
    // => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
    
    regex.test(string); // 正则操作即可,例如
    //regex.exec(string);
    //string.match(regex);
    console.log(RegExp.$1); // "2017"
    console.log(RegExp.$2); // "06"
    console.log(RegExp.$3); // "12"
    
    var result = string.replace(regex, "$2/$3/$1");
    console.log(result);
    // => "06/12/2017"
    
    // 等价于如下形式
    var result2 = string.replace(regex, function () {
        return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
    });
    var result3 = string.replace(regex, function (match, year, month, day) {
        return month + "/" + day + "/" + year;
    });
    

3.3 反向引用

  • 概念

    • 在正则本身里引用之前出现的分组

    • 示例:/\d{4}(-|\/|\.)\d{2}\1\d{2}/

      \1 表示的引用之前的那个分组 (-|/|.),管它匹配到什么(比如 -),\1 都匹配那个同样的具体某个字符

  • 括号嵌套

    以左括号为准

  • 引用不存在的分组

    不报错,此时 \ 起转义作用

  • 分组后的量词

    分组最终捕获的是最后一次的匹配

    var regex = /(\d)+/;
    var string = "12345";
    console.log( string.match(regex) );
    // => ["12345", "5", index: 0, input: "12345"]
    
    var regex = /(\d)+ \1/;
    console.log( regex.test("12345 1") );
    // => false
    console.log( regex.test("12345 5") );
    // => true
    

3.4 非捕获括号

  • (?:p)(?:p1|p2|p3)
  • 既不在 API 里引用,也不在正则里反向引用

3.5 案例

  • 字符串 trim 方法模拟

    // 方法1:匹配到开头和结尾的空白符,替换为空白符
    function trim1(str) {
    	return str.replace(/^\s+ | \s+$/g, '')
    }
    
    // 方法2:匹配整个字符串,然后应用分组提取数据
    function trim2(str) {
    	// 使用了惰性匹配 *?,否则会匹配最后一个空格前的所有空格
    	return str.replace(/^\s*(.*?)\s*$/g, '$1')
    }
    

    方法1 效率更高

  • 首字母转大写

    // 思路:匹配到首字母(开头或空格后的第一个字母)后转换
    function titleize(str) {
    	return str.toLowerCase().replace(/(?:^|\s)\w/g, (match) => match.toUpperCase()) // /(?:^|\s)\w/g 实际匹配到的是空格或起始位置加一个字母
    	// replace 第二个参数为一个回调函数
    	// 其第一个参数为匹配到的字串
    	// 对每一个匹配到的子字符串执行该回调函数
    }
    console.log( titleize('my name is epeli') );
    // => "My Name Is Epeli"
    
  • 驼峰化

    // 要求:-moz-transform => MozTransform
    // 		moz  transform => MozTransform
    // 		 moz_transform => MozTransform
    // 思路:匹配到开头和结尾的空白符,替换为空白符
    function camelize(str) {
    	return str.replace(/[-_\s]+([a-zA-z])?/g, 
    		   // match => match[match.length-1].toUpperCase())
    		   // (match, p1) => p1.toUpperCase())
    		   // 以上方法无法处理到末尾的 - _
    		   // '-moz__transform - -' => 
    		   // 改进如下:
    		   (match, p1) => p1 ? p1.toUpperCase() : '')
    }
    
    console.log( camelize('-moz-transform- -') );
    // => "MozTransform"
    
  • 中划线化

    // 驼峰化的逆过程
    function dasherize (str) {
     return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
    }
    console.log( dasherize('MozTransform') );
    // => "-moz-transform"
    
  • HTML 转义与反转义

    // 将HTML特殊字符转换成等值的实体
    function escapeHTML (str) {
        var escapeChars = {
          '<' : 'lt',
          '>' : 'gt',
          '"' : 'quot',
          '&' : 'amp',
          '\'' : '#39'
        };
        return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'),
    function (match) {
            return '&' + escapeChars[match] + ';';
        });
    }
    console.log( escapeHTML('<div>Blah blah blah</div>') );
    // => "&lt;div&gt;Blah blah blah&lt;/div&gt"; 
    
    // 实体字符转换为等值的HTML。
    function unescapeHTML (str) {
        var htmlEntities = {
          nbsp: ' ',
          lt: '<',
          gt: '>',
          quot: '"',
          amp: '&',
          apos: '\''
        };
        return str.replace(/\&([^;]+);/g, function (match, key) {
            if (key in htmlEntities) {
                return htmlEntities[key];
            }
            return match;
        });
    }
    console.log( unescapeHTML('&lt;div&gt;Blah blah blah&lt;/div&gt;') );
    // => "<div>Blah blah blah</div>"
    
  • 匹配成对标签

    // 
    var regex = /<([^>]+)>[\d\D]*<\/\1>/;
    var string1 = "<title>regular expression</title>";
    var string2 = "<p>laoyao bye bye</p>";
    var string3 = "<title>wrong!</p>";
    console.log( regex.test(string1) ); // true
    console.log( regex.test(string2) ); // true
    console.log( regex.test(string3) ); // false
    

4. 回溯法原理

4.1 没有回溯的匹配

  • 正则:/ab{1,3}c/
  • 匹配:abbbc
  • 过程
    在这里插入图片描述

4.2 有回溯的匹配

  • 正则:/ab{1,3}c/
  • 匹配:abbc
  • 过程
    在这里插入图片描述
  • 分析:第5步匹配不成功,就退回到之前的状态(即第6步与第4步一样),第6步即回溯

4.3 常见回溯形式

  • 回溯本质上就是深度优先搜索算法
  • 贪婪量词
    • “吃了的,有时候需要吐出来”
    • 多个贪婪量词并存:“先下手为强”
  • 惰性量词
    • 不贪,但是也可能有回溯
    • 你不能多拿,但我可以多给你
      在这里插入图片描述
  • 分支结构
    • 惰性,存在回溯
    • 讲究先入为主,一个不行再试下一个
      在这里插入图片描述

5. 拆分

5.1 操作符优先级

在这里插入图片描述

  • 分析
    在这里插入图片描述

5.2 注意要点

  • 管道符优先级最低,适当使用括号进行分组提高优先级
    在这里插入图片描述
  • 量词连缀
    在这里插入图片描述
  • 元字符转义
    • 元字符,就是正则中有特殊含义的字符
    • 总共19个: ^、$、.、*、+、?、|、\、/、(、)、[、]、{、}、=、!、:、-

5.3 案例

  • IPv4

    /^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/
    在这里插入图片描述

6. 构建

6.1 平衡法则

  • 匹配预期的字符串
  • 不匹配非预期的字符串
  • 可读性和可维护性
  • 效率

6.2 构建前提

  • 有选择的使用,而不是走向极端
  • 不一定要设计复杂的正则:复杂的正则可以分解为几个小正则

6.3 准确性

  • 匹配浮点数

    要求:1.23、+1.23、-1.23
    0、+10、-10
    .2、+.2、-.2

    /^[+-]?(\d+\.\d+|\d+|\.\d+)$/

    在这里插入图片描述

6.4 效率

  • 使用具体型字符组来代替通配符,来消除回溯
  • 使用非捕获型分组
  • 独立出确定字符
  • 提取分支公共部分
  • 减少分支的数量,缩小它们的范围

7. 应用

7.1 字符串的四种操作

  • 验证)

    • 匹配的本质是查找
    • 判断是否能匹配 成功即验证
    • 实现:字符串方法(search/match)、正则表达式方法(test、exec)
    • test方法最常用
    var regex = /\d/;
    var string = "abc123";
    console.log( !!~string.search(regex) );
    console.log( regex.test(string) );
    console.log( !!string.match(regex) );
    console.log( !!regex.exec(string) );
    
  • 切分(split)

    • 把目标字符串分成一段一段
    var regex = /\D/;
    console.log( "2017/06/26".split(regex) );
    console.log( "2017.06.26".split(regex) );
    console.log( "2017-06-26".split(regex) );
    // => ["2017", "06", "26"]
    // => ["2017", "06", "26"]
    // => ["2017", "06", "26"]
    
  • 提取

    • 方法:match/exec/test/search/replace
    • match最常用
    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
    var string = "2017-06-26";
    console.log( string.match(regex) );
    // =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
    
    console.log( regex.exec(string) );
    // =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
    
    regex.test(string);
    console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
    // => "2017" "06" "26"
    
    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
    var string = "2017-06-26";
    string.search(regex);
    console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
    // => "2017" "06" "26"
    
    var date = [];
    string.replace(regex, function (match, year, month, day) {
        date.push(year, month, day);
    });
    console.log(date);
    // => ["2017", "06", "26"]
    
  • 替换(replace)

    replace 比你想象的更加强大

7.2 相关 API 注意要点

  • 字符串正则方法参数

    ES6 将 match()、replace()、search()和split() 四个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上

  • match 返回值

    1. g ,返回标准匹配格式(捕获组,额外属性 indexinput
    2. g ,返回所有匹配内容(不捕获组,无额外属性)
    3. 不匹配时返回 null
  • exec 比 match 更强大

    exec 方法接着上一次匹配后继续匹配
    其中正则实例 lastIndex 属性,表示下一次匹配开始的位置

  • 修饰符 g,对 exex 和 test 的影响

    字符串的四个方法,每次匹配时,都是从 0 开始的,即 lastIndex 属性始终不变

    exec、test,当正则是全局匹配时,每一次匹配完成后,都会修改 lastIndex

  • split 相关注意事项

    可以有第二个参数,表示结果数组的最大长度

    正则使用分组时,结果数组中是包含分隔符的

    var string = "html,css,javascript";
    console.log( string.split(/(,)/), 4 );
    // =>["html", ",", "css", ","]
    
  • 强大的replace

    强大之处在于第二个参数可以是字符串也可以是函数,同时可以进行替换操作

    • 第二个参数为字符串
      + 如下字符串有特殊含义
      在这里插入图片描述
    • 第二个参数为函数

    该回调函数有多个参数
    会把匹配的字串替换为回调函数的返回值
    非全局模式只替换第一个匹配到的字串
    全局模式替换所有匹配的字串

    • 示例
    // 把匹配到的字符串替换为第二个参数
    var result1 = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2"); // "5=2+3"
    var result2 = "2,3,5".replace(/(\d+)/g, "$&$&$&"); // "222,333,555"
    var result3 = "2+3=5".replace(/=/, "$&$`$&$'$&");  // "2+3=2+3=5=5"
    
    // 把匹配到的字串替换为回调函数的返回值
    "1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) {
    	console.log([match, $1, $2, index, input]);
    });
    // => ["1234", "1", "4", 0, "1234 2345 3456"]
    // => ["2345", "2", "5", 5, "1234 2345 3456"]
    // => ["3456", "3", "6", 10, "1234 2345 3456"]
    
  • 正则构造函数和字面量正则

    一般不推荐使用构造函数生成正则,而应该优先使用字面量。因为用构造函数会多写很多 \
    字面量正则创建的RegExp对象共享一个单例

  • 正则对象实例的属性

    1.global:布尔值,是否全局
    2.ignoreCase:布尔值,是否忽略大小写
    3.multiline:布尔值是否多行匹配
    4.lastIndex:默认0,下一次匹配开始位置
    5.source:字符串,正则表达式源码(不包含修饰符)

    /<>?:"{}[],.\/\\/.source
    // "<>?:\"{}[],.\\/\\\\"
    

7.3 案例

  • 正则测试器

    <!DOCTYPE html>
    <html>
    
    <head>
      <title>正则测试器</title>
      <style>
    	  section {
    	    display: flex;
    	    flex-direction: column;
    	    justify-content: space-around;
    	    height: 300px;
    	    padding: 0 200px;
    	  }
    
    	  section * {
    	    min-height: 30px;
    	  }
    
    	  #err {
    	    color: red;
    	  }
    
    	  #result {
    	    line-height: 30px;
    	  }
    
    	  .info {
    	    background: #00c5ff;
    	    padding: 2px;
    	    margin: 2px;
    	    display: inline-block;
    	  }
    	</style>
    </head>
    
    <body>
      <section>
        <div id="err"></div>
        <input id="regex" placeholder="请输入正则表达式">
        <input id="text" placeholder="请输入测试文本">
        <button id="run">测试一下</button>
        <div id="result"></div>
      </section>
    </body>
    
    <script>
      (function() {
        // 获取相应dom元素
        var regexInput = document.getElementById("regex");
        var textInput = document.getElementById("text");
        var runBtn = document.getElementById("run");
        var errBox = document.getElementById("err");
        var resultBox = document.getElementById("result");
        // 绑定点击事件
        runBtn.onclick = function() {
          // 清除错误和结果
          errBox.innerHTML = "";
          resultBox.innerHTML = "";
          // 获取正则和文本
          var text = textInput.value;
          var regex = regexInput.value;
          if (regex == "") {
            errBox.innerHTML = "请输入正则表达式";
          } else if (text == "") {
            errBox.innerHTML = "请输入测试文本";
          } else {
            regex = createRegex(regex);
            if (!regex) return;
            var result, results = [];
            // 没有修饰符g的话,会死循环
            if (regex.global) {
              while (result = regex.exec(text)) {
                results.push(result);
              }
            } else {
              results.push(regex.exec(text));
            }
            if (results[0] == null) {
              resultBox.innerHTML = "匹配到0个结果";
              return;
            }
            // 倒序是有必要的
            for (var i = results.length - 1; i >= 0; i--) {
              var result = results[i];
              var match = result[0];
              var prefix = text.substr(0, result.index);
              var suffix = text.substr(result.index + match.length);
              text = prefix +
                '<span class="info">' +
                match +
                '</span>' +
                suffix;
            }
            resultBox.innerHTML = "匹配到" + results.length + "个结果:<br>" + text;
          }
        };
        // 生成正则表达式,核心函数
        function createRegex(regex) {
          try {
            if (regex[0] == "/") {
              regex = regex.split("/");
              regex.shift();
              var flags = regex.pop();
              regex = regex.join("/");
              regex = new RegExp(regex, flags);
            } else {
              regex = new RegExp(regex, "g");
            }
            return regex;
          } catch (e) {
            errBox.innerHTML = "无效的正则表达式";
            return false;
          }
        }
      })();
    </script>
    
    </html>
    
    • 效果图

      在这里插入图片描述

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值