如何使用正则表达式匹配带引号的字符串?

46
如果我想使用正则表达式匹配一个引号包含的字符串,以下哪种方法更“好”(其中“好”的意思是既更高效,又不太可能出现意外情况):
/"[^"]+"/ # match quote, then everything that's not a quote, then a quote
或者
/".+?"/   # match quote, then *anything* (non-greedy), then a quote
假设这个问题中空字符串(即“”)不是一个问题。在我看来(我不是正则表达式的新手,但肯定不是专家),这些表达式将是等效的。
更新:经过反思,我认为将“+”字符更改为“*”将正确处理空字符串。
9个回答

46

你应该使用第一种方法,因为第二种方法是不好的实践。考虑到后续的开发人员需要查找后面跟着感叹号的字符串,那么他应该使用:


You should use number one, because number two is bad practice. Consider that the developer who comes after you wants to match strings that are followed by an exclamation point. Should he use:
"[^"]*"!
或:
".*?"!

当主语存在时,就会出现差异:

"one" "two"!

第一个正则表达式匹配:

"two"!

当第二个正则表达式匹配时:

"one" "two"!

尽可能具体,使用否定字符类。与 .* 不同,[^"]* 可跨越多行,除非使用单行模式,否则 [^"\n]* 也会排除换行符。

至于回溯,第二个正则表达式在匹配每个字符串的每个字符时都会回溯。如果缺少闭合引号,则两个正则表达式都将通过整个文件进行回溯。只有它们回溯的顺序不同。因此,从理论上讲,第一个正则表达式更快。但实际上,你不会注意到任何差别。


1
这个反例正是我写这个问题时正在寻找的。谢谢Jan。 - Graeme Perrow

26

更加复杂,但它处理了转义引号和转义反斜杠(转义反斜杠后面跟着引号不是问题)

/(["'])((\\{2})*|(.*?[^\\](\\{2})*))\1/

示例:
   "hello\"world" 匹配 "hello\"world"
   "hello\\"world" 匹配 "hello\\"


你如何从结果字符串中删除/修复\或任何受反斜杠保护的字符? - kdubs

14

我建议:

([\"'])(?:\\\1|.)*?\1

之所以选择这个方法,是因为它能处理转义引号字符并允许单引号和双引号都作为引号。我还建议查看这篇深入探讨此问题的文章:http://blog.stevenlevithan.com/archives/match-quoted-string

然而,除非你有严重的性能问题或不能确定是否存在嵌套引号,否则请使用更简单和易读的方法:

/".*?"/

我必须承认,非贪婪模式不是基本的类Unix风格的“ed”正则表达式,但它们变得越来越普遍。我仍然不习惯像(?:stuff)这样的组操作符。


你提到的"(:?stuff)"是什么意思?我知道"(?:stuff)",但不知道另一个。 - Tomalak
在Perl中,这些被称为“扩展模式”。请查看http://perldoc.perl.org/perlre.html下的“扩展模式”(大约在页面的1/3处)。在这种情况下,它就像(stuff),只是没有涉及到捕获($1或\1)。 - Harold Bamford
是的,我刚刚修复了它。我没有理解原始评论(我以为SO中有一些晦涩的转义错误)。对于造成的混淆,我感到抱歉。 - Harold Bamford
没问题。;-) 实际上,您可以删除这整个对话,因为它已经没有实质内容了。 - Tomalak

6

我认为第二个更好,因为当缺少终止符"时会更快地失败。第一个将在字符串上回溯,这是一种潜在的昂贵操作。如果您使用的是Perl 5.10,则另一种正则表达式可能是/"[^"]++"/,它传达与版本1相同的含义,但与版本2一样快。


为什么第二个能够更快地失败? - innaM
我在你问之前几秒钟添加了解释。第二个不会回溯。 - Leon Timmermans
1
如果你想变得更加高级并支持正则表达式中的转义引号,你可以这样做:/"(?:[^"]|(?<!\)(?>\\)*\")++"/。我已经在https://dev59.com/zHVD5IYBdhLWcg3wL4mM解释过了。 - Leon Timmermans
Leon 的回溯理解是错误的。当有闭合引号时,.*? 会为字符串中的每个字符进行回溯,而当缺少闭合引号时,两个正则表达式都会进行回溯。 - Jan Goyvaerts
术语“回溯”意味着正则表达式引擎返回到正则表达式中的先前标记。它并没有说明在主题字符串中向前或向后移动。在我的答案中,我提供了一般理论。一些正则表达式引擎可能针对特定情况进行优化。 - Jan Goyvaerts
显示剩余3条评论

4

我会选择第二个选项,因为它更易读。但是我仍然希望匹配空字符串,所以我会使用:

/".*?"/

@Graeme Perrow:.*?是非贪婪匹配的事实标准。 - slf

2
使用否定字符类可以防止在输入中存在边界字符(例如您的示例中的双引号)时进行匹配。
您的示例#1: /"[^"]+"/ # 匹配引号,然后是除引号以外的所有内容,最后是一个引号 只匹配最小的一对匹配引号-非常好,大多数情况下这就是您所需要的。 但是,如果您有嵌套的引号,并且您对最大的匹配引号(或所有匹配的引号)感兴趣,则处于更加复杂的情况。
幸运的是,Damian Conway已经准备好了救援:Text::Balanced为您服务,如果您发现有多个匹配的引号标记。 它还具有匹配其他成对的标点符号(例如括号)的优点。

2

从性能角度来看(长字符串上的重型、长时间运行的循环),我可以想象

"[^"]*"

比...更快
".*?"

因为后者需要在每一步进行额外的检查:查看下一个字符。前者可以无意识地在字符串上滚动。

正如我所说,在实际情况下,这几乎不会被注意到。因此,我会选择第二种方法(如果我的当前正则表达式支持它),因为它更易读。否则就用第一种方法。


0

考虑到我直到今天才知道“*?”的事情,而我已经使用正则表达式20多年了,我会赞成第一个。它确实清楚地表明了你想要做什么-你想匹配一个不包含引号的字符串。


我也使用正则表达式很多年了,我知道有一种非贪婪的方式来做事情,但直到今天我才意识到它是多么容易。这就是为什么我问的原因 - 我发现它更容易阅读(现在我知道它的含义),那么我不应该使用它的原因是什么? - Graeme Perrow
唯一的限制在于你的正则表达式引擎。有可能你面对的是不支持非贪婪量词的引擎,但现代的引擎通常都支持。 - Tomalak
一些正则表达式引擎支持更改默认的贪婪性(使 .* 为非贪婪,而 .*? 为贪婪)。在 PHP 中,您可以使用 U 正则表达式修饰符来实现。我在抓取 HTML 时曾经用过它。 - PEZ
PEZ:我强烈建议您使用/.?/而不是/./U。大多数人会将.*?识别为懒惰量词,或者至少是他们不知道的东西。在正则表达式的末尾放置/U很容易被忽略。这关乎保持您的代码易于阅读。 - Jan Goyvaerts

0
我更喜欢第一个正则表达式,但这当然是个人口味的问题。
第一个可能会更高效?
Search for double-quote
add double-quote to group
for each char:
    if double-quote:
        break
    add to group
add double-quote to group

是做一些更复杂的回溯相关编程吗?


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