阅读 57

一篇文章学会正则表达式

正则相关

创建方式

  1. 字面量的方式:let reg = /xxx/
  2. 构造函数:let reg = new RegExp('xxxx','img')

区别:

  • 字面量里的转义符\不需要转义,而构造函数中传入的是字符串,所以在字符串中要表示\,需要再转义一次,例如:\d在构造函数创建方式中需要写为new RegExp('\\d','g')
  • 字面量的创建方式,正则表达式中不能使用变量,而构造函数由于是传入字符串,所以可以使用字符串和变量拼接的方式,或者使用模板字符串的方式,都可以插入变量。

模式

  • i:表示忽略大小写匹配。
  • m:表示多行匹配。
  • g:全局匹配。

例如:

//字面量形式
var reg = /xxx/img
//构造函数形式
var reg = new RegExp('xxx','img');
复制代码

作用

  • 匹配,匹配字符串是否符合正则表达式规则。
  • 捕获,获取符合正则表达式的相关内容。

元字符 -- 具有特殊含义的字符

量词元字符(限定符)

  • * 限定符,匹配前面的表达式 0 到多次,相当于{0,}

  • + 限定符,匹配前面的表达式 1 到多次,相当于{1,}

  • ? 限定符,匹配前面的表达式 0 到 1次,相当于{0,1},如果紧跟在另一个限定符(*,+,?,{n},{n,},{n,m})后面的话,则是表示将匹配模式转换为非贪婪模式。默认的是贪婪模式,会尽可能多的匹配字符,非贪婪模式会尽可能少的匹配,比如

    var str = 'aaab';
    var result1 = /a+/.exec(str); // result1[0] 为 aaa,默认是贪婪模式
    var result2 = /a+?/.exec(str); // result2[0] 为 a,通过?转换为非贪婪模式
    复制代码
  • {n}限定符,匹配前面表达式只能出现n次

  • {n,}限定符,匹配前面表达式n到多次

  • {n,m}限定符,匹配前面表达式n到m次

其他元字符

  • \ 转义字符或者8进制数标识。
  • ^ 匹配字符串开头,如果在[]中直接使用,则表示否定的含义,例如:[^abc]表示不是abc的任意一个字符。
  • $匹配字符串的结尾。
  • .匹配除了换行以外的任意字符。
  • \b 匹配单词边界,也就是单词之间的空格位置 ,例如:er\b 可以匹配 nerver 匹配不了 verb。
  • \B 匹配非单词边界。
  • \d 匹配一个数字字符,相当于[0-9]
  • \D 匹配一个非数字字符,相当于[^0-9]
  • \w 匹配一个数字,字母,下划线,相当于[0-9a-zA-Z_]
  • \W 匹配一个非数字字母下划线。
  • \s 匹配一个空白字符,包括空格和非打印字符,相当于[ \f\r\n\t\v]
  • \S 匹配一个非空白字符。
  • 非打印字符:\f--换页符,\r--回车符,\n--换行符,\t--制表符,\v--垂直制表符。
  • \n 匹配8进制n表示的字符。
  • \cx 匹配由x指明的控制字符,x只能是a-z,A-Z,比如X,那就表示 Control+X。
  • \xn 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字。
  • \un 匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符,例如中文的范围是\u4E00 ~ \u9FA5
  • x|y 匹配x或y。
  • [xyz] 字符集合,可以是范围,例如 a-z,A-Z等等。
  • [^xyz],非字符集合,可以指定某一个范围,如0-9,a-z等
  • (xxx) 匹配分组,匹配到的结果会存储下来,在字符串的match方法和正则的exec方法中可以使用1,1,2....来表示匹配到的值。
  • (?:xxx) 也会匹配,但是分组匹配的结果不会保存下来。只匹配不分组。
  • (?=xxx),正先行断言,exp1(?=exp2)表示匹配exp1后紧跟满足exp2的,整个就匹配。
  • (?!xxx) 负先行断言,exp1(?!exp2) 表示匹配exp1后只要不匹配exp2的,整个就匹配。
  • (?<=xxx) 正后发断言,(?<=exp2)exp1表示exp1前面是满足exp2的,整个就匹配
  • (?<!xxx) 负后发断言,(?<!exp2)exp1 表示匹配exp1前只要不匹配exp2,整个匹配。

使用注意:

1,^$使用

  • ^xxx:匹配以xxx开头。
  • xxx$:匹配以xxx结尾。
  • ^xxx$:从头到尾只能匹配xxx。

2,|(或)的使用

| 在使用的时候通常不单独写,一般会加上(),因为|优先级低,正则在优先级相同的时候,会尝试不同的组合尽可能去匹配字符串,这时候往往会出现一些意想不到的问题,导致结果不是我们想要的。例如:

let reg = /^18|29$/;
//可能我们本意是匹配18或29,但是解析的时候,js引擎会进行多种尝试:
//1,以18开头或29结尾的
//2,以1开头,中间是8或2,最后以9结尾的
//....,这与我们想要的结果不同,所以我们需要使用()来改变优先级
let reg = /^(18|29)$/;
//使用()之后,小括号里面就不会受影响了,| 优先级低,表示左边整体或右边整体
复制代码

3,[]的使用

1,[]中的字符通常表示字符本身的含义(除了以下特殊含义的字符),其中的括号也会表示括号的这个字符的含义。

  • ^:表示非的含义。
  • -:表示一个范围。
  • \d\D\w\W\、\s\S等等这些 \开头的特殊元字符。

2,整体只匹配一个字符

例如:[18-29]不会表示18到29,会表示1或8到2或9中的一个,8到2是非法的范围,所以这个正则会报错。

4,?的使用

  • 前面是非量词限定符时,表示量词 0 - 1次。
  • 前面是量词限定符时,表示取消前面量词匹配的贪婪模式,会让前面的内容匹配尽可能少的次数。
  • (?:exp)表示只匹配,不捕获。
  • xxx(?=exp):正先行断言。
  • xxx(?!exp):负先行断言。
  • (?<=exp)xxx:正后发断言。
  • (?<!exp)xxx:负后发断言。

5,\n的使用

  • \n前有至少n个分组时,\n指的是前面第n个分组匹配的内容。

    let str = "good good study,day day up";
    let reg = /\b([a-z]+)\b +\1/g;//注意\1前有空格,且是1到多次
    reg.exec(str);//匹配的子字符串:good good
    reg.exec(str);//匹配的子字符串:day day
    复制代码
  • \n前没有达不到n个分组且n是 0 - 7 的数字,则\n表示的是一个八进制的数字 8。

匹配优先级

  1. \:转义符
  2. (),(?:),(?=),[]..
  3. *,+,?,{n},{n,},{n,m}
  4. ^,$,\任意字符,任意字符
  5. |:或,例如m|food会匹配m或food,一般要搭配圆括号使用。

JS中的相关方法

RegExp实例属性

lastIndex:表示正则表达式下一次匹配从字符串开始时的索引。第一次匹配时为 0。

  • 非全局模式下:testexec等方法每次都从字符串索引为 0 处开始进行匹配,而且每一次匹配并不会修改lastIndex的值。

  • 全局模式下:testexec每一次匹配后都会自动修改lastIndex的值,匹配不成功,会将lastIndex的值重新置为0,再次调用就是又重新从头开始进行匹配。

这也是为什么exec等方法,为什么在非全局模式下,每次返回的结果都一样,因为都是从头开始匹配。

RegExp实例方法

  • RegExp.prototype.test(str),判断字符串是否匹配正则表达式。

    • 非全局模式下,每次匹配成功后,不会修改lastIndex的值,所以多次调用返回的结果都一样。
    • 全局模式下,每次匹配成功后会修改lastIndex的值,所以多次调用,返回的结果可能不一样。
  • RegExp.prototype.exec(str),用正则表达式模式在字符串中查找,并返回该查找结果的第一个值(数组),如果匹配失败,返回null。这个返回结果取决于正则表达式是否是全局模式

    • 普通模式:每次都从开始位置开始匹配,返回匹配结果。没有匹配到结果返回null

      var str = "aaa123asda123asd123asd123asd";
      var reg = /[0-9]+/;
      var result = reg.exec(str);
      // ["123", index: 3, input: "aaa123asda123asd123asd123asd", groups: undefined]
      // result[0],匹配到的内容
      // result[1]......result[n]:子表达式匹配的内容 
      // result["index"],匹配内容的索引
      // result["input"],源字符串
      复制代码
    • 全局模式:每次调用都会从上一次匹配后的lastIndex位置处开始往后进行匹配。如果上一次没有匹配到结果,会将lastIndex重置为 0,这时又会从字符串开始位置0处开始匹配。

      可以用循环来遍历匹配结果。

      var str = "aaa123asda456asd7asd8asd";
      var reg = /[0-9]+/g;
      var result;
      while((result = reg.exec(str)) !== null){
          // result[0],匹配到的内容 
          // result[1]......result[n]:子表达式匹配的内容 
      	// result["index"],匹配内容的索引
      	// result["input"],源字符串
          console.log(result[0]);
      }
      复制代码

字符串中的相关方法

  • String.prototype.search(reg) 返回第一次匹配正则的表达式的索引,没有匹配到返回-1。不区分是否为全局模式。

  • String.prototype.match(reg)使用正则表达式模式对字符串执行查找,并将包含查找的结果作为数组返回。这个方法的行为很大程度上依赖正则表达式是否为全局模式,即是否包含g

    1. 不包含全局标识g,那么match方法只会在字符串中进行一次匹配,匹配不到任何结果,返回null,如果匹配到则返回匹配的结果数组:

      • 数组的第0个元素为匹配到的内容

      • 数组的第1 ~ n个元素为匹配到的组中的子匹配项,即正则表达式中(xxx)匹配到的内容

      • 数组的index属性,值为匹配的内容在字符串中的索引

      • 数组的input属性,值为源字符串

    2. 如果包含全局标识gmatch方法将对字符串进行全局检索,找到所有匹配项,如果没有找到,返回null。找到一个或多个匹配的话,返回一个数组,该数组元素为所有匹配的内容,没有indexinput属性,不提供与子表达式匹配的文本的信息,也不提供每个匹配的子串在字符串中的位置。如果需要这些信息,使用正则对象的exec方法。

  • String.prototype.split(seq,length) 按照seq拆分,达到length个子串后停止,length不指定就是拆分整个字符串,seq可以是正则表达式。

  • String.prototype.replace(reg,replacement) 将字符串中满足reg的子串使用replacement替换。返回一个新的字符串。

    • reg:正则表达式,也可以是普通字符串,如果不是正则表达式的全局模式,只会替换字符串中第一次匹配到的结果。
    • replacement:可以是字符串,但是其中的$符具有特殊的含义。
      • $1、$2、...、$99:与 regexp 中的第 1 到第 99 个子表达式相匹配的文本
      • $&:与regexp相匹配的子串
      • $`:位于匹配子串左侧的文本
      • $':位于匹配子串右侧的文本
      • $$:直接量符号
    • replacement也可以是函数function replacement(item,$1,$2,.....,$n,index,sourceStr)
      • 第一个参数item,表示匹配的子字符串
      • 第 2 ~ n 个参数开始代表子表达式匹配的文本
      • 倒数第 2 个参数为整数,代表匹配的项在原字符串中的索引
      • 最后 1 个参数为源字符串

常用正则表达式

验证有效数字

规则:

  • 第一位可以是 + 、-,也可以没有。

  • 只有1位,0-9都行。

  • 多位时,第一位数字不能是0,是1-9,后面可以是0-9,0-多次

  • 然后是小数位,小数位可能有可能没有,整体是 0 - 1次

let reg = /^[+-]?(\d|([1-9]\d+))(\.\d+)?$/; 
复制代码

验证密码

规则:数字、字母、_,6-16位

let reg = /^\w{6,16}$/;
复制代码

验证真实姓名(中文)

注意:

  • 中文Unicode范围:\u4E00 -- \u9FA5

  • 译名:如"莱昂纳多·达·芬奇"

let reg = /^[\u4E00-\u9FA5]{2,}(·[\u4E00-\u9FA5]{1,})*$/
复制代码

验证邮箱

规则:

  • 邮箱名,即@前面的内容,可以是数字、字母、_、.、-

    \w+((-\w+)|(\.\w+))*

  • @后面可以是一级域名xx.xx,可以是二级域名xx.xx.xx,可以是企业域名xx-xxx.xxxx.xx.xxxx.xx-xx.xx

    [a-zA-Z0-9]+((\.|-)[a-zA-Z0-9]+)*

  • 最后一部分是域名\.[a-zA-Z0-9]+

let reg = /^\w+((-\w+)|(\.\w+))*@[a-zA-Z0-9]+((\.|-)[a-zA-Z0-9]+)*\.[a-zA-Z0-9]+$/;
复制代码

验证身份证号

规则:

  • 目前第一代身份证不能用了,第二代身份证是18位
  • 最后一位是数字或X(代表10)

所以简单版本:

let reg = /^\d{17}(\d|X)$/;
复制代码

但是身份证信息中有许多有用的信息,可能需要进行单独捕获,所以需要用到分组:

  • 前6位代表省市区/县,可以查询出来所有结果
  • 接着8位是出生年月日
  • 接着2位是签发机关编码
  • 倒数第二位代表性别,基数是男,偶数是女
  • 最后一位是通过算法计算出来的
let reg = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{2})(\d)(\d|X)$/;
复制代码

常见面试题

  1. 去掉字符串中间的空白字符

    function myTrim(sourceStr){    
        return sourceStr.replace(/\s+/g,'');
    }
    复制代码
  2. 去掉字符串最后一个指定字符

    function subLastChar(sourceStr,ch){    
        return sourceStr.split('').reverse().join('').replace(ch,'').split('').reverse().join('');
    }
    复制代码
  3. 把下划线命名转换为大驼峰命名

    //每次只匹配_x,把x变为大写
    function changeCamel(sourceStr){    
        let strResult = sourceStr.replace(/_+([a-zA-Z]{1})/g, function(){        
            return arguments[1].toUpperCase();    
        }); 	
        //把字符串最前面的字符变为大写,因为最前面的字符前不一定有_,有可能就变成小驼峰了    
        return strResult[0].toUpperCase() + strResult.substr(1);}
    复制代码
  4. 写一个字符串中大小写切换的方法

    function changeCase(sourceStr){    
        let strResult = sourceStr.replace(/([a-z]+)|([A-Z]+)/g, function(){        
            let str = arguments[0];        
            if(arguments[1]){            
                str = str.replace(arguments[1],arguments[1].toUpperCase());        
            }        
            if(arguments[2]){            
                str = str.replace(arguments[2],arguments[2].toLowerCase());        
            }        
            return str;    
        });    
        return strResult;
    }
    复制代码
  5. 统计某一字符或字符串在给定字符串中出现的次数

    function getStrCount(sourceStr,targetStr){    
        let reg = new RegExp(targetStr,'g');    
        let result = null;    
        let count =0;    
        while((result=reg.exec(sourceStr)) != null){        
            count++;    
        }    
        return count;
    }
    复制代码
  6. 写一个获取当前url查询字符串中的参数的方法

    function getUrlParams(str){    
        let reg = /[?&]\s*(\w+)\s*=\s*(\w+)\s*/g;    
        let result = null;    
        let paramObj={};    
        while((result = reg.exec(str)) !== null) {        
            paramObj[result[1]]=result[2];    
        }
    }
    getUrlParams("http://sss.cc.cc? a   =  11  & b=  22  ");
    复制代码
  7. 查询字符串中出现次数最多的字符

    function getMaxCountChar(str) {	
        let map = new Map();	
        let ary = str.split('');	
        let result = { char: undefined, count: 0 };	
        ary.forEach((ch) => {		
            let count = map.get(ch);		
            if (count) {			
                map.set(ch, ++count);		
            } else {			
                map.set(ch, 1);		
            }		
            if (count > result.count) {			
                result.char = ch;			
                result.count = count;		
            }	
        });	
        return result;
    }
    /** * 这种方法循环的次数少,时间复杂度低,性能高 */
    function getMaxCountChar1(str) {	
        let letter = str.length > 0 ? str[0] : '';	
        let srcCount = str.length;	
        let result = { char: '', count: 0 };	
        while (letter !== '') {		
            let reg = new RegExp(letter, 'g');//将字符串中当前字符全部替换为空		
            str = str.replace(reg, '');// 当前字符的出现次数就是没有替换前的字符个数减去替换后的字符个数		
            let letterCount = srcCount - str.length;// 将原字符串长度更新		
            srcCount = str.length;// 如果次数大于结果中的次数,重置结果		
            if (letterCount > result.count) {			
                result.char = letter;			
                result.count = letterCount;		
            }		
            // 每次循环完重置当前字符为新字符串的首个字符		
            letter = str.length > 0 ? str[0] : '';	
        }	
        return result;
    }
    let result = getMaxCountChar1('asdassdascasdd212wwqcsdgass');
    console.log(result);
    复制代码
文章分类
前端
文章标签