blog
blog copied to clipboard
正则手记——实例篇
前言
记录正则实际使用案例,举一反三。
搭配在线工具
- https://regex101.com/
- https://jex.im/regulex/
匹配字符
校验
- 网址
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/.test(
"https://regex101.com/"
); // true
- 迅雷链接
/^thunderx?:\/\/[a-zA-Z\d]+=$/;
- 磁力链接(宽松匹配)
/^magnet:\?xt=urn:btih:[0-9a-fA-F]{40,}.*$/;
- isPC
!/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent);
- 检查 MAC 地址
网络接口的 MAC 地址 由 6 个以冒号分隔的两位十六进制数字组成。
例如:'01:32:54:67:89:AB'。
编写一个检查字符串是否为 MAC 地址的正则表达式。
用例:
let regexp = /[0-9-A-F]{2}(:[0-9-A-F]{2}){5}/;
alert(regexp.test("01:32:54:67:89:AB")); // true
alert(regexp.test("0132546789AB")); // false (没有冒号分隔)
alert(regexp.test("01:32:54:67:89")); // false (5 个数字,必须为 6 个)
alert(regexp.test("01:32:54:67:89:ZZ")); // false (尾部为 ZZ)
- 中文姓名
/^(?:[\u4e00-\u9fa5·]{2,16})$/
- 手机号码(宽松)
只要是 13,14,15,16,17,18,19 开头即可
/^1[3-9]\d{9}$/;
- 用户名
仅允许中文、字母、数字和•_-
/^[\u4e00-\u9fa5\sa-zA-Z·_-]+$/;
- 日期(宽松)
/^\d{1,4}(-)(1[0-2]|0?[1-9])\1(0?[1-9]|[1-2]\d|30|31)$/;
- 身份证号(宽松)
2 代,18 位数字,最后一位是校验位,可能为数字或字符 X
/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/;
- 身份证号(严格)
// 1. 18 位数字 /17 数字 + X
// 2. 地区必须正确
// 3. 日期格式必须正确
// 4. 校验位
function idCard(idcard) {
// 1. 只能是18位身份证号码
if (!idcard || !/^\d{17}[\dXx]$/.test(idcard)) {
return "请输入18位身份证号码";
}
// 2. 地区必须正确
var areaReg = /[16][1-5]|2[1-3]|3[1-7]|4[1-6]|5[0-4]6[1-5]|71|81|82/;
if (!areaReg.test(idcard.substr(0, 2))) {
return "身份证格式不正确,请重新输入";
}
// 3. 日期必须正确
let [, y, m, d] = idcard.substr(6, 8).match(/([\d]{4})([\d]{2})([\d]{2})/);
let date = new Date(`${y}-${m}-${d}`);
if (date.getDate() !== +d) {
return "身份证格式不正确,请重新输入";
}
// 4. 校验位必须正确
let arr = idcard.split("").map((n) => +n);
let S = [7, 9, 10, 5, 8, 4, 2].reduce(
(s, n, i) => s + n * (arr[i] + arr[i + 10]),
[1, 6, 3].reduce((s, n, i) => s + n * arr[7 + i], 0)
);
let M = +"10X98765432".substr(S % 11, 1);
let ret = M.toString() === arr[17].toString();
return ret ? true : "身份证格式不正确,请重新输入";
}
- 数字范围正则
1-1000
/^[1-9](\d{0,1}\d)?$|^1000$/;
大于等于 0, 小于等于 150, 支持小数位出现 5, 如 145.5, 用于判断考卷分数
/^150$|^(?:\d|[1-9]\d|1[0-4]\d)(?:\.5)?$/;
1-65535
/^([1-9]\d{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/
// 拆分来看
/[1-9]\d{0,3}/; // 1-9999
/[1-5][0-9]{4}/; // 10000-59999
/6[0-4][0-9]{3}/; // 60000-64999
/65[0-4][0-9]{2}/; // 650000-65499
/655[0-2][0-9]/; // 65500-65529
/6553[0-5]/; // 65530-65535
提取有效信息
- 去除字符左右的空格
let str = " 1 24 ";
str.replace(/^\s+|\s+$/g, ""); // 去除左右空格 1 24
str.replace(/\s/g, ""); // 去除所有空格 124
- 匹配连续重复的字符
/(.)\1+/;
- html 标签(宽松匹配)
/<(\w+)[^>]*>(.*?<\/\1>)?/;
- 获取文件名
let url = `http://www.wuhuepb.gov.cn/WebFileDb/NewFile/201407010837081010.pdf`;
let name = "";
if (/.*\/(.+)\.pdf$/.test(href)) {
name = url.match(/.*\/(.+)\.pdf$/)[1];
}
console.log(name); // 201407010837081010
- 提取标签属性
// 匹配 id
// 1
let regex = /id=".*?"/; // 想想为什么要加? 不加的话 连后面的class都会匹配到
let string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
// 2
let regex = /id="[^"]*"/;
let string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
- 提取颜色
找出形如 #abc 或 abcdef 的颜色值。即 #
后面跟着 3 个或 6 个十六进制的数字。
let regexp = /#([a-f0-9]{3}){1,2}\b/gi;
let str = "color: #3f3; background-color: #AA00ef; and: #abcd";
alert(str.match(regexp)); // #3f3 #AA00ef
- 颜色转换
let code = `background: #000000B3;border: 1px solid #0003;`;
function hexToRgba(str) {
let arr;
if (str.length == 4) {
arr = str.split("").map((c) => c + c);
} else {
arr = [str.slice(0, 2), str.slice(2, 4), str.slice(4, 6), str.slice(6, 8)];
}
let [r, g, b, a] = arr.map((n) => parseInt(n, 16));
a = parseInt((a * 1000) / 256) / 1000;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
code = code.replace(/#([0-9A-Fa-f]{8}|[0-9A-Fa-f]{4})\b/g, ($, $1) => {
let v = hexToRgba($1);
return v;
});
console.log(code); // background: rgba(0, 0, 0, 0.699);border: 1px solid rgba(0, 0, 0, 0.199);
- 脱敏规则
(1)手机号
保留前 3 位和最后 4 位,其余用*代替。
value.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
(2)姓名
3 个字以内隐藏第 1 个字,4-6 个字隐藏前 2 个字,大于 6 个字隐藏第 3-6 个字,隐藏字用*代替;
if (value.length <= 3) {
return "*" + value.substr(1);
} else if (value.length <= 6) {
return "**" + value.substr(2);
} else {
return value.substr(0, 2) + "****" + value.substr(6);
}
(3)身份证
号码最后四位用*替换。
value.replace(/^(\d{14})\w{4}$/, "$1****");
- 找出所有数字
编写一个正则表达式,找出所有十进制数字,包括整数、浮点数和负数。
用例:
let regexp = /你的正则表达式/g;
let str = "-1.5 0 2 -123.4.";
alert(str.match(regexp)); // -1.5, 0, 2, -123.4
解决方案带有可选小数部分的正数:\d+(\.\d+)?
。
让我们在开头加上可选的 -:
let regexp = /-?\d+(\.\d+)?/g;
let str = "-1.5 0 2 -123.4.";
alert(str.match(regexp)); // -1.5, 0, 2, -123.4
- 找出算术式子
一个算术表达式由 2 个数字和一个它们之间的运算符组成,例如:
1 + 2
1.2 * 3.4
-3 / -6
-2 - 2
运算符为 "+"、"-"、"*" 或 "/" 中之一。
在开头、之间的部分或末尾可能有额外的空格。
创建一个函数 parse(expr)
,它接受一个表达式作为参数,并返回一个包含 3 个元素的数组:
- 第一个数字
- 运算符
- 第二个数字
用例:
let [a, op, b] = parse("1.2 * 3.4");
alert(a); // 1.2
alert(op); // *
alert(b); // 3.4
// (\d+(?:\.\d+)?)\s*([\+\-\*\/])\s*(\d+(?:\.\d+)?)
function parse(expr) {
let regexp = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/;
let result = expr.match(regexp);
if (!result) return [];
result.shift();
return result;
}
alert(parse("-1.23 * 3.45")); // -1.23, *, 3.45
匹配位置
语法例子
\b \B
\d \D
\s \S
// 开始与结束
'I love Rainbow.'.replace(/^/,'❤️'); // ❤️I love Rainbow.
'I love Rainbow.'.replace(/$/,'❤️'); // I love Rainbow.❤️
// \b 单词的边界
'I love Rainbow.'.replace(/\b/,'❤️'); // ❤️I❤️ ❤️love❤️ ❤️Rainbow❤️.
// \B 非单词的边界
'I love Rainbow.'.replace(/\B/,'❤️'); // I l❤️o❤️v❤️e R❤️a❤️i❤️n❤️b❤️o❤️w.❤️
// (?:p) 和直接匹配 p 差不多,表示非捕获组
'I love Rainbow.'.replace(/(?:R)/,'❤️'); // I love ❤️ainbow.
x(?=y) 前瞻肯定断言 x 被 y 跟随时匹配 x
x(?!y) 前瞻否定断言 x ,仅当后面不跟 y
(?<=y)x 后瞻肯定断言 x ,仅当跟在 y 后面
(?<!y)x 后瞻否定断言 x ,仅当不跟在 y 后面
// (?=p) 符合p子模式前面的那个位置 (正向先行断言)
'I love Rainbow.'.replace(/(?=R)/,'❤️'); // I love ❤️Rainbow.
// (?!p) 符合非p子模式前面的那个位置 (负向先行断言)
'I love Rainbow.'.replace(/(!R)/,'❤️'); // ❤️I❤️ ❤️l❤️o❤️v❤️e❤️ R❤️a❤️i❤️n❤️b❤️o❤️w❤️.❤️
// (?<=p) 符合p子模式后面(注意(?=p)表示的是前面)的那个位置
'I love Rainbow.'.replace(/(<=R)/,'❤️'); // I love R❤️ainbow.
// (?<=p)反过来的意思,可以理解为(?<=p)匹配到的位置之外的位置都是属于(?<!p)的
'I love Rainbow.'.replace(/(<!R)/,'❤️'); // ❤️I❤️ ❤️l❤️o❤️v❤️e❤️ ❤️Ra❤️i❤️n❤️b❤️o❤️w❤️.❤️
// 单词间隔换为 ❤️,实现类似 \s
"I love Rainbow.".replace(/(?!\.)\W/, "❤️"); // I❤️love❤️Rainbow.
数字的千分位分割法
自后向前
let price = "123456789";
let priceReg = /(?!^)(?=(\d{3})+$)/g;
console.log(price.replace(priceReg, ",")); // 123,456,789
/(\d{3})/+$/ //自后向前必须是1个或多个的3个连续数字;
/(?!^)/+$/ // 这个位置不在首位 (用 \B 也是可以的)
自前向后匹配
let price = "123456789";
let priceReg = /\B(?=(\d{3})+(?!\d))/g;
console.log(price.replace(priceReg, ",")); // 123,456,789
/\B(?=(\d{3})+)/ // 不在开头和结尾,自前向后必须是跟着1个或多个的3个连续数字的位置;
/(?!\d)/ // 这个位置后面不允许跟着数字;
手机号 3-4-4 分割
将手机号 13112345678 转化为 131-1234-5678
let mobile = "13112345678";
let mobileReg = /(?=(\d{4})+$)/g;
console.log(mobile.replace(mobileReg, "-")); // 131-1234-5678
综合案例
提取 img 信息
const tabContent = `<p style="text-align: center;"><img href="{"app":"https://a.cn/qwcrm/dl//fyiYVr","miniProgram":"https://a.cn/qwcrm/h5/#/codScanning?corp_id=wwe072c386cba35e96&channelId=367412"}" src="https://a.cn/xxxcms/file/upload/mob/productImg/S20200276/[email protected]"/></a></p><p style="text-align: center;"><img src="https://a.cn/xxxcms/ueditor/20220209/a96f4da8-f448-4862-8a2e-546759f85389.png" title="[email protected]" _src="https://a.cn/xxxcms/ueditor/20220209/a96f4da8-f448-4862-8a2e-546759f85389.png" alt="[email protected]"/></p></p>`;
/**
* 解析image标签
* @param {String} html 待解析的html文本
*/
function parseImage(html) {
let result = [];
//匹配图片
imageArray = html.match(/<img.*?(?:>|\/>)/gi);
result = imageArray
.map((str) => {
let src,
link = "";
//匹配链接
let srcReg = /src=(['"])?([^'"]*)\1?/i;
//匹配跳转链接
let hrefReg = /href=(['"])?([^'"]*)\1?/i;
let hrefReg__obj = /href=(['"])({.*})\1/i;
let srcMatching = str.match(srcReg) || [];
let hrefMatching1 = str.match(hrefReg) || [];
let hrefMatching2 = str.match(hrefReg__obj) || [];
src = srcMatching[2];
link = hrefMatching2[2] || hrefMatching1[2] || "";
return src ? { src, link } : false;
})
.filter((item) => {
//过滤掉无资源链接图片
return item;
});
return result;
}
console.log(parseImage(tabContent));
// [
// {
// src: "https://a.cn/xxxcms/file/upload/mob/productImg/S20200276/[email protected]",
// link: '{"app":"https://a.cn/qwcrm/dl//fyiYVr","miniProgram":"https://a.cn/qwcrm/h5/#/codScanning?corp_id=wwe072c386cba35e96&channelId=367412"}',
// },
// {
// src: "https://a.cn/xxxcms/ueditor/20220209/a96f4da8-f448-4862-8a2e-546759f85389.png",
// link: "",
// },
// ];
提取 table 内容
let trimHtml = table.replace(/\s{2,}/g, "");
const matchs = [...trimHtml.matchAll(/<td.*?>(.*?)<\/td>/g)];
const result = matchs
.map((v) => {
return (v && v[1]) || "";
})
.join("");
console.log(result);
实现一个模板引擎
var tplEngine = function (tpl, data) {
var reg = /<%([^%>]+)?%>/g,
code = "var r=[];\n",
cursor = 0; //主要的作用是定位代码最后一截
regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
var add = function (line, js) {
if (js) {
// 表达式原样输出
if (line.match(regOut)) {
code += line + "\n";
}
// 变量输出
else {
code += "r.push(" + line + ");\n";
}
} else {
// 非逻辑部分字符串转义输出 (保留字符串的引号)
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
}
};
// 改为变量直接注入
Object.entries(data).forEach(([key, val]) => {
code += `var ${key} = this['${key}'];\n`;
});
while ((match = reg.exec(tpl))) {
add(tpl.slice(cursor, match.index)); //添加非逻辑部分
add(match[1], true); //添加逻辑部分 match[0] = "<%" + match[1] + "%>";
cursor = match.index + match[0].length;
}
add(tpl.substr(cursor, tpl.length - cursor)); //代码的最后一截 如:" years old."
code += 'return r.join("");'; // 返回结果,在这里我们就拿到了装入数组后的代码
console.log("code:", code);
return new Function(code.replace(/[\r\t\n]/g, "")).apply(data);
};
测试例子:
var str = "Hei, my name is <%name%>, and I'm <%info.age%> years old. I like <%info.likes[0].name%>.";
var obj = {
name: "yanyue404",
info: {
age: "20",
likes: [
{
id: 1,
name: "basketball",
},
],
},
};
alert(tplEngine(str, obj)); // Hei, my name is yanyue404, and I'm 20 years old. I like basketball.
var tpl =
"<% for(var i = 0; i < posts.length; i++) {" +
"var post = posts[i]; %>" +
"<% if(!post.expert){ %>" +
"<span>post is null</span>" +
"<% } else { %>" +
'<a href="#"><% post.expert %> at <% post.time %></a>' +
"<% } %>" +
"<% } %>";
var data = {
posts: [
{
expert: "content 1",
time: "yesterday",
},
{
expert: "content 2",
time: "today",
},
{
expert: "content 3",
time: "tomorrow",
},
{
expert: "",
time: "eee",
},
],
};
console.log(tplEngine(tpl, data));
// <a href="#">content 1 at yesterday</a>
// <a href="#">content 2 at today</a>
// <a href="#">content 3 at tomorrow</a>
// <span>post is null</span>
参考链接
- https://zh.javascript.info/
- https://juejin.cn/post/7021672733213720613
- https://github.com/any86/any-rule
- JavaScript 模板引擎原理,几行代码的事儿
- 实现一个简单的模板引擎