前言
由于您表示没有偏好,这更多是个人兴趣而不是实际的生产产品,因此我使用了Ruby。然而,请注意,尽管这里使用的一些技巧可能在各种正则表达式引擎(如javascript所拥有的引擎)上无法工作,或者对于相同的事情具有不同的语法,但这里没有使用任何特定于Ruby的内容。相同的正则表达式将在Perl、Sublime Text和许多其他地方起作用。
但在我们开始之前...
免责声明:这不是正确的做法!请勿在生产代码库中使用此方法!
既然我们已经讲清楚了... 这是一个非常有趣的问题。像任何其他复杂问题一样,
分而治之 是解决问题的方式。
我们要使用的技巧:
- 命名组
就像您可以使用 (group_contents)
创建带编号的组一样,您也可以使用 (?<group_name>group_contents)
定义命名组。我们在技巧上并不需要这个,但它会使所有内容更加易于理解。
- 重新执行群组模式
您可以使用 \g<group_name_or_number>
来执行之前定义的相同模式。例如:
(?<three_letter_word>\b\w{3}\b) \g<three_letter_word>
将匹配xyz abc
。
- 重复零次
乍一看,{0}
可能看起来没用。然而,与上述两个结合使用,它可以像定义函数但不执行它们一样工作。例如:
(?<even>[02468]){0}7\g<even>8\<even>9\<even>0
将匹配7x8y9z0
,其中x
、y
和z
是偶数位数字。
- 删除匹配的字符
许多正则表达式引擎中常见的限制是无法定义具有可变长度的后顾断言。即使在一些允许定义的引擎中(如Java),仍然需要定义最大长度。因此,您不能执行诸如(?<=x*)
之类的操作。
\K
来解救。基本上,\K
的含义是“放弃到目前为止已匹配的所有内容”。因此,换句话说,(?<=x*)y
可以重写为x*\Ky
。
掌握了这些技巧,我们开始吧。
首先,让我们使用技巧
#3定义几个“函数”。
- escaped_quote
一个
转义的引号是一个被一个
奇数个
反斜杠(
\
)所跟随的引号(
"
)。反斜杠有特殊的转义字符含义,所以为了匹配一个单独的
反斜杠,我们需要用另一个反斜杠进行转义(即
\\
=
一个字面上的反斜杠)。
为了匹配
偶数个
反斜杠,我们可以使用
\\{2}*
(即两个
反斜杠零次或多次-
2*n)。为了使其变成
奇数,我们只需要再添加一个
反斜杠-
\\\\{2}*
(
2*n + 1)。
我们还需要指出,我们希望匹配此序列中的所有反斜杠。这是因为正则表达式引擎会尝试找到一个偶数个反斜杠来欺骗我们,除非我们告诉它不这样做。 \\\\\"将被解释为非转义引号,因为它只能匹配\ \",忽略第一个斜杠。为了不允许这种情况,我们将添加一个负回溯,如下所示:(?
我们的escaped_quote"函数"的最终定义如下:
(?<escaped_quote>(?<!\\)\\\\{2}*"){0}
- non_quoting
我们要表达的另一件事是没有有意义引号的东西。这是一系列字符,它们是转义引号或根本不是引号。
请注意,对于根本不是引号的部分,我们需要添加一个负向先行断言来排除escaped_quote。这是为了确保我们不会吃掉escaped_quote中的第一个\
,这将使我们剩下一个未转义的引号。
(?<non_quoting>(?:\g<escaped_quote>|(?!\g<escaped_quote>)[^"])*){0}
- 平衡引号
我们需要的最后一个“函数”是一个没有不匹配引号的序列。这可以是完全没有有意义的引号或者有偶数个有意义的引号:
(?<balanced_quotes>\g<non_quoting>|(?:\g<non_quoting>"\g<non_quoting>){2}+){0}
准备工作完成后,我们已经可以进行匹配了。
我们将从字符串的开头或单引号开始。前者是显而易见的。后者是因为我们的匹配会留下一个引号。 (?:^|")
编辑:结果证明这还不够。如果上次我们匹配了空字符串,\K
将不允许我们保持在相同的位置并在即席回顾中再次匹配空字符串。为了解决这个问题,我们将添加另一个选择 - 空字符串。请注意,这里的顺序很重要,以便只有其他两个失败时才使用此选择:(?:^|"|)
然后是一个non_quoting序列,使用#4技巧删除所有内容以实现后顾:
(?:^|"|)\g<non_quoting>"\K
接着,我们实际匹配的是一个非引用序列:
(?:^|"|)\g<non_quoting>"\K\g<non_quoting>
最后,我们必须确保在关闭当前引号后,字符串末尾仍有平衡的
引号:
(?:^|"|)\g<non_quoting>"\K\g<non_quoting>(?="\g<balanced_quotes>$)
终于完成了!
我们可以将"函数"定义和实际匹配组合在一起,以达到最终的正则表达式:
(?<escaped_quote>(?<!\\)\\\\{2}*"){0}(?<non_quoting>(?:\g<escaped_quote>|(?!\g<escaped_quote>)[^"])*){0}(?<balanced_quotes>\g<non_quoting>|(?:\g<non_quoting>"\g<non_quoting>){2}+){0}(?:^|"|)\g<non_quoting>"\K\g<non_quoting>(?="\g<balanced_quotes>$)
看它如何运作
最后的想法
这里需要注意的一件事是,即使您的正则表达式引擎不支持某些功能,您仍可以通过在函数调用中替换实现相同的正则表达式。唯一无处不在但您将需要的是\K
。
我希望阅读此文对每个人都是一个有趣的学习经历。