JS正则表达式的骚操作
参考资料
《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操作。所以我们产生了如下几个必要条件:
- HTML文档中的
<img>
、<audio>
、<video>
标签 - 上面这些标签
src
中的URL的HOST
满足bucketname.cos.ap-beijing.myqcloud.com
- 这个文件的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
- 连续的6个数字
- 第一个数字前面不是数字
- 最后一个数字后面不是数字
首先中间的分组([1-9]\d{5})
实现了开头不是0且6位数的作用,满足了1,2两条要求,但是对于4100011
这个数字,也都符合要求但是是7位的。
然后我们通过(?<!\d)
这个表达式,实现了反向否定预查,就是说我们要捕获的内容不以数字开头,从而满足了第3条要求。
最后我们通过(?!\d)
这个表达式,实现了正向否定预查,就是说我们要捕获的内容不以数字结尾,从而剔除了4100011
这个数字,实现了我们最终的要求。
问题解决
好啦,前面补充了这么多营(ji)养(chu)快(zhi)线(shi),我们回到本次的主题。怎么进行有效的替换呢?同样先来捋一捋,我们需要满足什么要求:
- 只替换HTML文档中的
<img>
、<audio>
、<video>
标签,其他标签不做处理 - 满足上面条件下,标签的SRC中,域名是
bucketname.cos.ap-beijing.myqcloud.com
的 - 满足上面条件下,标签的SRC中,路径是
/res/public/
开头的 - 满足上面条件下,保留原来的
http
或者https
协议
当然上面的这个条件并不一定满足所有的场景,因为我们实现可以知道对于这三种标签,没有什么复杂的嵌套关系,所以说根据实际的场景制定了上面的几个约束条件。那么我们就来看看该如何来制定正则呢:
/((\<(img|video|audio)+.*\s+src\=.*\/\/)|(\!\[.*\]\(.*\/\/))bucketname.cos.ap-beijing.myqcloud.com(?=\/res\/public\/)/gm
让我们一起来解释一下上面的这个正则:
-
(\<(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=
前面必须有至少一个空格
-
-
|(\!\[.*\]\(.*\/\/))
这是第二个捕获分组,中间用了|
来分割,用来捕获MARKDOWN语法中的图片标签,如![图片描述](http://
- 前面两个捕获分组合并成一个大的捕获分组:
((\<(img|video|audio)+.*\s(src)\=.*\/\/)|(\!\[.*\]\(.*\/\/))
,至此实现了指定域名下指定标签对象的内容的前部分的捕获,但是条件3
要求我们只替换/res/public/
路径下的内容,所以我们在后面加入了(?=\/res\/public\/)
这个正向肯定查询分组-
(?=\/res\/public\/)
正如上面所说,它只会限制捕获以/res/public/
结尾的字符串而不会将其捕获,为何呢?因为我们这里不需要将其捕获
-
- 最后我们通过
/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" />
运用上面的正则是无法匹配成功的,这个问题留给你自己探索吧!