【原创】用实例分析正则表达式设计过程

前几天在知道上偶然看到一个问题——“如何去除一段HTML字符串中的已知图片名称的图片标签”,一时心血来潮,就使用JS正则表达式简单写了个答案。

1
2
3
4
var domStr = '<div class="bac">测试文字</div><div class="def"><img src="../../../已知图片名称.jpg">这里还可以加点文本做干扰</div>';
var regImg = /<img\s+([\s\S]*?)src=("|')(.*?)已知图片名称\.(jpg|png|gif)(.*?)\2[^>]*>/g;
var rs = domStr.match(regImg);
console.log(rs); // ["<img src="../../../已知图片名称.jpg">"]

首先,解答这个问题,使用正则表达式并不是唯一的选择。只是我觉得这个问题,使用正则表达式来处理,可能更简单一些。刚巧也可以借由这个题目,复习一下正则表达式相关的知识。

正则表达式,计算机科学的一个概念。通常被用来检索、替换那些符合某个模式(规则)的文本。——百科

抛开JS对DOM的处理能力,将题设的HTML字符串当做一个普通的字符串来看待,结合正则表达式的概念描述,这无疑是正则表达式大显身手的最佳场景。原因如下:

  1. 已知图片名称的图片标签,这是一个明确的规则
  2. 正则表达式的检索能力,刚巧满足从一个字目标符串中找出符合1中规则的特征字符串的业务需求
  3. 将目标字符串中的特征字符串替换为空字符串,即满足题设中“去除”的目的

接下来,我就试着一步一步解释一下,前文答案中的正则表达式是如何一步一步写出来的。

首先,网络上有很多非常好的讲解正则表达式的文章,本文就不对基础知识进行赘述了。我们来简单了解下一个图片标签的基本结构:

  • 最简单

    1
    <img src="../../已知图片名称.jpg">
  • 带有系统属性

    1
    <img id="img" src="http://abc.def.ghi/jkl/mno/已知图片名称.png" width=100 height=100 />
  • 带有自定义属性

    1
    <img id="img" class="cls" data-abc="自定义属性的值" src="已知图片名称.gif" alt="Gif动画" />

以上便是HTML中img标签的存在形态,值得注意的是以上示例中的任意属性的位置都是不固定的,可以根据不同的编码规范或书写者的习惯进行随意调换。另外,img标签以<img开头,是一个自闭合的标签(就是不需要以</img>来闭合的标签),通常以/>闭合,而随着HTML标准的升级,现在也支持直接以>闭合。

到此,我们便可以书写出一个大致的匹配图片标签的正则表达式了:

1
var rImg = /<img[待补充]\/?>/;

/是特殊字符需要\/转义处理,?表示出现至多1次。然后,在HTML中标签名与属性之间至少需要一个空格,那么进一步改造前面这个正则表达式:

1
var rImg = /<img\s+[待补充]\/?>/

\s表示空格,+表示至少出现1次。接着,我们知道img标签是通过src属性来加载一个图片文件来进行显示,而题设中的关键部分“已知图片名称”也就包含在该路径中。而该路径有可能包含任意字符,在HTML标准写法中,该路径是由双引号包裹起来的,在一些JS字符串中由于引号嵌套书写的问题也可能写为单引号包裹,而在一些非标准写法中也可以省略引号(该正则忽略该情况)。根据以上分析,我们又可以得出一段正则:

1
var reg = /src=('|")(.*?)已知文件名.jpg\1/;

简单解释一下。第一对小括号内用|表示匹配'",匹配上其中之一即匹配成功,其后没有加范围限制的表达式,说明只匹配一次。第二对小括号表示匹配任意字符出现任一次,但以?结尾,表示惰性匹配——第一次匹配到其后所给表达式的结果及结束,否则直到匹配到最后一次匹配成功的结果才会结束。用图比较好说明:

第一次,不加”?”的时候,匹配直到内容为”GHI”之后的</div>才结束。而第二次,在添加”?”之后,匹配在内容为”DEF”之后的</div>之处就结束了。

另外,前面表达式中最为有意思的莫过于\1这个表达式了。看起来像是对数字1进行转义,而事实上他是匹配在整个表达式中分组索引与该数字相等的分组所匹配到的结果。如上即为匹配('|")匹配到的结果,若('|")匹配到',则\1',若前者为",则\1"

如上图所示,我们还可以直接通过RegExp.$x查看正则表达式中每个分组匹配到的结果。

然而,该部分正则表达式还存在一些不严谨的地方:其一,.匹配除换行符外的任意字符,要精确匹配.需要使用\.;其二,图片名确定,可图片格式不一定,可对常用图片格式做兼容;另外,我们在实际实践过程中有时候为了防止浏览器缓存,可能会给图片链接增加时间戳,如:已知图片名称.jpg?_t=12345677876。所以我们将该部分优化如下:

1
var reg = /src=('|")(.*?)已知文件名\.[jpe?g|png|gif](.*?)\1/;

现在,我们将两部分正则表达式组合一下,结果如下:

1
var rImg = /<img\s+src=('|")(.*?)已知图片名称\.[jpe?g|png|gif](.*?)\1\/?>/;

前面我们说过,HTML标签的属性顺序是不确定的,也就是说在src属性的前后都有可能出现其他属性,且其内容还可以是任意长度的任意字符。我们还是拆成两部分来看,先看看src之前的部分。

首先,匹配任意字符有很多种书写方式:空格+非空格、字符+非字符、点号+换行等等。按照个人情况选用就可以了,此处我们使用空格+非空格

1
var reg = /[\s\S]*/;

同样的,我们也需要考虑惰性问题。然后再结合前面的表达式,则可以得出:

1
var rImg = /<img\s+([\s\S]*?)src=('|")(.*?)已知图片名称\.[jpe?g|png|gif](.*?)\2\/?>/;

注意,加入该组表达式后,原来分组索引为1的单双引号,现在分组索引变为2,我们需要同时修改\1\2,否则会出错。

最后,我们再来看看src属性之后的部分。其实这部分可以归结为src属性结束到整个标签结束为止。我们知道img标签的结束是一个”>”,所以,我们只要发现还没有匹配到”>”便继续往前匹配即可:

1
var reg = /[^>]*>/;

这里补充说明一下,上面的表达式是不严谨的,如果在src属性之后的属性值中出现”>”字符,则会出现异常结果:

所以,使用之前需要先明白是否能满足当前的使用场景,如果不适合,则需要设计其他的表达式来支持。此外,前文中提到的以/>结尾的图片标签中,其实/也属于非>所以以上表达式可以直接包含,至此,我们的答案即将浮出水面。

1
var rImg = /<img\s+([\s\S]*?)src=("|')(.*?)已知图片名称\.(jpe?g|png|gif)(.*?)\2[^>]*>/;

为什么说是即将呢?原因也比较好解释,我们在一段HTML字符串中,有可能多次使用图片标签,使用同一张图片也并非不可能,因此我们还需要给上面的正则表达式加上全局查找功能。最后,终于真相大白:

1
var rImg = /<img\s+([\s\S]*?)src=("|')(.*?)已知图片名称\.(jpe?g|png|gif)(.*?)\2[^>]*>/g;

当然了,基于此我们还可以引申出更多的正则表达式:所有的图片标签、所有包含src属性的图片标签等等等等。

俗话说,“授人以鱼不如授人以渔”本文的目的不仅仅是想要说明如何解答文首提及的问题,更希望能通过对整个正则表达式设计过程的分析,给读到本文的小伙伴带来更多的灵感,帮助大家更好的理解正则表达式,解决更多的问题。