如何确保我的正则表达式捕获只被一对括号包围?

时间:2013-07-03 18:56:36

标签: javascript regex

我想要一个正则表达式来匹配由一对括号括起来的数字,例如,它会匹配看起来像这样的东西:

(1)

匹配<{1}}里面的内容:

(1)

最初我试过这个:

((1))

但它无法在字符串的开头或结尾处匹配单个带括号的数字。因此,([^\(])\(([0-9]+)\)([^\)]) 没有返回匹配项,即使它非常明确地包含blah blah (1)。这是因为上面的正则表达式查找不在打开或关闭括号的字符,当在字符串的开头或结尾时,没有要查找的字符。

然后我尝试了这个:

(1)

这已成功匹配([^\(]?)\(([0-9]+)\)([^\)]?) ,但也匹配(1)内的(1),因为它只是忽略了正则表达式中的周围括号。所以这个太宽泛了我的需求。

我会继续尝试,如果找到一个解决方案,我会在这里发布一个解决方案,但是我会非常感谢任何帮助。有什么想法吗?

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


更新:

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

所有发人深省的反应都让我为不同的情况制定了一堆预期的结果。希望这能使表达的目标更加清晰。

  • (1) ==&gt;匹配'(1)'并捕获'1'

  • (1) ==&gt;不匹配

  • ((1)) ==&gt;不匹配

  • (((1))) ==&gt;匹配'(1)'和'(2)'并捕获'1'和'2'

  • (1) (2) ==&gt;匹配'(1)'并捕获'1'

  • (1) ((2)) ==&gt;匹配'(1)'和'(2)'并捕获'1'和'2'

  • ((1) (2)) ==&gt;匹配'(1)'和'(2)'并捕获'1'和'2'[理想情况]或不匹配

  • (1)(2) ==&gt;匹配'(1)'并捕获'1'[理想情况]或不匹配

  • (1)((2)) ==&gt;匹配'(1)'和'(2)'并捕获'1'和'2'[理想情况]或不匹配

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

4 个答案:

答案 0 :(得分:5)

强大的解决方案

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

// 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} })?

我们现在要忽略第二个问题,把重点放在两者的难度上。

显然,如果我们不关心括号是否已关闭,我们可以通过这种方式得到我们想要的东西:

(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)"]

然后,我们需要匹配那个右括号,但不消耗它。这就是“先行”匹配背后的整个想法(有时称为“零宽度断言”)。这个想法是你确保它在那里,但你没有把它作为比赛的一部分包括在内,所以它不会阻止角色被包含在未来的比赛中。在JavaScript中,使用" (1)(2)((3)) ".match(/[^(]\(\d+\)/g) => [" (1)"] 语法指定前瞻匹配:

(?=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"]

稍微好一点的RexExp-Only解决方案

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

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

基本上这个解决方案涉及首先匹配(sortof)看起来像括号组的东西:

// 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]; }

一旦获得所有这些内容,您就会检查它们是无效标签(/\(+.+?\)+/g ((n))等)还是好标签:

(((n)))

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

http://jsfiddle.net/Cb5aG/

答案 1 :(得分:4)

回答您的修改

所以你要替换!这意味着你的问题实际上等同于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),让群组1undefined

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


关于匹配的原始答案:

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

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

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

所以我们将它从第一场比赛中删除,将数字后的所有内容放入前瞻:

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

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

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

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


答案 2 :(得分:2)

这是一个负面的前瞻,然后是一个负向前瞻:

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

Regular expression image

Edit live on Debuggex

答案 3 :(得分:1)

这就是你想要的吗?

"(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)"]