前奏
我使用 Ruby,因为您说您没有偏好,这更多是个人兴趣,而不是实际的现场产品。但是,请注意,虽然这里使用的一些技巧可能不适用于各种正则表达式引擎(如 javascript 所拥有的引擎)或对相同事物有不同的语法,但这里没有使用任何 Ruby 特定的东西。相同的正则表达式可以在 Perl、Sublime Text 和更多地方使用。
但在我们开始之前...
免责声明:这不是这样做的方法!不要在您的生产代码库中使用它!
现在我们解决了这个问题……这是一个非常有趣的问题。与任何其他复杂问题一样,
divide and conquer 是一种方法。
我们将要使用的技巧:
- 命名组
正如您可以使用(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 是偶数。
- 删除匹配的字符
许多正则表达式引擎中的一个常见限制是您不能定义可变长度的lookbehinds。即使在你可以的地方(比如在 java 中),你仍然必须定义最大长度。所以,你不能做(?<=x*)之类的事情。
\K 来救援。 \K 基本上翻译为删除到目前为止匹配的所有内容。所以换句话说,(?<=x*)y 可以重写为x*\Ky。
有了这些技巧,让我们开始吧。
首先,让我们定义一些
“函数”(使用技巧
#3)。
- escaped_quote
转义引用是前面有奇数个反斜杠(\)的"。 反斜杠具有转义字符的特殊含义,因此,要匹配单个反斜杠,我们需要用另一个(又名\\ = 一个文字反斜杠)。
要匹配偶数个反斜杠,我们可以使用\\{2}*(也就是两个反斜杠零次或多次 - 2*n)。为了使它奇怪,我们只需添加一个反斜杠 - \\\\{2}* (2*n + 1)。
我们还想说我们要匹配此序列中的所有反斜杠。这是因为正则表达式引擎会尝试找到 偶数 个 反斜杠 来搞砸我们,除非我们另有说明。 \\\" 将被解释为 non escaped 引号,因为它可以匹配 \\",忽略第一个斜杠。为了避免这种情况,我们将添加一个否定的lookbehind,如下所示:(?<!\\)\\\\{2}*
我们的escaped_quote “函数”的最终定义如下:
(?<escaped_quote>(?<!\\)\\\\{2}*"){0}
- 非引用
我们要表达的另一件事是没有有意义的引号。这是一个字符序列,它们是转义的引号或根本不是引号。
请注意,对于 not 引号部分,我们需要为 escaped_quote 添加负前瞻。这是为了确保我们不会吃掉 escaped_quote 中的第一个 \,这会给我们留下剩余的未转义的引用。
(?<non_quoting>(?:\g<escaped_quote>|(?!\g<escaped_quote>)[^"])*){0}
- balanced_quotes
我们需要的最后一个 "function" 是一个序列,它没有不匹配的引号。这可以是完全没有有意义引号的东西,也可以是偶数个有意义引号:
(?<balanced_quotes>\g<non_quoting>|(?:\g<non_quoting>"\g<non_quoting>){2}+){0}
完成所有准备工作后,我们就可以匹配了。
我们将从字符串的开头或单引号开始。前者很明显。后者是因为我们的比赛会留下一个报价。 (?:^|")
编辑:事实证明这些还不够。对于上次我们匹配 empty string 的情况,\K 将不允许我们在 improvised lookbehind 中保持在同一位置并匹配 empty string 再次。为了解决这个问题,我们将添加另一个替代方案 - 空字符串。请注意,此处的顺序很重要,因此我们仅在其他两个失败时才使用此替代方案:(?:^|"|)
随后是 non_quoting 序列,所有内容都被丢弃(使用 #4 技巧)以实现后视:
(?:^|"|)\g<non_quoting>"\K
之后,我们实际匹配的是一个non_quoting序列:
(?:^|"|)\g<non_quoting>"\K\g<non_quoting>
最后,我们必须确保在关闭当前报价后,在字符串的末尾还剩下 balanced_quotes:
(?:^|"|)\g<non_quoting>"\K\g<non_quoting>(?="\g<balanced_quotes>$)
终于!
我们可以将我们的"function"定义和实际匹配一起添加,来实现最终的正则表达式:
(?<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>$)
See it in action
最后的想法
这里需要注意的是,即使您的正则表达式引擎不支持某些功能,您也可以通过放置函数调用来实现相同的正则表达式。唯一没有随处可见但您需要的是\K。
我希望这对阅读本文的每个人来说都是一次有趣的学习经历。