2

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

(1)

但它不应该内部匹配(1)

((1))

最初我试过这个:

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

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

然后我尝试了这个:

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

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

我会继续试验,如果我找到一个解决方案,我会在这里发布一个解决方案,但任何帮助都将不胜感激。有任何想法吗?

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


更新:

当匹配很重要时,我没有明确指出在括号内捕获数字很重要。(我希望这不会对下面给出的解决方案产生不利影响,除了使它们更难阅读!)但是,整个 of(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' [理想情况下] 或不匹配

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

For these last three, I say 'ideally' because there is leniency. The first result is the preferred one but, if that isn't possible, I can live with there being no match at all. I realise this is something of a challenge (maybe even impossible, within JavaScript's RegExp limitations) but that's why I'm putting the question to this expert forum.

4

4 回答 4

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)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-Only 解决方案

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

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

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

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

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

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

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

http://jsfiddle.net/Cb5aG/

于 2013-07-03T19:00:24.743 回答
4

回答您的编辑

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

  • 要么匹配((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中,如果更方便的话,可以使用它。


原始答案,关于匹配:

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

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

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

因此,我们将其从第一个匹配中删除,方法是将数字后面的所有内容放入前瞻中:

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

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

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

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


于 2013-07-03T19:15:55.220 回答
2

这是一个负面的展望,然后是一个负面的展望:

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

正则表达式图片

在 Debuggex 上实时编辑

于 2013-07-03T19:07:49.767 回答
1

这是你想要的吗?

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

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

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

好吧,js regexp 中存在一个限制,这将使代码难以承受,所以我会做一些稍微不同的事情来获得所需的结果:

 "(1)(2)((3))".match(/(\({1,}\d+\){1,})/g)
  .filter(/./.test, /^\(\d\)$/) // == ["(1)", "(2)"]
于 2013-07-03T19:07:23.670 回答