【问题标题】:How can I ensure that my regular expression capture is surrounded by only a single pair of parentheses?如何确保我的正则表达式捕获只被一对括号包围?
【发布时间】:2013-07-03 18:56:36
【问题描述】:

我想要一个正则表达式来匹配一个由一对括号括起来的数字,例如,它会匹配如下所示的内容:

(1)

但它应该匹配其中的(1)

((1))

最初我尝试过这个:

([^\(])\(([0-9]+)\)([^\)])

但它无法匹配字符串开头或结尾的单括号数字。所以blah blah (1) 没有返回匹配项,即使它非常清楚地包含(1)。这是因为上面的正则表达式会查找不在左括号或右括号中的字符,而在字符串的开头或结尾没有可找到的字符。

然后我尝试了这个:

([^\(]?)\(([0-9]+)\)([^\)]?)

这成功匹配了(1),但也匹配了((1)) 中的(1),因为它只是忽略了正则表达式中的括号。所以这个太宽泛了,无法满足我的需求。

我会继续尝试,如果找到解决方案,我会在此处发布解决方案,但我们将不胜感激。有什么想法吗?

请注意:我使用的是 JavaScript。 JavaScript 中不包含一些正则表达式功能。


更新:

当匹配很重要时,我没有明确指出在括号内捕获数字很重要。 (我希望这不会对下面给出的解决方案产生不利影响,除了让它们更难阅读!)但是,整个(1) 应该因此被替换,因此匹配两个括号也很重要。

所有发人深省的回答让我为不同的情况制定了一堆期望的结果。希望这能让表达的目的更清楚。

  • (1) ==> 匹配 '(1)' 并捕获 '1'

  • ((1)) ==> 不匹配

  • (((1))) ==> 不匹配

  • (1) (2) ==> 匹配“(1)”和“(2)”并捕获“1”和“2”

  • (1) ((2)) ==> 匹配 '(1)' 并捕获 '1'

  • ((1) (2)) ==> 匹配“(1)”和“(2)”并捕获“1”和“2”

  • (1)(2) ==> 匹配 '(1)' 和 '(2)' 并捕获 '1' 和 '2' [理想情况下] OR 不匹配

  • (1)((2)) ==> 匹配 '(1)' 并捕获 '1' [理想情况下] OR 不匹配

  • ((1)(2)) ==> 匹配 '(1)' 和 '(2)' 并捕获 '1' 和 '2' [理想情况下] OR 不匹配

对于最后三个,我说“理想”是因为有宽大处理。第一个结果是首选结果,但如果这不可能,我可以忍受根本没有匹配。我意识到这是一个挑战(在 JavaScript 的 RegExp 限制内甚至可能是不可能的),但这就是我将问题提交给这个专家论坛的原因。

【问题讨论】:

  • 明天我会看看可能的副本,因为它看起来很有用。谢谢。
  • @m.buettner 我现在可以看到这个问题的解决方案与其他问题的解决方案有何相似之处。为我的问题辩护,我认为它比catdogcat 难题更容易阅读和理解。而且我最初并没有询问环视。我预先承认这是一个 JavaScript 问题,因为我知道 JS 中没有后向功能。但我承认,也许这个问题与另一个问题太相似了,无法让我保持开放。
  • 我将标题中的“匹配”一词更改为“捕获”,因为我认为这个词可能具有误导性......向所有人道歉;在制作标题时,我没有考虑到 JavaScript 特定意义上的 match 这个词。
  • 别担心,我并不是说你的问题不好。对于 SO 上的正则表达式问题,它实际上已经很好地指定了。尽管如此,由于我的答案最终与其他问题几乎相同,因此有点重复。这并不意味着您的问题应该被删除或任何东西 - 标记重复项在您的情况下主要是一个有用的交叉引用。

标签: javascript regex


【解决方案1】:

稳健的解决方案

仅使用正则表达式可能无法以稳健的方式解决此问题,因为这不是常规语法:平衡括号基本上将其移至乔姆斯基的语言复杂性层次结构中。所以为了稳健地解决这个问题,你实际上必须编写一个解析器并创建一个表达式树。虽然这听起来可能令人生畏,但实际上并没有那么糟糕。这是完整的解决方案:

// parse our little parentheses-based language; this will result in an expression
// object that contains the text of the expression, and any children (subexpressions)
// that represent balanced parentheses groups.  because the expression objects contain
// start indexes for each balanced parentheses group, you can do fast substition in the
// original input string if desired
function parse(s) {
    var expr = {text:s, children:[]};    // root expression; also stores current context
    for( var i=0; i<s.length; i++ ) {
        switch( s[i] ) {
            case '(':
                // start of a subexpression; create subexpression and change context
                var subexpr = {parent: expr, start_idx: i, children:[]};
                expr.children.push(subexpr);
                expr = subexpr;
                break;
            case ')':
                // end of a subexpression; fill out subexpression details and change context
                if( !expr.parent ) throw new Error( 'Unmatched group!' );
                expr.text = s.substr( expr.start_idx, i - expr.start_idx + 1 );
                expr = expr.parent;
                break;
        }
    }
    return expr;
}

// a "valid tag" is (n) where the parent is not ((n));
function getValidTags(expr,tags) {
    // at the beginning of recursion, tags may not be defined
    if( tags===undefined ) tags = [];
    // if the parent is ((n)), this is not a valid tags so we can just kill the recursion
    if( expr.parent && expr.parent.text.match(/^\(\(\d+\)\)$/) ) return tags;
    // since we've already handled the ((n)) case, all we have to do is see if this is an (n) tag
    if( expr.text.match(/^\(\d+\)$/) ) tags.push( expr );
    // recurse into children
    expr.children.forEach(function(c){tags.concat(getValidTags(c,tags));});
    return tags;
}

您可以在此处查看此解决方案的实际效果:http://jsfiddle.net/SK5ee/3/

在不了解您的应用程序或您正在尝试做什么的所有详细信息的情况下,此解决方案可能对您来说可能过大,也可能不会过大。但是,它的优点是您几乎可以使您的解决方案任意复杂。例如,您可能希望能够“转义”输入中的括号,从而将它们从正常的括号平衡方程中取出。或者您可能想忽略引号等内的括号。使用此解决方案,您只需扩展解析器以涵盖这些情况,并且该解决方案可以变得更加健壮。如果你坚持使用一些聪明的基于正则表达式的解决方案,如果你需要扩展你的语法以涵盖这些类型的增强,你可能会发现自己碰壁了。

原始讨论和幼稚解决方案

如果我的理解是正确的,你想得到单括号内的数字,但你想排除双括号内的数字。我将进一步假设您只需要这些数字的有序列表。基于此,这就是您要查找的内容:

a) "(1)(2)((3))" => [1,2]
b) " (5) ((7)) (8) " => [5,8]

不清楚的是,如果括号不平衡,或者括号内不仅仅是数字,会发生什么。 JavaScript正则表达式不支持平衡匹配,所以以下情况会出现问题:

"((3) (2)" => [2] (probably we want [3,2]???)
"((3) (2) (4) (5))" => [2,4] (probably we want [3,2,4,5]???)

从最后两个例子可以清楚地看出,整个事情取决于确定数字前是否有一个或两个括号;不是当括号组关闭时。如果需要处理这些示例,您将必须构建一个括号组树并从那里开始。这是一个更难的问题,我不打算在这里解决。

那么,这给我们留下了两个问题:我们如何处理相互对接的匹配项 ((1)(2)) 以及我们如何处理从字符串开头开始的匹配项 ((1)blah blah)?

我们现在将忽略第二个问题,专注于两者中较难的一个。

显然,如果我们不在乎括号是否闭合,我们可以这样得到我们想要的:

" (1)(2)((3)) ".match(/[^(]\(\d+/g)   => [" (1", ")(2"]

到目前为止一切顺利,但这可能会产生我们不想要的结果:

" (1: a thing (2)(3)((4)) ".match(/[^(]\(\d+/g) => [" (1)", " (2", ")(3"]

所以我们显然想要检查右括号,这适用于:

" (1) (2) ((3)) ".match(/[^(]\(\d+\)/g) => [" (1)", " (2)"]

但是当比赛相互对撞时失败:

" (1)(2)((3)) ".match(/[^(]\(\d+\)/g) => [" (1)"]

那么,我们需要的是匹配右括号,但不要使用它。这就是“前瞻”匹配(有时称为“零宽度断言”)背后的全部理念。这个想法是你确保它在那里,但你不将它作为匹配的一部分包含在内,因此它不会阻止角色被包含在未来的匹配中。在 JavaScript 中,前瞻匹配是使用 (?=subexpression) 语法指定的:

" (1)(2)((3)) ".match(/[^(]\(\d+(?=\))/g) => [" (1", ")(2"]

好的,这样就解决了这个问题!关于如何处理字符串开头/结尾处发生的匹配的更简单的问题。真的,我们所要做的就是使用交替来表示“匹配不是左括号或字符串开头的东西”等:

"(1)(2)((3))".match(/(^|[^(])\(\d+(?=\))/g) => ["(1", ")(2"]

另一种“偷偷摸摸”的做法是只需填充您的输入字符串即可完全避免该问题:

s = "(1)(2)((3))";   // our original input
(" " + s + " ").match(/[^(]\(\d+(?=\))/g) => ["(1", ")(2"]

这样我们就不必大惊小怪了。

好的,这是一个疯狂的长答案,但我将总结如何清理我们的输出。显然,我们不想要那些带有我们不想要的所有额外匹配垃圾的字符串:我们只想要数字。有很多方法可以做到这一点,但这里是我的最爱:

// if your JavaScript implementation supports Array.prototype.map():
" (1)(2)((3)) ".match( /[^(]\(\d+(?=\))/g )
    .map(function(m){return m.match(/\d+/)[0];})

// and if not:
var matches = " (1)(2)((3)) ".match( /[^(]\(\d+(?=\))/g );
for( var i=0; i<matches.length; i++ ) 
    { matches[i] = matches[i].match(/\d+/)[0]; }

稍微好一点的仅 RexExp 解决方案

在 OP 用一些输入样本和预期输出更新问题后,我能够制作一些正则表达式来满足所有样本输入。像许多正则表达式解决方案一样,答案通常是多个正则表达式,而不是一个巨大的。

注意:虽然此解决方案适用于所有 OP 的样本输入,但在各种情况下它都会失败。请参阅下面的完整防水解决方案。

基本上,此解决方案涉及首先匹配(某种)看起来像括号组的事物:

/\(+.+?\)+/g

获得所有这些后,您可以检查它们是无效标签(((n))(((n))) 等)还是好的标签:

if( s.match(/\(\(\d+\)\)/) ) return null;
return s.match(/\(\d+\)/);

您可以在此处看到此解决方案适用于所有 OP 的示例输入:

http://jsfiddle.net/Cb5aG/

【讨论】:

  • 这个问题是,它只会报告一个匹配(1)(2)
  • 啊,那么我们应该让量词变得懒惰......会更新我的答案。
  • 这不是问题。问题是匹配不能重叠。不过,您可以将所有内容放在数字之后的前瞻中:(^|[^(])\((\d+)(?=\)([^)]|$))
  • 啊,你是对的。那么你需要前瞻。也会解决这个问题!
  • 喂。这比我想象的要棘手……好问题。点赞。
【解决方案2】:

回答您的修改

所以你想替换!这意味着您的问题实际上等同于this one。这也让事情变得容易多了。我们要做的是:

  • 匹配 ((number)) 并忽略它
  • 或匹配(number)并替换它

第一个选项将自动获得优先权(因为如果两者都适用,它会从更左侧开始),因此该选项将吞噬所有不需要的事件:

"input".replace(/([(][(]\d+[)][)])|[(]\d+[)]/g, function(match, $1) {
    if ($1)
        return $1;
    else
        return do_whatever_you_want_with(match);
});

所以我们有两种情况:匹配((number)) 并捕获到组1 - 或匹配(number) 并让组1 成为undefined

替换是通过回调完成的,回调将整个match 作为第一个参数,将第一个捕获组作为第二个参数(此处为$1)。然后我们检查是否使用了$1 - 如果是,我们只需返回它,因此不替换任何内容。如果没有,我们可以用match(即(number))做任何我们想做的事情。当然,您也可以将number 仅捕获到另一个变量$2 中,如果更方便,请使用它。


原始答案,关于匹配:

需要的是lookarounds,但是JavaScript不支持lookbehinds。我已经解释了一些更详细的解决方法here。 但是由于您的后视仅针对单个字符,因此检查字符串的开头或不同的字符就足够了。这导致

/(?:^|[^(])[(](\d+)[)](?:[^)]|$)/

还有另一个问题:匹配不能重叠!在(1)(2) 中,引擎匹配(1)((因为[^)] 在匹配中包含一个字符)。因此,(2) 无法匹配,因为这将与之前的匹配重叠。

所以我们将它从第一个匹配中删除,将数字后面的所有内容都放入前瞻中:

/(?:^|[^(])[(](\d+)(?=[)](?:[^)]|$))/

但是请注意,此解决方案也排除了只有一个双括号包围的数字:例如,((1) abc)(abc (2))((1) (2)) 都不会产生匹配项。如果这不是您要查找的内容,则需要将两种情况(前括号和前括号)交替放置。为了使这更容易,它有助于将前瞻拉到数字前面:

/(?:^|[^(]|(?=[(]\d+[)](?:[^)]|$)))[(](\d+)/

令人困惑,我知道。但毕竟 JavaScript 的正则表达式风格非常有限。


【讨论】:

  • 感谢您提供详细、深思熟虑的解释。这里有一点需要考虑。我要休息一下,做一些测试,稍后再回复你。
  • 谢谢。我已经为此 +1 了,但我不知道我是否会使用这些解决方案,因为它们不匹配两个括号。我意识到现在可能不可能,但我已经更新了我的问题以使其更清楚。
  • @guypursey 啊,所以你想在replace 中使用它。这允许完全不同的解决方案(基本上正是我在对您的问题的第一条评论中链接到的问题)。请看我的编辑。
  • 很好的思考和分析,@m.buettner!
  • @guypursey 是的,您的解决方案正是我在新解决方案的最后一句中所指的。顺便说一句,你不需要else if 那...只需使用else... 因为如果$1 没有定义,$2 必须被定义(因为只有匹配时才会执行代码)
【解决方案3】:

这是一个消极的前瞻,然后是一个消极的前瞻:

\((?!\()(\d+)\)(?!\))

Edit live on Debuggex

【讨论】:

  • JS 不支持 Lookbehind。
  • 对我说“无效的正则表达式”:(
  • @m.buettner 正要说同样的话。
  • doh...习惯了Actionscript
  • 修复它并添加了一个演示。
【解决方案4】:

这是你想要的吗?

"(1)(2)((3))".match(/(\({1}\d+\){1})/g) // === ["(1)", "(2)", "(3)"]

看起来像你想要的,而且似乎比其他方法更简单,但也许我遗漏了一些东西......

编辑:错过了一个请求,认为这太容易了......

嗯,js 正则表达式有一个限制,这会使代码难以承受,所以我会做一些稍微不同的事情来获得所需的结果:

 "(1)(2)((3))".match(/(\({1,}\d+\){1,})/g)
  .filter(/./.test, /^\(\d\)$/) // == ["(1)", "(2)"]

【讨论】:

  • 谢谢。是的,恐怕这太简单了。我不希望 (3) 在您的示例中匹配;只是(1)(2)
  • 感谢您的编辑。不幸的是,我只使用正则表达式非常重要,因为我正在使用替换,并且打算使用 RegExp 构造函数用字符串形成表达式。
  • 如果moderndegrees适合你,我会去的,我只是不想让你空手而归......
  • 感谢您的思考。我已经 +1 了你的答案,这样你就不会空手而归。无论如何,我最终可能不得不调整我的代码,因此您的解决方案可能会派上用场。
猜你喜欢
  • 2020-02-21
  • 1970-01-01
  • 2015-01-20
  • 2013-10-31
  • 2013-06-17
  • 1970-01-01
  • 1970-01-01
  • 2012-04-13
  • 1970-01-01
相关资源
最近更新 更多