在前端工作中,正则表达式的作用不仅仅是"验证手机"、"验证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'
}
**/
解析:
- 翻译一下正则表达式/([^=&?]+)=([^=&?]*)/g 就是: 使用global模式,匹配
(一个或多个非=&?的字符)=(零个或多个非=&?的字符); - exec 方法就是用正则去匹配目标字符串,如果能找到匹配的话,则返回一个数组(其实是多了index、input等属性的数组,暂时将其命名为group),否则返回null。其中group[0]为整个正则匹配到的内容,group[n]为第n个()匹配的内容。由于案例中正则是global模式,所以每次exec 正则都会记住每次匹配在目标字符串的最终位置,再次exec时会从上次匹配的最终位置后一位开始匹配。看看下面的内容是否能够帮助理解global和分组
每次的执行过程可以这么理解
第1次reg.exec(query):
用于匹配的内容是"?name=A&id=123&abc=&ticket=fxxxx&time=2021-03-04&serveicename=sys"
得到group的值为 ['name=A','name','A']
第2次reg.exec(query):
用于匹配的内容是"&id=123&abc=&ticket=fxxxx&time=2021-03-04&serveicename=sys"
得到group的值为 ['id=123','id','123']
第3次reg.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
**/
解析:
- 此处使用 new RegExp的方式动态新建 正则表达式对象,若参数name的值为’name‘, 代码中reg对象等效于 /(?:^|[?&])name=([^=&?]*)/
- 其中(?:)表示’非存储式匹配‘, 效果就是 执行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"));
解析:
- 该正则可以分为五个部分看,即^,(?=.\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
解析:
- "预匹配" 即(?=) 部分。
- 正则可以拆分为两个部分:(\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;
}
解析
- 我们先解析一下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}
- 大家可以自己构想几个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;
}