正则表达式的使用实例

201 阅读6分钟

在前端工作中,正则表达式的作用不仅仅是"验证手机"、"验证email"这些,还有很多有趣的用途。我收集整理了一些关于正则表达式的用法,想就着实例来发表自己对正则的一些理解和心得。

一、 分割windonw.location.query, 将其转换为一个json对象

比如有路径为 xxxxx.com?name=A&abc=&id=123&ticket=fxxxx&time=2021-03-04&serveicename=sys 的页面,可以利用以下方法将window.location.query字符串解析成json对象。该案例主要用到分组的概念。

// 模拟 window.location.query
let query = "?name=A&abc=&id=123&ticket=fxxxx&time=2021-03-04&serveicename=sys"
function resolveToObject(query) {
    const reg = /([^=&?]+)=([^=&?]*)/g;
    let group = reg.exec(query);
    const object = {};
    while (group) {
        object[group[1]] = group[2];
        group = reg.exec(query);
    }
    return object;
}
console.log(resolveToObject(query));
/**
此处 window.location.query 为 "?name=A&id=123&abc=&ticket=fxxxx&time=2021-03-04&serveicename=sys";
于是我们得到的输出为
{
  name: 'A',
  id: '123',
  abc: '',
  ticket: 'fxxxx',
  time: '2021-03-04',
  serveicename: 'sys'
}
**/

解析:

  1. 翻译一下正则表达式/([^=&?]+)=([^=&?]*)/g 就是: 使用global模式,匹配(一个或多个非=&?的字符)=(零个或多个非=&?的字符);
  2. exec 方法就是用正则去匹配目标字符串,如果能找到匹配的话,则返回一个数组(其实是多了index、input等属性的数组,暂时将其命名为group),否则返回null。其中group[0]为整个正则匹配到的内容,group[n]为第n个()匹配的内容。由于案例中正则是global模式,所以每次exec 正则都会记住每次匹配在目标字符串的最终位置,再次exec时会从上次匹配的最终位置后一位开始匹配。看看下面的内容是否能够帮助理解global和分组
每次的执行过程可以这么理解
第1reg.exec(query):
   用于匹配的内容是"?name=A&id=123&abc=&ticket=fxxxx&time=2021-03-04&serveicename=sys"
   得到group的值为 ['name=A','name','A']2reg.exec(query):
   用于匹配的内容是"&id=123&abc=&ticket=fxxxx&time=2021-03-04&serveicename=sys"
   得到group的值为 ['id=123','id','123']3reg.exec(query):
   用于匹配的内容是"&abc=&ticket=fxxxx&time=2021-03-04&serveicename=sys"
   得到group的值为 ['abc=','abc','']
.
.
.
注:如果把 /([^=&?]+)=([^=&?]*)/g 中的个去掉,就会死循环,每次循环用于匹配的内容不会反生变化

java 对应的写法:

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Test {
    public static void main(String[] args) {
        String query = "?name=A&abc=&id=123&ticket=fxxxx&time=2021-03-04&serveicename=sys";
        Pattern reg = Pattern.compile("([^=&?]+)=([^=&?]*)");
        Matcher m = reg.matcher(query);
        Map<String,String> ret = new HashMap<String,String>();
        while (m.find()) {
            System.out.println(m.group());
            // m.group() 等效于 js 中group[0]
            ret.put(m.group(1),m.group(2));
        }
        System.out.println(ret);
    }
}

二、 从query字符串中查询某个参数值

可以使用下面的function

// 模拟 window.location.query
let query = "?name=A&abc=&id=123&ticket=fxxxx&time=2021-03-04&serveicename=sys"
function getValueByParamName(name, query) {
  const reg = new RegExp(`(?:^|[?&])${name}=([^=&?]*)`);
  const group = reg.exec(query);
  if (group) {
    return group[1];
  }
  return null;
}
console.log(getValueByParamName("name", query));
/**
输出为 A
**/

解析:

  1. 此处使用 new RegExp的方式动态新建 正则表达式对象,若参数name的值为’name‘, 代码中reg对象等效于 /(?:^|[?&])name=([^=&?]*)/
  2. 其中(?:)表示’非存储式匹配‘, 效果就是 执行exec后返回的group对象中不会存储当前括号匹配的内容,即group[1]中是第二个()匹配的内容。若未使用(?:) 而直接用(),则需要return group[2]

java的对应写法

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Test {
    public static void main(String[] args) {
        String query = "?name=A&abc=&id=123&ticket=fxxxx&time=2021-03-04&serveicename=sys";
        System.out.println(getValueByParamName("name", query));
    }

    private static String getValueByParamName(String name,String query) {
       Pattern reg = Pattern.compile("(?:^|[?*])" + name + "=([^=&?]*)");
       Matcher m = reg.matcher(query);
       if (m.find()) {
           return m.group(1);
       }
       return null;
    }
}

三、密码格式验证:必须包含 数字、大写字母、小写字母至少六位

案例中主要用到预匹配

function validate(str) {
    let reg = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).{6,}$/;
    return reg.test(str);
}
console.log(validate("123456"));
console.log(validate("123A456"));
console.log(validate("123Ab4*6"));

解析:

  1. 该正则可以分为五个部分看,即^,(?=.\d),(?=.[A-Z]),(?=.[a-z]),.{6,}$,假设str为 “123Ab6”,匹配的过程可以如此理解:
    ^表示从字符串第一个字符开始,(?=.*\d)表示从当前的匹配位置开始(此时即字符串起始处)
    匹配.*\d, 匹配到了“123Ab*6”,接着(?=.*[A-Z])从当前的匹配位置开始(还是字符串起始处,
    因为预匹配不会改变当前匹配位置)匹配到了“123A”,后一个也是同样道理。
    这样三个预匹配都通过了。开始 匹配 .{6,}$,即匹配"到字符串结束位置的任意6个或6个以上字符"。
    由此最终达到验证 “必须包含 数字、大写字母、小写字母至少六位”的目的

java 的对应写法为

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Test {
    public static void main(String[] args) {
        System.out.println(validate("123456"));
        System.out.println(validate("123A456"));
        System.out.println(validate("123Ab4*6"));
    }

    private static boolean validate(String pwd) {
       Pattern reg = Pattern.compile("^(?=.*\\d)(?=.*[A-Z])(?=.*[a-z]).{6,}$");
       Matcher m = reg.matcher(pwd);
       return m.matches();
    }
}

四、 数字中添加千位分隔符,

例如将数字 1234567890 转为1,234,567,890。案例主要是分组与预匹配的搭配使用

   let num = "1234567890";
   let ret = num.replace(/(\d{1,3})(?=(\d{3})+$)/g,function(g,g1){
       return `${g1},`
   });
   console.log(ret);
   // 打印出的结果为 1,234,567,890

解析:

  1. "预匹配" 即(?=) 部分。
  2. 正则可以拆分为两个部分:(\d{1,3})与(?=(\d{3})+$)。第一个()匹配到的内容即为fucntion中g1参数的值,可以打印出来便于理解。第二个预匹配的正则(\d{3})+$表示"直至字符串结尾的一个或多个3个数字的组合"。

repalce的执行过程可以这么理解:

第1次循环:
    匹配的内容为"1234567890"
    (?=(\d{3})+$)先进行预匹配,匹配到 "234567890",于是 (\d{1,3}) 只能匹配到 "1",function中g1即为 1, return "1,"
第2次循环:
    匹配的内容为"234567890"
    (?=(\d{3})+$)先进行预匹配,匹配到 "567890",于是 (\d{1,3}) 只能匹配到 "234",function中g1即为 234, return "234,"
.
.
.
第n次循环:
    匹配的内容为"890"
    (?=(\d{3})+$)先进行预匹配,匹配到 "890",于是 (\d{1,3}) 未能匹配到内容,循环结束
于是最终结果 “1,234,567,890”

注:还有个快捷写法为 num.replace(/(\d{1,3})(?=(\d{3})+$)/g,"$1,");其中$1等效于g1, 如果有多个括号还可以用$2,$3等 java对应写法为

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Test {
    public static void main(String[] args) {
        System.out.println(format("1234567890"));
    }

    private static String format(String pwd) {
       Pattern reg = Pattern.compile("(\\d{1,3})(?=(\\d{3})+$)");
       Matcher m = reg.matcher(pwd);
       return m.replaceAll("$1,");
    }

}

五、解析xml htlm格式字符串

案例中主要正则与栈的搭配使用,使用到了正则中“回溯”的概念

function compile(template) {
    const tagStack = [];
    const all = [];
    /**
    1. 正则分为三个部分:<([a-zA-Z]+)([^<>]*?)(\/?)>,([^<>]+)和<\/([a-zA-Z]+)
    2. 三个部分由 | 号分隔(也就是说三个部分是‘或'的关系)
    3. 解析一下这三个部分:
        1)<([a-zA-Z]+)([^<>]*?)(\/?)>目的是匹配到类似<div name='A' sex="female">或者<input type='file' />这样的部分,其中“*?”意为“非贪婪匹配”(即尽可能少的匹配),与之相比的“*”为贪婪匹配(即尽可能多的匹配)。
        2)([^<>]+?) 目的是匹配到<div>text</div>中间的text部分,不含首尾标签,加上括号以抓取文字内容。
        3)<\/([a-zA-Z]+) 目的是配到</div> 这样的标签结束部分,加上括号以抓取标签名称,以此来判断一个标签是否已结束
    */
    const startReg = /<([a-zA-Z]+)([^<>]*)(\/?)>|([^<>]+)|<\/([a-zA-Z]+)>/g;
    let ret = startReg.exec(template);
    let obj;
    while (ret) {
        // 如果ret[1]有值则说明这是一个标签的开头,ret[1]即为匹配到的标签名
        if (ret[1]) {
            obj = {
                tag: ret[1],
                // ret[2]为匹配到的属性字符串部分,用resolveAttrs方法解析为对象
                attrs: resolveAttrs(ret[2]),
                children: [],
            };
            // 若栈中存在之前存在之前匹配到的标签,说明当前标签为栈中最后一个标签的子标签
            if (tagStack[tagStack.length - 1]) {
                tagStack[tagStack.length - 1].children.push(obj);
            } else if(ret[3]) {// 若当前栈中为空,并且标签已结束
                all.push(obj);
            }
            // ret[3]为 ’/>‘, ret[3]为空,则说明标签还未结束
            if (!ret[3]) {
                tagStack.push(obj);
            }
        } 
        // ret[4]为匹配到的标签内文字内容,此时在栈中最后一个元素中存入一个文本node
        else if (ret[4] && ret[4].replace(/\s/g, "")) {
            tagStack[tagStack.length - 1].children.push({
                tag: "",
                text: ret[4],
            });
        } 
        // ret[5]不为空,则说明当前标签已经结束
        else if (ret[5]) {
            obj = tagStack.pop();
            // 此时栈为空说明,当前标签没有父标签了
            if (tagStack.length === 0) {
                all.push(obj);
            }
        }
        ret = startReg.exec(template);
    }
    return all;
}
// 用于解析属性值
function resolveAttrs(attrString) {
    const reg = /([^<>=\s'"]+)(=(['"])([^"<>=]*)\3)?/g;
    let ret = reg.exec(attrString);
    const map = {};
    while (ret) {
        map[ret[1]] = ret[4];
        ret = reg.exec(attrString);
    }
    return map;
}

解析

  1. 我们先解析一下resolveAttrs的作用
1)可以看到,该function 使用到的正则包含4个group, 其中第3、第4个包含在第2个group中。
2)其中'\3'即为回溯的概念。例如 标签<person name='A' sex="'f" active /> ,
   传给resolveAttrs 的入参为“ name='A' sex="f" active ”,
   第1次exec时第1个group匹配到了name(ret[1]即为name),第3个group匹配到了单引号,于是 \3 也是单引号(\3 表示第3个group匹配到的内容,于是第4个group匹配到了"A"(ret[4]即为 A)。
   第2次exec时,ret[1]为 sex,\3 为双引号,ret[4]即为f。
   第3次exec时,ret[1]为 active,第2个group未匹配到任何内容
   于是最终结果为 {name: "A", sex: "f", active: undefined}
  1. 大家可以自己构想几个xml格式的字符串,就着注释来探究一下compile的过程

六、最长回文子串的正则解法

这是在leetcode上的一道题,我自己琢磨出来的一个解法,本质上也属于“中心扩散法”,只是用的正则表达式来寻找的扩散中心

var longestPalindrome = function(s) {
    // 本身是回文数返回本身
    if (isPalindrome(s)) {
        return s;
    }
    // 本身非回文数,字符串长度为2,返回第一个字符
    if (s.length === 2) {
        return s[0];
    }
    // 此处开始寻找ABA模式的扩散中心
    let reg = /(\w)(?=(\w\1))/g;
    let ret = reg.exec(s);
    let starts = [];
    while (ret) {
        let len = ret[2].length + 1;
        let start = ret.index;
        starts.push({
            start,
            len
        });
        ret = reg.exec(s);
    }
    // 此处开始寻找AA模式的扩散中心
    reg = /(\w)(?=(\1))/g;
    ret = reg.exec(s);
    while (ret) {
        let len = ret[2].length + 1;
        let start = ret.index;
        starts.push({
            start,
            len
        });
        ret = reg.exec(s);
    }
    let max = s[0];
    starts.forEach(pair => {
        let { start,len } = pair;
        let _max = s.substring(start, start + len);
        while (true) {
            start--;
            if (start < 0) {
                break;
            }
            len += 2;
            let target = s.substring(start, start + len);
            // 开始扩散,开始找最长回文串
            if (s[start] === s[start+len-1]) {
                if (_max.length < target.length) {
                    _max = target;
                }
            } else {
                break;
            }
        }
        if (max.length < _max.length) {
            max = _max;
        }
    });
    return max;
};
function isPalindrome(s) {
    let _s = [...s];
    _s.reverse();
    return _s.join('') === s;
}