yanyue404 / blog

Just blog and not just blog.
https://yanyue404.github.io/blog/
Other
87 stars 13 forks source link

正则手记——实例篇 #230

Open yanyue404 opened 2 years ago

yanyue404 commented 2 years ago

前言

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

搭配在线工具

匹配字符

校验

/^(((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,}.*$/;
!/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent);

网络接口的 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+/;
/<(\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/banenr@3x.png"/></a></p><p style="text-align: center;"><img src="https://a.cn/xxxcms/ueditor/20220209/a96f4da8-f448-4862-8a2e-546759f85389.png" title="02@2x.png" _src="https://a.cn/xxxcms/ueditor/20220209/a96f4da8-f448-4862-8a2e-546759f85389.png" alt="02@2x.png"/></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/banenr@3x.png",
//     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>

参考链接