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

时间:2013-09-23 12:01:52

标签: java regex

所以,某种方式(玩弄),我发现自己有一个像\d{1}{2}这样的正则表达式。

逻辑上,对我来说,它应该意味着:

  

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

但事实上,它似乎只是意味着“一个数字恰好一次”(因此忽略了{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}或类似的结果可以看到类似的结果。

为什么会这样?它是在regex / Java文档中明确说明的,还是只是Java开发人员即时做出的决定,还是可能是一个bug?

或者它实际上没有被忽略,它实际上完全意味着什么呢?

并不重要,但并非全面的正则表达式行为,Rubular符合我的预期。

注意 - 标题主要用于想要了解其工作原理的用户(不是为什么)的可搜索性。

7 个答案:

答案 0 :(得分:108)

IEEE-Standard 1003.1说:

  

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

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

答案 1 :(得分:76)

当我使用Java正则表达式语法在RegexBuddy中输入正则表达式时,它会显示以下消息

  

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

的标记

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


我认为java正则表达式引擎只是忽略了错误/表达式,并使用到目前为止编译的内容。

修改

IEEE-Standard中对@piet.t's answer的引用似乎支持这一假设。

编辑2 (感谢@fncomp)

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

答案 2 :(得分:10)

科学方法:
单击模式以查看regexplanet.com上的示例,并单击绿色Java按钮

  • 您已经显示\d{1}{2}次匹配"1",但与"12"不匹配,因此我们知道它不会被解释为{{1 }}。
  • 仍然,1是一个无聊的数字,(?:\d{1}){2} 可能被优化掉,让我们尝试更有趣的事情:
    \d{2}{3}。这仍然只匹配两个字符(而不是六个),{1}被忽略。
  • 确定。有一种简单的方法可以查看正则表达式引擎的功能。是否捕获? 让我们试试(\d{1})({2})。奇怪的是,这是有效的。第二组{3}捕获空字符串。
  • 那么为什么我们需要第一组呢? ({1})怎么样?仍然有效。
  • 只是{1}?没问题。
    看起来Java在这里有点奇怪。
  • 大!所以$2是有效的。我们知道Java expands * and + to {0,0x7FFFFFFF} and {1,0x7FFFFFFF}{1}*也会工作吗?号:

      

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

    必须在+*展开之前进行验证。

我没有在规范中找到任何解释的内容,看起来就像量词必须至少在一个字符,括号或括号之后出现。

大多数这些模式被其他正则表达式的味道视为无效,并且有充分的理由 - 它们没有意义。

答案 3 :(得分:4)

起初我很惊讶这不会引发PatternSyntaxException

我无法根据任何事实得出答案,所以这只是一个有根据的猜测:

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

答案 4 :(得分:4)

我从未在任何地方看到{m}{n}语法。看来,此Rubular页面上的正则表达式引擎将{2}量词应用于此前最小的可能令牌 - 即\\d{1}。要在Java(或其他大多数其他正则表达式引擎)中模仿这个,你需要将\\d{1}分组,如下所示:

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

in action here

答案 5 :(得分:4)

正则表达式的编译结构

Kobi's answer关于案例"^\\d{1}{2}$""{1}"的Java正则表达式(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()来解析原子,然后通过调用closure()将量词附加到原子,并将所有原子与闭合链在一起成为一个序列。

例如,鉴于此正则表达式:

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

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

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

@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循环中断,而会在长度为0的新切片中断已创建(长度来自first,为0)。

当返回此切片时,量词由closure()解析,从而得到我们所看到的。

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

正则表达式标准

top-voted answer引用IEEE-Standard 1003.1(或POSIX标准)与讨论无关,因为Java 未实现 BRE和ERE。

根据标准,有许多语法导致未定义的行为,但是在许多其他正则表达式风格中是明确定义的行为(尽管它们是否同意是另一个问题)。例如,\d根据标准未定义,但它匹配许多正则表达式中的数字(ASCII / Unicode)。

可悲的是,正则表达式语法没有其他标准。

然而,Unicode正则表达式有一个标准,它关注Unicode正则表达式引擎应该具有的功能。 Java Pattern类或多或少地实现了UTS #18: Unicode Regular Expression和RL2.1中描述的Level 1支持(虽然非常错误)。

答案 6 :(得分:0)

我猜测{}的定义类似于“回头查找有效表达式(不包括我自己 - {}”,所以在您的示例中}{之间没有任何内容{{1}}。

无论如何,如果你将它包装在括号中,它将按预期工作:http://refiddle.com/gv6