深入剖析vscode工具函数(八)解密复杂正则表达式
VSCode中的一段正则
正则表达式是程序员的有力武器,但对于复杂的正则表达式,很多人可能感到困惑。今天,我们来分析一段高级正则表达式,并探讨它的内涵与应用场景。
代码语言:javascript复制const regexp = /("[^"\]*(?:\.[^"\]*)*")|('[^'\]*(?:\.[^'\]*)*')|(/*[^/*]*(?:(?:*|/)[^/*]*)*?*/)|(/{2,}.*?(?:(?:r?n)|$))|(,s*[}]])/g;
这个表达式非常长,我们可以先从大体上拆分一下它的结构:
代码语言:javascript复制/(...)|(...)|(...)|(...)|(...)/
可以看到整个正则表达式就是有五个分组构成,我们分别对这五个分组进行详细拆解:
1. 匹配双引号内的字符串:**"[^"\]*(?:\.[^"\]*)*"
**
- **
"
**:匹配双引号; - **
[^"\]*
**:匹配非双引号和非反斜杠的字符零次或多次; - **
(?:\.[^"\]*)*
**:非捕获分组,匹配转义字符后的任意字符,以及随后的非双引号和非反斜杠的字符零次或多次;这个分组可以重复零次或多次; - **
"
**:匹配双引号。
这个正则的作用就是匹配一个双引号字符串,比如:
代码语言:javascript复制"Hello World"
我们先不管它怎么实现,如果自己要实现怎么办?简单的思路就是:
代码语言:javascript复制/"[^"]*"/
这个我相信大多数人都能写出来,无非就是匹配两个引号和中间的内容。但这里的问题就在于转义字符,比如我这个字符串是这样的:
代码语言:javascript复制"Hello " World"
如果用上面的正则去匹配,在遇到 "
就终止了,实际上难点就是把这些转义字符也识别出来,不能让他们阻断整个表达式。
识别转义字符很简单,就是一个反斜杠加上任意一个字符:
代码语言:javascript复制/\./
在转义字符之后还有任意的字符,甚至还能有转义字符,所以剩余的匹配部分就是:
代码语言:javascript复制/[^\"]*/
两个加起来,我们就能匹配到后面那个字符串了:
代码语言:javascript复制/\.[^\"]*/
这个匹配的是 `" World`
再把这个模式重复多次,就可以匹配到多个转义字符以及后面的字符了:
代码语言:javascript复制/(\.[^\"]*)*/
最后,由于这个分组并不需要捕获,所以我们加上 ?:
来提高性能,其实这里最难的正则就出来了:
/(?:\.[^\"]*)*/
2. 匹配单引号内的字符串:**'[^'\]*(?:\.[^'\]*)*'
**
这个和1是一样的,只是双引号变成了单引号,就不赘述了。
3. 匹配块注释:**/*[^/*]*(?:(?:*|/)[^/*]*)*?*/
**
- **
/*
**:匹配 **/*
**; - **
[^/*]*
**:匹配非星号和非斜杠的字符零次或多次; - **
(?:(?:*|/)[^/*]*)*?
**:非捕获分组,匹配星号或斜杠后的非星号和非斜杠的字符零次或多次;这个分组可以重复零次或多次,但尽量少重复(懒惰匹配); - **
*/
**:匹配*/
。
同样我们可以想象一下自己要匹配块注释怎么写,很自然地想到:
代码语言:javascript复制//*(.*?)*//
这个正则可以匹配一般的注释,但是无法匹配嵌套注释的情况。
因此,我们和前面一样,把关键词先排除:
代码语言:javascript复制//*[^/*]**//
但这样会导致遇到 /*
就停止了,所以我们还得匹配 /*
:
/((*|/)[^/*]*)*?/
其实这样就实现了匹配一个 /
一些其他字符,或者是 *
加上其他字符,并且是非贪婪匹配,这样能够匹配到最后的那个 */
。
把这几个加起来,再加上非捕获分组,就是这里的正则表达的含义了。
4. 匹配行注释:**/{2,}.*?(?:(?:r?n)|$)
**
- **
/{2,}
**:匹配两个或更多的斜杠; - **
.*?
**:匹配任意字符零次或多次,但尽量少重复(懒惰匹配); (?:(?:r?n)|$)
:非捕获分组,匹配换行符(rn
或 **n
**)或字符串末尾。
行注释就简单地多了,只需要匹配两个斜杠开头,然后一直匹配到换行符或者整个字符串的末尾就行。
5. 匹配尾部多余的逗号:**,s*[}]]
**
- **
,
**:匹配逗号; - **
s*
**:匹配空白字符(空格、制表符、换行符等)零次或多次; - **
[}]]
**:匹配右大括号或右方括号。
这个相信大家都能看懂了。
正则的使用
代码语言:javascript复制function stripComments(content) {
return content.replace(regexp, function (match, _m1, _m2, m3, m4, m5) {
// Only one of m1, m2, m3, m4, m5 matches
if (m3) {
// A block comment. Replace with nothing
return '';
} else if (m4) {
// Since m4 is a single line comment is is at least of length 2 (e.g. //)
// If it ends in r?n then keep it.
const length = m4.length;
if (m4[length - 1] === 'n') {
return m4[length - 2] === 'r' ? 'rn' : 'n';
}
else {
return '';
}
} else if (m5) {
// Remove the trailing comma
return match.substring(1);
} else {
// We match a string
return match;
}
});
}
VSCode利用这段正则移除给定内容中的注释。以下是 stripComments
函数的逐行解析:
- 使用
content.replace(regexp, ...)
方法查找并替换content
中与正则表达式regexp
匹配的内容。这个方法的第二个参数是一个回调函数,它根据匹配结果来决定替换内容。 - 回调函数接收 6 个参数:**
match
** 是整个匹配的字符串,**_m1
**,_m2
,m3
,m4
,m5
分别对应正则表达式中的捕获分组。这里,我们只关心m3
,m4
, **m5
**,因为它们分别代表块注释、单行注释和多余的逗号。 - 使用
if (m3) { ... }
判断是否匹配到了块注释。如果是,返回空字符串(**''
**),即将块注释移除。 - 使用
else if (m4) { ... }
判断是否匹配到了单行注释。如果是,首先获取单行注释的长度。然后判断单行注释是否以换行符(**r?n
)结尾。如果以换行符结尾,则保留换行符;否则返回空字符串(''
**),即将单行注释移除。 - 使用
else if (m5) { ... }
判断是否匹配到了多余的逗号。如果是,返回匹配字符串去掉首字符的子字符串,即将多余的逗号移除。 - 如果都没有匹配到(即匹配到了字符串),则返回原始匹配字符串。
在VSCode中的应用
这个函数在VSCode中用来去除 json
中的注释,因为 json
本身是不支持注释的。
关于为什么 JSON 不支持注释,Douglas Crockford 在他的许多演讲和访谈中都谈到了这个问题。他的解释主要包括以下几点:
- 简单性:Crockford 认为 JSON 应该保持简单、紧凑和易于解析。引入注释会增加解析器的复杂性,从而违背了 JSON 设计的初衷。通过避免注释,JSON 可以确保跨平台和编程语言的可读性和可用性。
- 减少滥用:Crockford 观察到在 XML 中,注释经常被滥用,如用于配置文件中的条件处理等。他不希望同样的情况出现在 JSON 中,因此决定从一开始就不支持注释。
- 数据与元数据的分离:JSON 的设计目标是用于数据交换。允许注释可能导致数据与元数据之间的界限变得模糊,使解析和处理 JSON 数据变得困难。为了保持数据与元数据的分离,Crockford 决定不在 JSON 中支持注释。
如果一定要用注释,可以像VSCode这样提供一个去除注释的方法,也可以使用 JSON5
。
JSON5
的起源可以追溯到 2012 年,当时 JSON
已经成为许多 Web 开发者的首选数据交换格式。然而,尽管 JSON
的简洁性和跨平台兼容性使其在许多场景中非常实用,但其严格的语法规则使得在某些方面使用起来不够便捷。
为了解决这些问题,JSON5
的创建者 Michael Bolin
开发了一个基于 JSON
的扩展,旨在使 JSON
更容易阅读和编写。Michael Bolin
受到了 ECMAScript 5
的启发,因此将其命名为 JSON5
。JSON5
的设计目标是继承原始 JSON
的优点,同时添加一些类似 JavaScript 对象字面量的功能,以提高灵活性和易用性。
JSON5
的主要特性包括支持注释、宽松的字符串引号规则、尾随逗号、更灵活的数字表示、未引用的属性名以及多行字符串。这些特性使得 JSON5
在阅读和编写方面更加友好,尤其适用于需要添加注释或使用更接近 JavaScript 语法的场景。
不过目前很多解析器都不支持 JSON5
,为了保证更高效简洁的性能,多半还是采用 VSCode
这种minify
的方式,在最后将注释剔除。
小结
本文介绍了VSCode中如何实现去除JSON注释。由于JSON本身不支持注释,因此需要使用正则表达式去除注释。VSCode使用了一个很复杂的正则表达式的多个分组,分别用于匹配双引号内的字符串、单引号内的字符串、块注释、单行注释以及尾部多余的逗号。在这里我们详细拆解分析了整个正则的细节和作用。
虽然JSON不支持注释,但是可以使用JSON5这种扩展格式来支持注释。不过目前很多解析器都不支持JSON5,因此在实际开发中,还是需要使用类似VSCode这种minify的方式来去除注释,以保证更高效简洁的性能。