使用正则表达式,如何高效地匹配双引号中包含双引号的字符串?

6

让我们来看一个文本,我们想匹配所有双引号之间的字符串;但在这些双引号之间,可能会有被引用的双引号。例如:

"He said \"Hello\" to me for the first time"

使用正则表达式,如何高效匹配此内容?


1
你应该先提出一个问题,然后再将其作为答案发布,这样不是更好吗?否则恐怕会被关闭,因为它不是一个真正的问题... - HamZa
2
这被称为“展开循环”(或者Friedl在他的书中这样称呼它)。我认为你应该把这个(或者主要部分)放到一个答案中,因为它显然不是一个问题。一个好的问题可以是“如何高效地匹配带有嵌入引号的字符串?”或类似的问题。 - Tim Pietzcker
非常好。标题仍然包含答案(因此有点长)- 为什么不删除后半部分呢? - Tim Pietzcker
@TimPietzcker 我可以做到... 那么将其标记为主题中的社区条目如何?这是一个候选项。有没有最佳实践? - fge
3
为什么?你应该为此所获得的声誉感到自豪。当有足够多的其他人进行修改时,它就会自动变成社区维护(CW)。 - Tim Pietzcker
显示剩余2条评论
1个回答

17

匹配此类输入的非常有效的解决方案是使用normal* (special normal*)*模式;这个名称来自Jeffrey Friedl的优秀书籍《精通正则表达式》

一般来说,这是一个有用的模式,用于匹配由常规条目(正常部分)和之间分隔符(特殊部分)组成的输入。

请注意,与所有正则表达式一样,只有在没有更好选择时才应使用它;例如,虽然您可以使用此模式解析CSV数据,但如果您使用Java,则最好使用OpenCSV。

还要注意,虽然模式名称中的量词是星号(即零个或多个),但您可以根据需要进行变化。

带嵌入双引号的字符串

让我们再次看上面的例子;请考虑此文本示例可能出现在您的输入中的任何位置:

"He said \"Hello\" to me for the first time"

无论你如何努力,使用“点加贪婪/懒惰量词”魔法也不能帮助你解决它。相反,将输入内容分类为普通和特殊引号:

  • 普通引号是指除反斜杠和双引号之外的任何字符:[^\\"]
  • 特殊引号是由反斜杠后跟双引号组成的序列:\\"

将此替换为normal* (special normal*)*模式,则得到以下正则表达式:

[^\\"]*(\\"[^\\"]*)*

在原正则表达式的基础上加上双引号以匹配完整文本,得到最终的正则表达式:

"[^\\"]*(\\"[^\\"]*)*"

请注意,这也将匹配空引号字符串。

带破折号分隔符的单词

在这里,我们需要使用变量的变体,因为:

  • 我们不想要空单词,
  • 我们不想要以破折号开头的单词,
  • 当出现破折号时,如果有的话,它必须至少有一个字母位于另一个破折号之前。

为简单起见,我们还假设只允许小写ASCII字母。

示例输入:

the-word-to-match

让我们再次将其分解为正常和特殊:

  • 正常字符:小写ASCII字母:[a-z]
  • 特殊字符:破折号:-

该模式的规范形式应为:

[a-z]*(-[a-z]*)*

但正如我们所说:

  • 我们不想要以破折号开头的单词:第一个*应该变成+
  • 当找到破折号时,它后面应该至少有一个字母:第二个*应该变成+

最终得到:

[a-z]+(-[a-z]+)*

在它周围添加单词锚点以获得最终结果:

\b[a-z]+(-[a-z]+)*\b

其他操作符变体

上述示例仅将*替换为+,但是您当然可以拥有尽可能多的变体。一个极其经典的例子就是 IP 地址:

  • 正常情况下是最多三位数字 (\d{1,3}),
  • 特殊字符是点号:\.
  • 第一个normal只出现一次,因此没有量词,
  • (special normal*)中的normal也只出现一次,因此没有量词,
  • 最后(special normal*)部分恰好出现三次,因此使用{3}

这样就得到了一个带有单词锚点的表达式:

\b\d{1,3}(\.\d{1,3}){3}\b

结论

这种模式的灵活性使其成为您正则表达式工具箱中最有用的工具之一。虽然存在许多问题,如果存在库,则不应使用正则表达式,但在某些情况下,您必须使用正则表达式。一旦您练习了一段时间,它将成为您最好的朋友之一!

提示

  • 很可能你不需要(或不想)捕获重复部分(即(special normal*) 部分),因此建议使用非捕获组。例如,对于引号字符串,请使用 "[^\\"]*(?:\\"[^\\"]*)*"。实际上,在这种情况下,除非您在.NET中使用此模式,否则捕获几乎永远不会产生所需的结果,因为重复一个捕获组只会给出最后一个捕获(所有先前的重复都将被覆盖)。 (感谢@ohaal)

@ohaal 这是真的(应该在提示部分或其他地方提到);然而,我想保持事情相对简单。此外,我相信一些正则表达式引擎会优化捕获组,如果你实际上不使用它们的话。 - fge
@fge,我自由地修改了一下这个提示,因为我认为措辞略有偏差。 - Martin Ender
@m.buettner 实际上我的评论一开始并不是那个意图。正如我所说,许多正则表达式引擎会在没有使用捕获组时将其优化掉(即丢弃它们)。我提到的是确实需要捕获的情况。 - fge
1
@fge 只有极少数人可能能够优化它们,因为他们无法知道您是否稍后会从引擎外部访问它们。此外,仅在模式周围添加捕获与这种特定技术无关,因此我认为更重要的是专注于模式内部的优化。 - Martin Ender
@fge 嗯,我喜欢那个编辑更好。而且.NET是唯一一种正则表达式风格,允许您访问组的所有捕获,即使它重复出现或者您两次使用相同的组名。有了这种功能,捕获重复部分(或者至少是重复部分的“普通”部分)就有意义了。 - Martin Ender
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接