Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

正则手记——实例篇 #230

Open
yanyue404 opened this issue Jun 24, 2022 · 0 comments
Open

正则手记——实例篇 #230

yanyue404 opened this issue Jun 24, 2022 · 0 comments

Comments

@yanyue404
Copy link
Owner

yanyue404 commented Jun 24, 2022

前言

记录正则实际使用案例,举一反三。

搭配在线工具

匹配字符

校验

  • 网址
/^(((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 个元素的数组:

  1. 第一个数字
  2. 运算符
  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>

参考链接

@yanyue404 yanyue404 changed the title 正则手记 前端正则手记 Aug 11, 2022
@yanyue404 yanyue404 changed the title 前端正则手记 正则手记——实例篇 Nov 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant