美文网首页
JS正则表达式的骚操作

JS正则表达式的骚操作

作者: 囍冯总囍 | 来源:发表于2020-08-12 15:01 被阅读0次

    参考资料

    《JS正则表达式的分组匹配》
    《正则表达式之捕获组/非捕获组介绍》
    《正则表达式中(?:pattern)、(?=pattern)、(?!pattern)、(?<=pattern)和(?<!pattern)》
    《正则基础之——NFA引擎匹配原理》

    NB的工具网站

    《正则在线测试》
    《正则测试、学习工具》
    《正则测试、学习工具-英文原版》

    背景介绍

    由来

    做Hexo的时候遇到一个问题,因为图片等文件很多已经上传到了腾讯云的COS对象存储,而且COS开启了CDN加速,所以其实每个上传的对象都存在一个对应的CDN加速的域名,比如上传文件的原访问地址为:https://bucketname.cos.ap-beijing.myqcloud.com/res/public/test.jpg,由于Hexo是生成的静态页面,所以我们需要在生成静态页面前将这些静态资源的地址替换为CDN加速的域名地址,比如上面的域名替换为:https://blog.666.top/res/public/test.jpg

    由于Hexo使用的是Markdown语法然后渲染为静态HTML页面,所以我们实际上需要针对渲染完成的静态HTML页面中的特殊标签的特殊内容进行替换。比如说有一个<img src="https://bucketname.cos.ap-beijing.myqcloud.com/res/public/test.jpg"></img>这种情况。

    需求描述

    我们替换不是对所有标签的src进行替换,可能只替换诸如<img><audio><video>这三个标签的src,同时由于src中可能会引入其他外部网站的图片,所以肯定只需要对来自我们特定域名下特定目录下的特定标签中的src进行域名CDNIFY操作。所以我们产生了如下几个必要条件:

    1. HTML文档中的<img><audio><video>标签
    2. 上面这些标签src中的URL的HOST满足bucketname.cos.ap-beijing.myqcloud.com
    3. 这个文件的URL中目录前缀是/res/public/,也就是说/res/private/aaa.jpg这种是不进行替换的

    解决方法当然也有很多种,比如Hexo中有专门的第三方插件库(无法满足要求所以放弃),解析HTML后替换(速度慢)等,但介于无法满足我们的要求,最后还是采用将生成的页面通过正则进行替换,所以也引发了这篇对正则的学习笔记。

    知识积累

    对于正则的使用,一直都是什么时候用什么时候查,用到了再想办法那种。因为觉得脑子根本记不住那些个表达式(蛤蛤蛤蛤)。直到遇到今天这个问题,于是对正则表达式的语法进行了进一步的学习。

    一、JS正则表达式的分组匹配

    • 什么是分组

    通俗来说,我理解的分组就是在正则表达式中用()包起来的内容代表了一个分组,像这样的:

    var reg = /(\d{2})/
    reg.test('12');  //true
    

    这里reg中的(/d{2})就表示一个分组,匹配两位数字

    • 分组内容的的形式

    一个分组中可以像上面这样有一个具体的表达式,这样可以优雅地表达一个重复的字符串

    /hahaha/
    /(ha){3}/
    

    这两个表达式是等效的,但有了分组之后可以更急简洁。
    体格分组中还可以有多个候选表达式,例如

    var reg = /I come from (hunan|hubei|zhejiang)/;
    reg.test('I come from hunan');   //true
    reg.test('I come from hubei');   //true
    

    也就是说在这个分组中,通过|隔开的几个候选表达式是并列的关系,所以可以把这个|理解为或的意思

    • 捕获组

    正则表达式 描述 示例
    (pattern) 匹配pattern并捕获结果,自动设置组号。 (abc)+d匹配abcd或者abcabcd
    (?<name>pattern)

    (?'name'pattern)
    匹配pattern并捕获结果,设置name为组名。 经测试JS中支持<>命名
    \num 对捕获组的反向引用。其中 num 是一个正整数。 (\w)(\w)\2\1 匹配abba
    \k<name>

    \k'name'
    对命名捕获组的反向引用。其中 name 是捕获组名。 (?<group>\w)abc\k<group>匹配xabcx

    使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下,每个捕获组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。

    示例一 明名捕获组:

    const str = 'http://reg-test-server:8080/download/file1.html#'
    const reg = /(?<protocol>\w+):\/\/(?<host>[^/:]+)(?<port>:\d+)?(?<path>[^# :]*)/
    console.log(reg.exec(str))
    

    输出结果:

    [ 'http://reg-test-server:8080/download/file1.html',
      'http',
      'reg-test-server',
      ':8080',
      '/download/file1.html',
      index: 0,
      input: 'http://reg-test-server:8080/download/file1.html#',
      groups: [Object: null prototype] {
        protocol: 'http',
        host: 'reg-test-server',
        port: ':8080',
        path: '/download/file1.html' } ]
    

    可以看到URL中各个分组已经在groups中命名了。如果不命名,则groups为空

    示例二 明名分组的引用:

    const str = 'http://reg-test-server:8080/download/file1.html#'
    const reg = /(?<protocol>\w+):\/\/(?<host>[^/:]+)(?<port>:\d+)?(?<path>[^# :]*)/
    
    console.log('Before', str)
    console.log('After ', str.replace(reg, '$<protocol>://www.baidu.com$<port>$<path>'))
    

    输出结果为:

    Before http://reg-test-server:8080/download/file1.html#
    After  http://www.baidu.com:8080/download/file1.html#
    

    当然上面只是示例了一下明名分组的引用,实际可能无需这么麻烦。

    示例三 反向引用:

    const str1 = 'https://www.baidu.com?method=https'
    const str2 = 'https://www.baidu.com?method=http'
    
    const reg = /(\w+):\/\/[^/:]+\?method=\1/
    
    console.log(reg.test(str1), reg.test(str2)) //true false
    

    首先通过分组(\w+)捕获了https这个协议串,然后在最后的method=\1中通过反向引用,引用了之前捕获的https这个串,所以在验证的时候,method=https返回为true,而method=http则会返回false

    还有个常用的反向引用示例如下:

    var reg = /(\w{3}) is \1/
    reg.test('kid is kid') // true
    reg.test('dik is dik') // true
    reg.test('kid is dik') // false
    reg.test('dik is kid') // false
    

    需要注意的是,如果引用了越界或者不存在的编号的话,就被被解析为普通的表达式

    var reg = /(\w{3}) is \6/;
    reg.test( 'kid is kid' ); // false
    reg.test( 'kid is \6' );  // true
    

    二、正则表达式的非捕获型分组

    字符 描述 示例
                                  (?:pattern)
                            
    匹配pattern,但不捕获匹配结果。 'industr(?:y|ies)' 匹配'industry'或'industries'。
    (?=pattern) 正向肯定预查,匹配pattern前面的位置。不捕获匹配结果。 'Windows(?=95|98|NT|2000)' 匹配 "Windows2000" 中的 "Windows" 不匹配 "Windows3.1" 中的 "Windows"。

    ※ 简单说,以 xxx(?=pattern)为例,就是捕获以pattern结尾的内容xxx
    (?!pattern) 正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。不捕获匹配结果。 'Windows(?!95|98|NT|2000)' 匹配 "Windows3.1" 中的 "Windows" 不匹配 "Windows2000" 中的 "Windows"。

    ※ 简单说,以 xxx(?!pattern)为例,就是捕获不以pattern结尾的内容xxx
    (?<=pattern) 反向肯定预查,与正向肯定预查类似,只是方向相反。不捕获匹配结果。 '2000(?<=Office|Word|Excel)' 匹配 " Office2000" 中的 "2000" 不匹配 "Windows2000" 中的 "2000"。

    ※ 简单说,以(?<=pattern)xxx为例,就是捕获以pattern开头的内容xxx。
    (?<!pattern) 反向否定预查,与正向否定预查类似,只是方向相反。不捕获匹配结果。 '2000(?<!Office|Word|Excel)' 匹配 " Windows2000" 中的 "2000" 不匹配 " Office2000" 中的 "2000"。

    ※ 简单说,以(?<!pattern)xxx为例,就是捕获不以pattern开头的内容xxx。

    非捕获组只匹配结果,但不捕获结果,也不会分配组号,当然也不能在表达式和程序中做进一步处理。
    首先(?:pattern)(pattern)不同之处只是在于不捕获结果。
    接下来的四个非捕获组用于匹配pattern(或者不匹配pattern)位置之前(或之后)的内容。匹配的结果不包括pattern。

    应用举例

    (?<=<(\w+)>).*(?=<\/\1>)匹配不包含属性的简单HTML标签内的内容。如:<div>hello</div>之中的hello,匹配结果不包括前缀<div>和后缀</div>。
    下面是程序中非捕获组的示例,用来提取数字。 可以看到反向回查和反向预查都没有被捕获。

    const str = '有下面几组数字:010001,100,21000,4100011,510002,310000,把6位数且开头不是0的数挑出来。'
    const reg1 = /([1-9]\d{5})/g
    console.log(str.match(reg1)) // [ '410001', '510002', '310000' ]
    
    const reg2 = /(?<!\d)([1-9]\d{5})/g
    console.log(str.match(reg2)) // [ '410001', '510002', '310000' ]
    
    const reg3 = /(?<!\d)([1-9]\d{5})(?!\d)/g
    console.log(str.match(reg3)) // [ '510002', '310000' ]
    

    我们看到,只有第三个正则才是输出的正确的结果。首先我们捋一下,什么样的数字如何我们的要求呢?

    1. 开头不为1
    2. 连续的6个数字
    3. 第一个数字前面不是数字
    4. 最后一个数字后面不是数字

    首先中间的分组([1-9]\d{5})实现了开头不是0且6位数的作用,满足了1,2两条要求,但是对于4100011这个数字,也都符合要求但是是7位的。
    然后我们通过(?<!\d)这个表达式,实现了反向否定预查,就是说我们要捕获的内容不以数字开头,从而满足了第3条要求。
    最后我们通过(?!\d)这个表达式,实现了正向否定预查,就是说我们要捕获的内容不以数字结尾,从而剔除了4100011这个数字,实现了我们最终的要求。

    问题解决

    好啦,前面补充了这么多营(ji)养(chu)快(zhi)线(shi),我们回到本次的主题。怎么进行有效的替换呢?同样先来捋一捋,我们需要满足什么要求:

    1. 只替换HTML文档中的<img><audio><video>标签,其他标签不做处理
    2. 满足上面条件下,标签的SRC中,域名是bucketname.cos.ap-beijing.myqcloud.com
    3. 满足上面条件下,标签的SRC中,路径是/res/public/开头的
    4. 满足上面条件下,保留原来的http或者https协议

    当然上面的这个条件并不一定满足所有的场景,因为我们实现可以知道对于这三种标签,没有什么复杂的嵌套关系,所以说根据实际的场景制定了上面的几个约束条件。那么我们就来看看该如何来制定正则呢:
    /((\<(img|video|audio)+.*\s+src\=.*\/\/)|(\!\[.*\]\(.*\/\/))bucketname.cos.ap-beijing.myqcloud.com(?=\/res\/public\/)/gm

    让我们一起来解释一下上面的这个正则:

    1. (\<(img|video|audio)+.*\s+src\=.*\/\/)这是第一个捕获分组,用于捕获HTML语法中的指定标签
      • \<(img|video|audio)+,这个捕获分组规定了匹配那三类标签的开头,如<img字符
      • .*\s+src\=.*\/\/,这部分用来匹配<img ..... src="http://这部分,当然img和src中间可能存在其他属性定义,一并捕获,例如:<img width="100%" alt="aae" src="http://
      • 我们通过\s+src\=来进一步限制捕获的src=前面必须有至少一个空格
    2. |(\!\[.*\]\(.*\/\/))这是第二个捕获分组,中间用了|来分割,用来捕获MARKDOWN语法中的图片标签,如![图片描述](http://
    3. 前面两个捕获分组合并成一个大的捕获分组:((\<(img|video|audio)+.*\s(src)\=.*\/\/)|(\!\[.*\]\(.*\/\/)),至此实现了指定域名下指定标签对象的内容的前部分的捕获,但是条件3要求我们只替换/res/public/路径下的内容,所以我们在后面加入了(?=\/res\/public\/)这个正向肯定查询分组
      • (?=\/res\/public\/) 正如上面所说,它只会限制捕获以/res/public/结尾的字符串而不会将其捕获,为何呢?因为我们这里不需要将其捕获
    4. 最后我们通过/gm模式,启用全局和多行模式。

    搞定上面的正则表达式后,我们来考虑替换的问题。替换自然用的是JS中的replace函数,直接上代码:

    const reg = /((\<(img|video|audio)+.*\s+src\=.*\/\/)|(\!\[.*\]\(.*\/\/))bucketname.cos.ap-beijing.myqcloud.com(?=\/res\/public\/)/gm
    
    const str = `
    <img src="http://bucketname.cos.ap-beijing.myqcloud.com/res/public/test.jpg" />
    <img width="100%" height="100%" alt="上海鲜花港 - 郁金香" src="https://bucketname.cos.ap-beijing.myqcloud.com/res/public/test.jpg" />
    <img width="100%" height="100%" alt="上海鲜花港 - 郁金香"
         src="http://bucketname.cos.ap-beijing.myqcloud.com/res/public/test.jpg" />
    ![上海鲜花港 - 郁金香](https://bucketname.cos.ap-beijing.myqcloud.com/res/public/markdown.md)
    
    <img src="http://bucketname.cos.ap-beijing.myqcloud.com/public/test.jpg" width=100% height=100% alt="上海鲜花港 - 郁金香"/>
    <img alt="上海鲜花港 - 郁金香" src="http://bucketname.cos.ap-beijing.myqcloud.com/res/private/test.jpg" width=100% height=100% />
    `
    
    console.log(str.replace(reg, '$1blog.666.top'))
    

    输出结果:

    <img src="http://blog.666.top/res/public/test.jpg" />
    <img width="100%" height="100%" alt="上海鲜花港 - 郁金香" src="https://blog.666.top/res/public/test.jpg" />
    <img width="100%" height="100%" alt="上海鲜花港 - 郁金香"
         src="http://blog.666.top/res/public/test.jpg" />
    ![上海鲜花港 - 郁金香](https://blog.666.top/res/public/markdown.md)
    
    <img src="http://bucketname.cos.ap-beijing.myqcloud.com/public/test.jpg" width=100% height=100% alt="上海鲜花港 - 郁金香"/>
    <img alt="上海鲜花港 - 郁金香" src="http://bucketname.cos.ap-beijing.myqcloud.com/res/private/test.jpg" width=100% height=100% />
    

    由此,之前提出的问题就已经解决啦!但是如果要匹配的内容如下:

    <img src="http://bucketname.cos.ap-beijing.myqcloud.com/res/public/test.jpg" />
    <img width="100%" height="100%"
         alt="上海鲜花港 - 郁金香"
         src="http://bucketname.cos.ap-beijing.myqcloud.com/res/public/test.jpg" />
    

    运用上面的正则是无法匹配成功的,这个问题留给你自己探索吧!

    相关文章

      网友评论

          本文标题:JS正则表达式的骚操作

          本文链接:https://www.haomeiwen.com/subject/vvafdktx.html