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

guy*_*sey 2 javascript regex

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

(1)
Run Code Online (Sandbox Code Playgroud)

但是,应该不匹配(1)这里面:

((1))
Run Code Online (Sandbox Code Playgroud)

本来我试过这个:

([^\(])\(([0-9]+)\)([^\)])
Run Code Online (Sandbox Code Playgroud)

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

然后我尝试了这个:

([^\(]?)\(([0-9]+)\)([^\)]?)
Run Code Online (Sandbox Code Playgroud)

这成功匹配(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'[理想]或不匹配

  • (1)((2)) ==>匹配'(1)'并捕获'1'[理想]或不匹配

  • ((1)(2)) ==>匹配'(1)'和'(2)'并捕获'1'和'2'[理想]或不匹配

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

Eth*_*own 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;
}
Run Code Online (Sandbox Code Playgroud)

你可以在这里看到这个解决方案:http://jsfiddle.net/SK5ee/3/

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

原创讨论和朴素的解决方案

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

a) "(1)(2)((3))" => [1,2]
b) " (5) ((7)) (8) " => [5,8]
Run Code Online (Sandbox Code Playgroud)

不清楚的是当括号不平衡时,或者括号内的数字不仅仅是数字时会发生什么.JavaScript正则表达式中不支持均衡匹配,因此以下情况会导致问题:

"((3) (2)" => [2] (probably we want [3,2]???)
"((3) (2) (4) (5))" => [2,4] (probably we want [3,2,4,5]???)
Run Code Online (Sandbox Code Playgroud)

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

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

我们现在将忽略第二个问题,把重点放在两者中更难的问题上.

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

" (1)(2)((3)) ".match(/[^(]\(\d+/g)   => [" (1", ")(2"]
Run Code Online (Sandbox Code Playgroud)

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

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

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

" (1) (2) ((3)) ".match(/[^(]\(\d+\)/g) => [" (1)", " (2)"]
Run Code Online (Sandbox Code Playgroud)

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

" (1)(2)((3)) ".match(/[^(]\(\d+\)/g) => [" (1)"]
Run Code Online (Sandbox Code Playgroud)

那么,我们需要匹配那个右括号,但不要消耗它.这就是"先行"匹配背后的整个想法(有时称为"零宽度断言").我们的想法是确保它在那里,但是你没有将它作为匹配的一部分包含在内,因此它不会阻止该角色被包含在将来的比赛中.在JavaScript中,使用以下(?=subexpression)语法指定前瞻匹配:

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

好的,这样才能解决这个问题!关于如何处理在字符串的开头/结尾发生的匹配的更容易的问题.实际上,我们所要做的就是使用交替来说"匹配不是左括号或字符串开头的东西",等等:

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

另一种"偷偷摸摸"的方法是填充输入字符串以完全避免问题:

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

这样我们就不必为交替而烦恼.

好吧,这是一个疯狂的长期答案,但我将用如何清理我们的输出结束.显然,我们不希望那些带有我们不想要的额外匹配垃圾的字符串:我们只想要数字.有很多方法可以实现这一目标,但这是我的最爱:

// 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]; }
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

/\(+.+?\)+/g
Run Code Online (Sandbox Code Playgroud)

一旦你得到所有这些,你检查,看看他们是否无效标签(((n)),(((n))),等),或好的:

if( s.match(/\(\(\d+\)\)/) ) return null;
return s.match(/\(\d+\)/);
Run Code Online (Sandbox Code Playgroud)

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

http://jsfiddle.net/Cb5aG/