{m} {n}(“正好n次”两次)是如何工作的?

所以,不pipe怎么样,我都会用\d{1}{2}这样的正则expression式。

从逻辑上讲,对我来说,这应该是:

(一个数字恰好一次)恰好两次,即一个数字恰好两次。

但事实上,它似乎只是“数字一次”(因此忽略了{2} )。

 String regex = "^\\d{1}{2}$"; // ^$ to make those not familiar with 'matches' happy System.out.println("1".matches(regex)); // true System.out.println("12".matches(regex)); // false 

使用{n}{m,n}或类似的结果可以看出类似的结果。

为什么会这样呢? 它是否在正则expression式/ Java文档中明确表示,还是只是Java开发人员在飞行中做出的决定,还是可能是一个错误?

或者它实际上是不被忽视的,它实际上意味着其他的东西呢?

这并不重要,但它不是全面的正则expression式, Rubular按我所期望的做了。

注 – 标题主要是为了想知道它是如何工作(而不是为什么)的用户的可search性。

当我使用Java正则expression式在RegexBuddy中input正则expression式时,它会显示以下消息

量词前面必须有一个可重复的标记«{2}»

更改正则expression式以显式使用分组^(\d{1}){2}可解决该错误,并按预期工作。


我假设java的正则expression式引擎简单地忽略了错误/expression式,并与迄今已编译的工作。

编辑

在@ piet.t的答案中引用IEEE标准似乎支持这一假设。

编辑2 (荣誉@fncomp)

为了完整性,通常使用(?:)来避免捕获组。 完整的正则expression式然后变成^(?:\d{1}){2}

IEEE-Standard 1003.1说:

多个相邻复制符号('*'和区间)的行为会产生未定义的结果。

所以每个实现都可以随心所欲,只是不要依赖任何具体的东西。

科学方法:
点击模式在regexplanet.com上查看示例,然后单击绿色的Javabutton

  • 您已经显示\d{1}{2}匹配"1" ,并且不匹配"12" ,所以我们知道它不会被解释为(?:\d{1}){2}
  • 尽pipe如此,1是一个无聊的数字, {1} 可能会被优化,让我们尝试一些更有趣的事情:
    \d{2}{3} 。 这仍然只匹配两个字符(不是六个), {3}被忽略。
  • 好。 有一个简单的方法来看看一个正则expression式引擎做什么。 它捕获?
    让我们试试(\d{1})({2}) 。 奇怪的是,这个工程。 第二组$2捕获空string。
  • 那么为什么我们需要第一组呢? ({1})怎么样? 仍然有效。
  • 只是{1} ? 那里没问题。
    看起来Java在这里有点奇怪。
  • 大! 所以{1}是有效的。 我们知道Java把*+扩展到{0,0x7FFFFFFF}{1,0x7FFFFFFF} ,所以*或者+工作吗? 没有:

    在索引0附近悬挂元字符“+”
    +
    ^

    validation必须在*+扩展之前进行。

在规范中我没有find任何解释的东西, 看起来量词必须至less出现在字符,括号或括号之后。

大多数这些模式被认为是无效的其他正则expression式,并有一个很好的理由 – 他们没有意义。

起初我很惊讶,这不会抛出一个PatternSyntaxException

我不能根据任何事实来回答,所以这只是一个有教养的猜测:

 "\\d{1}" // matches a single digit "\\d{1}{2}" // matches a single digit followed by two empty strings 

我从来没有见过{m}{n}语法。 似乎这个Rubular页面上的正则expression式引擎将{2}量词应用到最小的可能标记之前 – 这是\\d{1} 。 为了在Java(或大多数其他正则expression式引擎)中模拟这一点,您需要像这样对\\d{1}进行分组:

 ^(\\d{1}){2}$ 

在这里看到它的行动 。

编译正则expression式的结构

对于"^\\d{1}{2}$""{1}"的情况, Kobi的回答是关于Java正则expression式(Sun / Oracle实现)的行为。

以下是"^\\d{1}{2}$"的内部编译结构:

 ^\d{1}{2}$ Begin. \A or default ^ Curly. Greedy quantifier {1,1} Ctype. POSIX (US-ASCII): DIGIT Node. Accept match Curly. Greedy quantifier {2,2} Slice. (length=0) Node. Accept match Dollar(multiline=false). \Z or default $ java.util.regex.Pattern$LastNode Node. Accept match 

看着源代码

从我的调查,这个错误可能是由于这个事实{私人方法sequence()没有正确检查。

方法sequence()调用atom()来parsingprimefaces,然后通过调用closure()将定量符附加到primefaces上,并将所有的primefaces与闭合一起链接成一个序列。

例如,给定这个正则expression式:

 ^\d{4}a(bc|gh)+d*$ 

然后,对sequence()的顶层调用将接收^\d{4}a(bc|gh)+d*$的编译节点并将它们链接在一起。

考虑到这个想法,让我们看一下从OpenJDK 8-b132 (Oracle使用相同的代码库)复制的sequence()的源代码:

 @SuppressWarnings("fallthrough") /** * Parsing of sequences between alternations. */ private Node sequence(Node end) { Node head = null; Node tail = null; Node node = null; LOOP: for (;;) { int ch = peek(); switch (ch) { case '(': // Because group handles its own closure, // we need to treat it differently node = group0(); // Check for comment or flag group if (node == null) continue; if (head == null) head = node; else tail.next = node; // Double return: Tail was returned in root tail = root; continue; case '[': node = clazz(true); break; case '\\': ch = nextEscaped(); if (ch == 'p' || ch == 'P') { boolean oneLetter = true; boolean comp = (ch == 'P'); ch = next(); // Consume { if present if (ch != '{') { unread(); } else { oneLetter = false; } node = family(oneLetter, comp); } else { unread(); node = atom(); } break; case '^': next(); if (has(MULTILINE)) { if (has(UNIX_LINES)) node = new UnixCaret(); else node = new Caret(); } else { node = new Begin(); } break; case '$': next(); if (has(UNIX_LINES)) node = new UnixDollar(has(MULTILINE)); else node = new Dollar(has(MULTILINE)); break; case '.': next(); if (has(DOTALL)) { node = new All(); } else { if (has(UNIX_LINES)) node = new UnixDot(); else { node = new Dot(); } } break; case '|': case ')': break LOOP; case ']': // Now interpreting dangling ] and } as literals case '}': node = atom(); break; case '?': case '*': case '+': next(); throw error("Dangling meta character '" + ((char)ch) + "'"); case 0: if (cursor >= patternLength) { break LOOP; } // Fall through default: node = atom(); break; } node = closure(node); if (head == null) { head = tail = node; } else { tail.next = node; tail = node; } } if (head == null) { return end; } tail.next = end; root = tail; //double return return head; } 

注意行throw error("Dangling meta character '" + ((char)ch) + "'"); 。 这是错误发生的地方,如果+* ? 是悬挂的,不是前面的标记的一部分。 正如你所看到的, {不是在抛出错误的情况下。 实际上,它并不存在于sequence()中的案例列表中,编译过程将default情况下直接转换为atom()

 @SuppressWarnings("fallthrough") /** * Parse and add a new Single or Slice. */ private Node atom() { int first = 0; int prev = -1; boolean hasSupplementary = false; int ch = peek(); for (;;) { switch (ch) { case '*': case '+': case '?': case '{': if (first > 1) { cursor = prev; // Unwind one character first--; } break; // Irrelevant cases omitted // [...] } break; } if (first == 1) { return newSingle(buffer[0]); } else { return newSlice(buffer, first, hasSupplementary); } } 

当进程进入atom() ,由于它立即遇到{ ,它从switchfor循环中断开,并创build一个长度为0的新片 (长度从0开始)。

当这个片返回时,量化器被closure()parsing,导致我们看到的东西。

比较Java 1.4.0,Java 5和Java 8的源代码, sequence()atom()的源代码似乎没有太多变化。 看来这个bug从一开始就一直存在。

正则expression式的标准

引用IEEE-Standard 1003.1 (或POSIX标准)的最高票数的答案与讨论无关,因为Java 没有实现 BRE和ERE。

根据标准,有很多语法会导致未定义的行为,但在许多其他正则expression式中却是定义良好的行为(尽pipe他们是否同意是另一回事)。 例如,根据标准, \d是未定义的,但它匹配很多正则expression式中的数字(ASCII / Unicode)。

可悲的是,在正则expression式语法上没有其他标准。

然而,Unicode正则expression式有一个标准,它着重于一个Unicode正则expression式引擎应具有的function。 Java Pattern类或多或less地实现了UTS#18:Unicode正则expression式和RL2.1(尽pipe非常麻烦)中描述的1级支持。

我猜测在{}定义中是“回顾寻找有效的expression式(不包括我自己 – {} ”),所以在你的例子中, }{之间没有任何关系。

无论如何,如果你用圆括号包装它,它会按照你的预期工作: http : //refiddle.com/gv6 。