WebGL Tutorial
and more

正则表达式应用技巧

撰写时间:2024-02-29

修订时间:2024-03-07

思维的养成

学习、应用正则表达式,应养成以字符为基本单位的思维习惯。先匹配哪个字符,紧接着是哪类字符,再后面是什么字符,最后的字符是什么?

我们不能一看满眼都只是字符串,然后想:我要匹配这个字符串。正确的想法应是:我要匹配一系列的哪些字符?我们要真正做到:眼中无她,却有力加。

我们不生产水,我们只是大自然水的搬运工。我们不生产字符,我们只是字符的搬运工。

字符的来源在哪里?匹配、提取、替换后要应用到哪些方面?我是否已经清楚为什么要转义、哪些地方需要转义、在那个地方应如何正确地转义?

如果不注重学习转义的知识,很难当好字符的搬运工。

注意所捕获的值是否空值

捕获一定会在结果中生成一个新的数组元素,但该数组元素的值可能是undefined

const src = `Mike and Tom are friends.`; let re = /(Mike)|(Tom)/g;

因有两次匹配,因此有两个匹配结果。而又因为有两个捕获组,故每次匹配结果数组的长度都为3

在第一个匹配结果数组中,其数组元素分别为:

0: "Mike" 1: "Mike" 2: undefined

而在第二个匹配结果数组中,其数组元素分别为:

0: "Tom" 1: undefined 2: "Tom"

这种情况发生在将捕获组运用于逻辑或的场合。此时,应注意判断所捕获的数值是否undefined

转换思路,曲线救国

设有以下字符串,我们准备将其显示到HTML网页上。

One

two

其中:

One

是需要按HTML输出的代码,而其他部分不是。因此我们需要对其他部分进行转义,结果应为:

<p>One</p><p>Two</p>

此时,我们发现若要通过匹配span的前后特征而保留

One

的代码不动比较困难。

我们可以换个思路,将要保护的span代码部分先提取并留存起来,例如,在一个数组中留存。然后,记下其在数组中保存的索引位置,再在文本输入源中将这一部分替换为诸如___PH0___的值。PH代表place holder0代表所存储数组中的索引值。如:

___PH0___

two

使用较长的占位符名称的原因是不易与文本输入源中的其他文本发生冲突。

若出现其他相应的span代码,则重复上面的步骤:提取、留存、记下索引位置、替换为有索引位置的占位符。

现在,观察上面的代码,我们要保护的代码已经被替换为占位符___PH0___,它其实就是一个指向留存数组中相应元素的指针,因此,我们已经可以放心地下手了。将文本输入源的HTML代码进行转义。最后,将转义后的文本输入源中的各个占位符再次替换回数组中的相应元素值。

可编写一个函数来完成此任务:

function escapeHTMLExcept(src, re) { let placeHolders = []; let temp = src.replace(re, (matched) => { // extracted placeHolders.push(matched); // save let index = placeHolders.length - 1; // retrieve index return `___PH${index}___`; // replace }); // escape temp = temp.replace(/</g, '&lt;').replace(/>/g, '&gt;'); // restore temp = temp.replace(/___PH(\d+)___/g, (matched, p1) => { return placeHolders[p1]; }); return temp; }

先后4次调用了String类的replace方法而实现目的。与正则表达式相互配合使用,replace方法可充分发挥代码简洁而功能强大的作用。当然,这里也离不开回调机制及lambda表达式的贡献。

现在,客户端如此调用:

let src = `

One

two

`; let re = /.+/g; let result = escapeHTMLExcept(src, re); console.log(result);

变量src是需要从HTML转义为替代字符的部分,而变量re则是其匹配结果将得到妥善保护的正则表达式。

结果为:

&lt;p&gt;One&lt;/p&gt;&lt;p&gt;Two&lt;/p&gt;

相应的span代码得以保留而未转义,而其他的内容已全部转换为普通的文本。将此代码显示在HTML网页上,如果我们已对span.matched设置了例如加亮的样式,则该部分就会加亮。

打包replace方法中回调函数参数

Stringreplace方法,可接受一个回调函数。该函数的参数列表如下:

1. matched 2. p1, p2, ..., pN 3. offset 4. src 5. namedGroupsObj

其中,第2行的可变长参数对应的是捕获组的数量,而最后一个参数对应的是命名捕获组。正由于第2行为可变长参数,且又置于各个参数列表的中间,因此,很难通过...args参数取余rest parameters)的方式来确定所排列的参数中各自对应哪个部分。

为解决此问题,我们可设计一个通用的参数打包的函数如下:

function packReplacerArgs(args) { let matched = args[0]; let namedGroupsObj; let src; let offset; let lastItems = 0; if (typeof args.at(-1) === 'object') { namedGroupsObj = args.at(-1); src = args.at(-2); offset = args.at(-3); lastItems = 3; } else { src = args.at(-1); offset = args.at(-2); lastItems = 2; } let capturedGroupsNum = args.length - lastItems; let capturedGroups = args.slice(1, capturedGroupsNum); return { matched: matched, capturedGroups: capturedGroups, offset: offset, src: src, namedGroupsObj: namedGroupsObj }; }

客户端调用:

let outputStr = src.replace(re, replacer); function replacer(...args) { let obj = packReplacerArgs(args); ... }

这里有个细节需注意。客户端将replacer作为Stringreplace方法的回调函数。而在该函数的参数部分,使用...args接收所有的参数并展开为一个数组,然后再以此数组传递给packReplacerArgs函数。

而在packReplacerArgs函数中,先根据最后一个参数的数据类型,判定其是否为namedGroupsObj。确定后,再依此依序来确定srcoffset的值。而第1个参数matched是可确定的。这样,当这些参数的数量及位置都确定下来后,可变长的与捕获组相关的参数也可通过减法而确定下来。

最后,将这些参数都打包为一个对象而返回。

转义

使用字符串字面符创建RegExp

当在字符串字面符中使用\转义符时,一定要变成\\。如:

let pattern = '<(div)>\\w*?</\\1>';

详见ASCII转义字符。

参考资源

  1. ECMA 262: RegExp Objects
  2. MDN: Regular expressions