正则表达式:匹配所有不在引号内的实例

76

这个问题/回答中,我推断出匹配给定正则表达式不在引号内的所有实例是不可能的。也就是说,它不能匹配转义引号(例如:"this whole \"match\" should be taken")。如果有我不知道的方法可以做到这一点,那将解决我的问题。

但是,如果没有这样的方法,我想知道在JavaScript中是否有任何有效的替代方案。我已经考虑了一下,但是无法想出任何优雅的解决方案,这些方案在大多数情况下,如果不是全部情况下都会生效。

具体来说,我只需要替代方法与.split()和.replace()方法一起使用即可,但如果能更加通用,那就最好了。

例如:
输入字符串为:
+bar+baz"not+or\"+or+\"this+"foo+bar+
将 + 替换为 #,不在引号内,则返回:
#bar#baz"not+or\"+or+\"this+"foo#bar#

4个回答

127

实际上,对于任何字符串,您都可以匹配正则表达式不在引号内的所有实例,其中每个开放引号都再次关闭。比如,就像你上面的示例中,你想匹配\+

这里的关键观察是,如果在单词后面跟随偶数个引号,则其在引号外。这可以建模为前瞻断言:

\+(?=([^"]*"[^"]*")*[^"]*$)

现在,您希望不计算转义引号。这变得有点复杂。您需要考虑反斜杠并使用 [^"\\]* 而不是 [^"]* ,这会进入下一个引号。当您遇到反斜杠或引号时,如果您遇到反斜杠,您需要忽略下一个字符,否则请继续前进到下一个未转义的引号。这看起来像 (\\.|"([^"\\]*\\.)*[^"\\]*")。组合起来,您会得到

\+(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)

我承认这有点晦涩。 =)


4
谢谢!没想到这是可能的。我完全理解理论,大约60%的正则表达式,但当需要自己写时,我的理解度降为0%。哦,那好吧,也许有一天我会掌握它。 - Azmisov
1
没事了,我只是忘记在所有括号内放置 ?:\+(?=(?:[^"\\]*(?:\\.|"(?:[^"\\]*\\.)*[^"\\]*"))*[^"]*$) - Azmisov
3
尝试在一个项目中使用这个,但失败了。我发现原因是如果在两个单引号 '' 中间有一个双引号 '"' ... 这将导致字符串中双引号的数量是奇数。 - anson
在最后一个正则表达式中,括号似乎不匹配。我看到有4个左括号和6个右括号。 - jcollum
6
请大家查看@zx81在他的回答中提出的解决方案。如果可以使用,这个方案更容易编写且性能更好。 - Gildor
显示剩余3条评论

81

Azmisov,我重新提出这个问题,因为你说你正在寻找“可以在JavaScript中使用的任何有效替代方法”和“在大多数情况下(如果不是全部),可以使用的任何优雅解决方案”。

实际上有一个简单的通用解决方案,没有被提到。

与其他替代方案相比,该解决方案的正则表达式非常简单:

"[^"]+"|(\+)

这个想法是我们匹配但忽略引号内的内容,以使其中性化(在交替符的左侧)。在右侧,我们将没有被中性化的所有+都捕获到第1组中,然后替换函数检查第1组。以下是完整的工作代码:

<script>
var subject = '+bar+baz"not+these+"foo+bar+';
var regex = /"[^"]+"|(\+)/g;
replaced = subject.replace(regex, function(m, group1) {
    if (!group1) return m;
    else return "#";
});
document.write(replaced);

在线演示

您可以使用相同的原理进行匹配或拆分。请参阅参考中的问题和文章,它们还将为您提供代码示例。

希望这给您提供了一个不同的想法去完成这个任务。 :)

那么空字符串呢?

以上是一个通用的答案来展示技术,可以根据您的确切需求进行调整。如果您担心您的文本可能包含空字符串,只需将字符串捕获表达式内的量词从 + 更改为 *:

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

请看演示

转义引号怎么办?

同样,上面的内容是为了展示该技术的一般答案。不仅可以将“忽略此匹配项”的正则表达式调整到您的需求,还可以添加多个要忽略的表达式。例如,如果您想确保适当忽略转义引号,则可以在其他两个正则表达式前添加选择分支 \\"| 以匹配(并忽略)标点符号中残留的转义双引号。

接下来,在捕获双引号字符串内容的 "[^"]*" 部分内,您可以添加一个选择分支以确保在其 " 转换为结束符号之前,匹配转义的双引号,将它转换为 "(?:\\"|[^"])*"

最终的表达式有三个分支:

  1. \\" 匹配并忽略
  2. "(?:\\"|[^"])*" 匹配并忽略
  3. (\+) 匹配、捕获和处理

请注意,在其他正则表达式环境中,我们可以使用回顾后发来更轻松地完成此任务,但JS不支持此功能。

完整的正则表达式如下:

\\"|"(?:\\"|[^"])*"|(\+)

请参阅正则表达式演示完整脚本

参考资料

  1. 如何匹配除s1、s2、s3等情况之外的模式
  2. 如何匹配一个模式,除非...

5
这种方法实际上比@Jens建议的先行查找法更好。它更容易编写,并且性能更好。直到我遇到了性能问题,需要匹配1.5M文本时,我才意识到并使用了先行查找方式,但它需要约90秒,而这种方法只需要600毫秒。 - Gildor
1
是的,这样更好 =) - Jens
1
你如何避免在此情况下出现转义引号?使用这个模式是否可能实现? - Pomme.Verte
1
@BrianLow 你说得对。答案意在尽可能简单地展示技巧。我已经根据你的评论扩展了它(请参见“空字符串怎么办?”和“转义引号怎么办?”部分)。 - zx81
4
这个不是匹配双引号内的所有字符吗?我以为问题是如何匹配双引号外的内容。 - Akin Hwan
显示剩余6条评论

6

您可以通过以下三个步骤完成该操作:

  1. 使用正则表达式全局替换将所有字符串的内容提取到一个辅助表中。
  2. 进行逗号翻译。
  3. 使用正则表达式全局替换将字符串的内容交换回来。

下面是代码:

// Step 1
var sideTable = [];
myString = myString.replace(
    /"(?:[^"\\]|\\.)*"/g,
    function (_) {
      var index = sideTable.length;
      sideTable[index] = _;
      return '"' + index + '"';
    });
// Step 2, replace commas with newlines
myString = myString.replace(/,/g, "\n");
// Step 3, swap the string bodies back
myString = myString.replace(/"(\d+)"/g,
    function (_, index) {
      return sideTable[index];
    });

如果您设置后运行该操作
myString = '{:a "ab,cd, efg", :b "ab,def, egf,", :c "Conjecture"}';

你应该获取

{:a "ab,cd, efg"
 :b "ab,def, egf,"
 :c "Conjecture"}

这个方法可行,因为在第一步之后,

myString = '{:a "0", :b "1", :c "2"}'
sideTable = ["ab,cd, efg", "ab,def, egf,", "Conjecture"];

因此,myString中唯一的逗号都在字符串外。然后,第二步将逗号转换为换行符:

myString = '{:a "0"\n :b "1"\n :c "2"}'

最后,我们将仅包含数字的字符串替换为其原始内容。

对于优雅的非正则表达式解决方案点赞。不过,正则表达式对于我正在做的事情更加灵活。 - Azmisov

3

虽然zx81的答案似乎是最高效和干净的,但它需要以下修复才能正确捕获转义引号:

var subject = '+bar+baz"not+or\\"+or+\\"this+"foo+bar+';

并且

var regex = /"(?:[^"\\]|\\.)*"|(\+)/g;

此外,已经提到的"group1 === undefined"或"!group1"。 尤其是第2点似乎很重要,需要考虑原始问题中提出的所有内容。

但应该注意的是,此方法隐式地要求字符串在未转义引号对之外没有转义引号。


这使得我在regexr中出现了错误。错误为未转义的正斜杠。 - behelit

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