jawil/blog

JavaScript正则进阶之路——活学妙用奇淫正则表达式

jawil opened this issue · 36 comments

jawil commented

原文收录在我的 GitHub博客 (https://github.com/jawil/blog) ,喜欢的可以关注最新动态,大家一起多交流学习,共同进步,以学习者的身份写博客,记录点滴。

有些童鞋肯定有所疑惑,花了大量时间学习正则表达式,却发现没有用武之地,正则不就是验证个邮箱嘛,其他地方基本用不上,其实,大部分人都是这种感觉,所以有些人干脆不学,觉得又难又没多大用处。殊不知,想要成为编程大牛,正则表达式必须玩转,GitHub上优秀的开源库和框架里面到处都是强大的正则匹配,当年jQuery作者也被称为正则小王子。这里分享一些工作中用到的和自己收集的一些正则表达式的妙用,到处闪耀着开发者智慧的火花。

实现一个需求的方法很多种,哪种更好,仁者见仁智者见智,这里只提供一种对比的思维来激发大家学习正则的兴趣和养成活用正则的思维。

作为前端开发人员,总会有点自己的奇技淫巧,毕竟前端开发不同于后端,代码全部暴漏给用户不说,代码冗余了少则影响带宽,多则效率降低。正则表达式(Regular Expression),这是一块硬骨头,很难啃,但是啃着又很香。所以今天我也来爆一些正则表达式的奇技淫巧。

正则大法好,正则大法好,正则大法好,重要的事情说三遍。

1、获取链接 https://www.baidu.com?name=jawil&age=23 name的value值

非正则实现:

function getParamName(attr) {

  let search = window.location.search // "?name=jawil&age=23"

  let param_str = search.split('?')[1] // "name=jawil&age=23"

  let param_arr = param_str.split('&') // ["name=jawil", "age=23"]

  let filter_arr = param_arr.filter(ele => { // ["name=jawil"]
    return ele.split('=')[0] === attr
  })

  return decodeURIComponent(filter_arr[0].split('=')[1])
}

console.log(getParamName('name')) // "jawil"

用正则实现:

function getParamName(attr) {

  let match = RegExp(`[?&]${attr}=([^&]*)`) //分组运算符是为了把结果存到exec函数返回的结果里
    .exec(window.location.search)
  //["?name=jawil", "jawil", index: 0, input: "?name=jawil&age=23"]
  return match && decodeURIComponent(match[1].replace(/\+/g, ' ')) // url中+号表示空格,要替换掉
}
  
console.log(getParamName('name'))  // "jawil"

看不太懂先学习一下这篇文章:[ JS 进阶 ] test, exec, match, replace

2、 数字格式化问题,1234567890 --> 1,234,567,890

非正则实现:

let test = '1234567890'

function formatCash(str) {
  let arr = []

  for (let i = 1; i < str.length; i++) {
    if (str.length % 3 && i == 1)
      arr.push(str.substr(0, str.length % 3))

    if (i % 3 === 0)
      arr.push(str.substr(i - 2, 3))

  }

  return arr.join(',')
}

console.log(formatCash(test)) // 1,234,567,890

用正则实现:

let test1 = '1234567890'
let format = test1.replace(/\B(?=(\d{3})+(?!\d))/g, ',')

console.log(format) // 1,234,567,890

下面简单分析下正则/\B(?=(\d{3})+(?!\d))/g

  1. /\B(?=(\d{3})+(?!\d))/g:正则匹配边界\B,边界后面必须跟着(\d{3})+(?!\d);
  2. (\d{3})+:必须是1个或多个的3个连续数字;
  3. (?!\d):第2步中的3个数字不允许后面跟着数字;
  4. (\d{3})+(?!\d):所以匹配的边界后面必须跟着3*n(n>=1)的数字。

最终把匹配到的所有边界换成,即可达成目标。

3、去掉字符串左右两边的空格," jaw il " --> “jaw il”

非正则实现:

function trim(str) {
    let start, end
    for (let i = 0; i < str.length; i++) {
        if (str[i] !== ' ') {
            start = i
            break
        }
    }
    for (let i = str.length - 1; i > 0; i--) {
        if (str[i] !== ' ') {
            end = i
            break
        }
    }

    return str.substring(start, end + 1)
}


let str = "  jaw il "
console.log(trim(str)) // "jaw il"

用正则实现:

function trim(str) {
    return str.replace(/(^\s*)|(\s*$)/g, "")
}

let str = "  jaw il "
console.log(trim(str)) // "jaw il"

4、判断一个数是否是质数 3 --> true

质数又称素数。指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。

非正则实现:

function isPrime(num){
    // 不是数字或者数字小于2
    if(typeof num !== "number" || !Number.isInteger(num)){      
    // Number.isInterget 判断是否为整数
        return false
    }

    //2是质数
    if(num == 2){
        return true
    }else if(num % 2 == 0){  //排除偶数
        return false
    }
    //依次判断是否能被奇数整除,最大循环为数值的开方
    let squareRoot = Math.sqrt(num)
    //因为2已经验证过,所以从3开始;且已经排除偶数,所以每次加2
    for(let i = 3; i <= squareRoot; i += 2) {
      if (num % i === 0) {
         return false
      }
    }
    return true
}

console.log(isPrime(19)) // true

用正则实现:

function isPrime(num) {
return !/^1?$|^(11+?)\1+$/.test(Array(num+1).join('1'))
}

console.log(isPrime(19)) // true

要使用这个正规则表达式,你需要把自然数转成多个1的字符串,如:2 要写成 “11”, 3 要写成 “111”, 17 要写成“11111111111111111”,这种工作使用一些脚本语言可以轻松的完成,JS实现也很简单,我用Array(num+1).join('1')这种方式实现了一下。

一开始我对这个表达式持怀疑态度,但仔细研究了一下这个表达式,发现是非常合理的,下面,让我带你来细细剖析一下是这个表达式的工作原理。

首先,我们看到这个表达式中有“|”,也就是说这个表达式可以分成两个部分:/^1?$//^(11+?)\1+$/

  • 第一部分:/^1?$/, 这个部分相信不用我多说了,其表示匹配“空串”以及字串中只有一个“1”的字符串。
  • 第二部分:/^(11+?)\1+$/ ,这个部分是整个表达式的关键部分。其可以分成两个部分,(11+?) 和 \1+$ ,前半部很简单了,匹配以“11”开头的并重复0或n个1的字符串,后面的部分意思是把前半部分作为一个字串去匹配还剩下的字符串1次或多次(这句话的意思是——剩余的字串的1的个数要是前面字串1个数的整数倍)。

可见这个正规则表达式是取非素数,要得到素数还得要对整个表达式求反。通过上面的分析,我们知道,第二部分是最重要的,对于第二部分,举几个例子,

示例一:判断自然数8。我们可以知道,8转成我们的格式就是“11111111”,对于 (11+?) ,其匹配了“11”,于是还剩下“111111”,而 \1+$ 正好匹配了剩下的“111111”,因为,“11”这个模式在“111111”出现了三次,符合模式匹配,返回true。所以,匹配成功,于是这个数不是质数。

示例二:判断自然数11。转成我们需要的格式是“11111111111”(11个1),对于 (11+?) ,其匹配了“11”(前两个1),还剩下“111111111”(九个1),而 \1+$ 无法为“11”匹配那“九个1”,因为“11”这个模式并没有在“九个1”这个串中正好出现N次。于是,我们的正则表达式引擎会尝试下一种方法,先匹配“111”(前三个1),然后把“111”作为模式去匹配剩下的“11111111”(八个1),很明显,那“八个1”并没有匹配“三个1”多次。所以,引擎会继续向下尝试……直至尝试所有可能都无法匹配成功。所以11是素数。

通过示例二,我们可以得到这样的等价数算算法,正则表达式会匹配这若干个1中有没有出现“二个1”的整数倍,“三个1”的整数倍,“四个1”的整数倍……,而,这正好是我们需要的算素数的算法。现在大家明白了吧。

5、字符串数组去重 ["a","b","c","a","b","c"] --> ["a","b","c"]

这里只考虑最简单字符串的数组去重,暂不考虑,对象,函数,NaN等情况,这种用正则实现起来就吃力不讨好了。

非正则实现:

①ES6实现

let str_arr=["a","b","c","a","b","c"]

function unique(arr){
  return [...new Set(arr)]
}

console.log(unique(str_arr)) // ["a","b","c"]

②ES5实现

var str_arr = ["a", "b", "c", "a", "b", "c"]

function unique(arr) {
    return arr.filter(function(ele, index, array) {
        return array.indexOf(ele) === index
    })
}

console.log(unique(str_arr)) // ["a","b","c"]

③ES3实现

var str_arr = ["a", "b", "c", "a", "b", "c"]

function unique(arr) {
    var obj = {},
        array = []

    for (var i = 0, len = arr.length; i < len; i++) {
        var key = arr[i] + typeof arr[i]
        if (!obj[key]) {
            obj[key] = true
            array.push(arr[i])
        }
    }
    return array
}

console.log(unique(str_arr)) // ["a","b","c"]

额,ES4呢。。。对不起,由于历史原因,ES4改动太大,所以被废弃了。
可以看到从ES3到ES6,代码越来越简洁,JavaScript也越来越强大。

用正则实现:

var str_arr = ["a", "b", "c", "a", "b", "c"]

function unique(arr) {
    return arr.sort().join(",,").
    replace(/(,|^)([^,]+)(,,\2)+(,|$)/g, "$1$2$4").
    replace(/,,+/g, ",").
    replace(/,$/, "").
    split(",")
}

console.log(unique(str_arr)) // ["a","b","c"]

这里我只是抛砖引玉的利用几个例子对比来展现正则表达式的强大,其实正则表达式的应用远远不止这些,这里列出的只是冰山一角,更多的奇淫技巧需要你们来创造,知识点API是有限的,技巧和创造却是无限的,欢迎大家开动脑门,创造或分享自己的奇淫技巧。

学习正则

如果还没有系统学习正则表达式,这里提供一些网上经典的教程供大家学习。

正则表达式(Regular Expression),这是一块硬骨头,很难啃,但是啃着又很香。

正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。很多地方我们都需要使用正则,所以今天就将一些优秀的教程,工具总结起来。

基本内容

https://en.wikipedia.org/wiki/Regular_expression 了解一样东西,当然先从WIKI开始最好了。

// Regular Expression examples
I had a \S+ day today
[A-Za-z0-9\-_]{3,16}
\d\d\d\d-\d\d-\d\d
v(\d+)(\.\d+)*
TotalMessages="(.*?)"
<[^<>]>

教程

http://deerchao.net/tutorials/regex/regex.htm 30分钟入门教程,网上流传甚广
https://qntm.org/files/re/re.html 55分钟教程【英文】,
http://regex.learncodethehardway.org/book/ 一本简单的书,每一节就是一块内容
https://swtch.com/~rsc/regexp/regexp1.html 正则匹配原理解析
http://stackoverflow.com/tags/regex/info stackoverflow 正则标签,标签下有值得点击的链接,一些典型的问题
http://regexr.com/ 正则学习测试于一身
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions MDN出品,JavaScript方面内容

验证与测试

https://regex101.com/ in JavaScript, Python, PCRE 16-bit, generates explanation of pattern
https://www.debuggex.com/ 正则验证测试,清晰明了
https://mengzhuo.org/regex/ 中文版正则验证测试
http://refiddle.com/ 测试工具
http://myregexp.com/ 也是测试工具,都可以试一试

闯关模式实践

http://regex.alf.nu 闯关模式练习正则表达式,完成一个个正则匹配的测验
http://regexone.com/ 通过实际练习掌握正则表达式
https://regexcrossword.com/ 正则挑战,有不同难度,很丰富
http://callumacrae.github.io/regex-tuesday/ 正则挑战,完成正则匹配要求

其它

https://msdn.microsoft.com/zh-cn/library/az24scfc.aspx MSDN 微软出品
http://www.jb51.net/tools/regex.htm 常用正则表达式,如匹配网址、日期啊这种,这个谷歌一搜很多的
https://www.cheatography.com/davechild/cheat-sheets/regular-expressions/ 速查表地址,如下图

jawil commented

欢迎分享奇淫技巧,送1024邀请码。

460df166b05db38d 2017-05-29 12:21:30

未使用 邀请
abae4264abff93b1 2017-05-29 12:21:30

未使用 邀请

有缘人得。

w550 commented

@jawil
邀請碼不存在或已被使用,您無法注冊!

jawil commented

我放出去十分钟之内就被人抢注了,一个邀请码只能注册一次。。。@w550

我学正则学的都有心理阴影了……话说,这篇文章为什么加了“玄学”的标签?

jawil commented

正则就是玄学,很少人愿意去深入研究的知识我觉得都是玄学😄

52cik commented

https://github.com/52cik/regex-tuesday
正则表达式周二挑战赛 汉化版
之前想搞,结果半途而废了。。欢迎一起折腾。

jawil commented

感觉这种题已经超出能力的范围了,瞎折腾一下还可以,正则表达式匹配回文字符串那个也太那个吧,毫无思路,用js到是很简单😄

flycj commented

还有 邀请码吗? 老司机

2、 数字格式化问题的 for (let i = 1; i < str.length; i--) , 应该是 for (let i = 1; i < str.length; i++) 吧。
而且当字符串长度为1时,返回空。

jawil commented

恩,改错了,之前下面那个应该是--,结果改成上面的了。。。

@fengyun2 已修正

这里提到的判断质数的算法是非常非常慢的——它比最 naïve 的 sqrt(N) 还要慢很多很多。

另外数组去重的算法是错误的,例如字符串以 ,, 为子串的情况。一个实践中可用的改造是用新生成的 GUID 做分隔符,一个完全正确的改造是选择一个比所有字符串串接起来还要长的、border 长度大于任何已经存在字符串或者是 0 的字符串做分隔符。

jawil commented

首先感谢反馈和意见。
但是对于你说的判断质数的算法是非常非常慢的,在下却不敢苟同。

就上面非正则和正则实现的两种方式的性能我们就事论事,用数据说话,考虑到浏览器控制台的诸多沙箱机制以及安全性等等额外不必要因素,这里测试环境我们选择为Node v6.10.2

先看一个比较小的数字:19,这里贴一下代码:

console.time('循环非正则')
function isPrime(num){
    if(typeof num !== "number" || !Number.isInteger(num)){      
        return false
    }
    if(num == 2){
        return true
    }else if(num % 2 == 0){
        return false
    }
    let squareRoot = Math.sqrt(num)
    for(let i = 3; i <= squareRoot; i += 2) {
      if (num % i === 0) {
         return false
      }
    }
    return true
}
console.log(isPrime(19))
console.timeEnd('循环非正则')

console.time('正则')
function isPrime(num) {
return !/^1?$|^(11+?)\1+$/.test(Array(num+1).join('1'))
}
console.log(isPrime(19))
console.timeEnd('正则')

输出结果:

看到结果了吧,再来看看稍微大点情况下各自的时间,在数组允许的长度下,这里取123456。

输出结果:

不要以为正则就一定很慢,什么事情都是相对的,最好是用数据说话,哪怕测试的方式不太对,口说无凭,没有足够信服力。

数组去重那块肯定是不完善的,我也说了,用正则属于吃力不讨好,所以最后一个就为了对比,正则也有它不好用的场景,反正二者各有千秋,没有孰好孰坏,合适的就是最好的。

@GeeLaw

@jawil 请您学习一下计算机科学。

您首先要意识到的是,判断质数属于 P,用正则表达式的方法是 指数级别 的算法,即使不用那个 高明 的 P 算法,也可以用 sqrt(N) 的方法,同样比正则表达式具有更优的 渐近时间复杂度

其次,您的实现是不公平的,正则表达式版本缺少对 num 的检验,循环的版本里面开方会面临浮点误差的问题。对于开方的问题,一种常见的实现是换成 i*i<=x(实践上性能也更好),另一种是把开方结果四舍五入。

最后,您测量时间的 方法是错误的,因为:

  • 您忘了考虑第一次 JIT 时间混进来;
  • 您并没有测试多次取平均数(当然这里也要小心编译器发现这些函数是纯粹的,从而优化掉);
  • 您的数据很糟糕——123456 具有一个非常非常小的质因子。

我重新写了一份代码。先贴结果,我的环境是 NodeJS 6.10.3,多次运行结果接近这种排序:

isPrimeSqrtN(19): 0.154ms
isPrimeRegex(19): 0.161ms
isPrimeSqrtN(123456): 0.460ms
isPrimeRegex(123456): 2.868ms
isPrimeSqrtN(347*347): 0.581ms
isPrimeRegex(347*347): 30.239ms

测试的命令是 node <file.js> 2> nul,测试的代码是(347 是一个质数):

function isPrimeSqrtN(x)
{
    if (x < 2) return false;
    for (var i = 2; i * i <= x; ++i)
        if (x % i == 0)
            return false;
    return true;
}
function isPrimeRegex(x)
{
    return !/^1?$|^(11+?)\1+$/.test(Array(x+1).join('1'));
}

console.error(isPrimeSqrtN(9));
console.error(isPrimeSqrtN(7));
console.error(isPrimeRegex(9));
console.error(isPrimeRegex(7));

console.time('isPrimeSqrtN(19)');
console.error(isPrimeSqrtN(19));
console.timeEnd('isPrimeSqrtN(19)');
console.time('isPrimeRegex(19)');
console.error(isPrimeRegex(19));
console.timeEnd('isPrimeRegex(19)');

console.time('isPrimeSqrtN(123456)');
console.error(isPrimeSqrtN(123456));
console.timeEnd('isPrimeSqrtN(123456)');
console.time('isPrimeRegex(123456)');
console.error(isPrimeRegex(123456));
console.timeEnd('isPrimeRegex(123456)');

console.time('isPrimeSqrtN(347*347)');
console.error(isPrimeSqrtN(347*347));
console.timeEnd('isPrimeSqrtN(347*347)');
console.time('isPrimeRegex(347*347)');
console.error(isPrimeRegex(347*347));
console.timeEnd('isPrimeRegex(347*347)');

本 issue 里面我的代码都是 MIT 授权,但加入一个额外条款:分发的时候必须同时链接到 我的这篇 blog

jawil commented

@GeeLaw 受教了,我测试的方式不对,正则效率确实慢一些,对于算法这门计算机科学的学习也还有很长的路要走。

wunci commented

3、去掉字符串左右两边的空格," jaw il " --> “jaw il”

应该是

return str.substring(start, end+ 1) 
jawil commented

感谢指出,已更正@wclimb

感觉挺有意思的。
数字格式化问题,1234567890 --> 1,234,567,890
第一种利用for循环的函数formatCash不够通用。当传入“10234567890”,结果是:10,023,456,789
需要改两处。
function formatCash(str) {
let arr = [];
for (let i = 1; i <= str.length; i++) { //这里应该是<=
if (str.length % 3 && i == 1)
arr.push(str.substr(0, str.length % 3))
if (i % 3 === 0)
arr.push(str.substr(i - 3 + str.length%3, 3)) //这里应该是i-3+str.length%3
}
return arr.join(',')
}

@jsdchenye 两种实现结果都是一样的吧?

@jawil 我也实现了一下:

 function formatCash(str) {
       return str.split('').reverse().reduce((prev, next, index) => {
            return ((index % 3) ? next : (next + ',')) + prev
       })
}

console.log(formatCash('1234567890')) // 1,234,567,890

@mqyqingfeng
文章中利用for循环的函数formatCash处理“10234567890”会出错,显示:10,023,456,789

jawil commented

@mqyqingfeng 感谢回答,这个非常巧妙
@jsdchenye 感谢提醒反馈,确实逻辑没考虑细致,出现纰漏了

@jsdchenye 原来是多了个 0,我还以为是逗号的位置不对……😂

看着看着怎么突然就开车了。。。

用正则进行数组去重也可以这样,只适用字符串数组

["a","b","c","a","b","c"].sort().join("").replace(/(\w)\1*/g, "$1").split("")

好像在知乎还是哪里看到的,对于问题二: 数字格式化问题,1234567890 --> 1,234,567,890,
其实可以这样做:
var num=1234567890;
num.toLocaleString('en-us')
当然还可以更直接:1234567890..toLocaleString('en-us')

function format(str){
let arr=[];
let len=str.length;
for(let i=1;i<=len;i++){
if(str.length % 3 && i==1)
arr.push( str.substr(0,len % 3) );
if ( i % 3 === 0 ){
arr.push(str.substr(i-(3-len % 3),3));
}
}
return arr.join(',');
}
console.log(format('12345678'));
console.log(format('1234567');

huyoo commented

求指点,jquery里有这种代码(?:\d*.|),这里把或放最后是进行什么操作,可以为空吗,我试了好久都找到窍门

@huyoo ?: 表示非捕获性分组,这个不影响匹配结果,我们忽略掉这个表达式就变成 ( \d*\.|),其中 \d* 表示匹配任意个数字,\. 表示匹配一个 . 号,最后跟一个 | 表示可以什么都不匹配,这个正则表达式后面应该再拼接一个 \d+,变成 (?:\d*.|)\d+,表示匹配 1.1 或者 1

var str1 = "1.9";
var str2 = "1";
var patt1 =  /(\d*\.|)\d/

console.log(patt1.test(str1)) // true
console.log(patt1.test(str2)) // true
huyoo commented

@mqyqingfeng 原来如此,感谢指点

文章吸引人,标题很重要。正则表达式小白前来膜拜学习

gnehs commented

3、去掉字符串左右两边的空格," jaw il " --> “jaw il”

" test ".trim() //test

// 获取URL的查询参数, 我见过最精简
q={};location.search.replace(/([^?&=]+)=([^&]+)/g,(_,k,v)=>q[k]=v);q;

关于数组去重,我找到一种更简单的思路:

["a", "b", "c", "a", "b", "c"]
  .sort()
  .join()
  .replace(/(\w),\1/g, '$1')
  .split(',')
// ["a", "b", "c"]

关于数组去重,我找到一种更简单的思路:

["a", "b", "c", "a", "b", "c"]
  .sort()
  .join()
  .replace(/(\w),\1/g, '$1')
  .split(',')
// ["a", "b", "c"]

这种只适用于重复两个,考虑多个需要完善一下
/(\w)(,\1)+/g

"ab,ab,ab,k,k,k,lf,lf,lf,lf,kjf,kjf,kjf".replace(/(^|\b)([^,]+)(,\2)+(,|$)/g, "$1$2$4")

"ab,ab,ab,k,k,k,lf,lf,lf,lf,kjf,kjf,kjf".replace(/\b([^,]+)(,\1)+\b/g, "$1")