78

所以,以某种方式(玩弄),我发现自己有一个像\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}使用或类似可以看到类似的结果。

为什么会这样?它是在某个地方的正则表达式/Java文档中明确说明,还是只是Java开发人员即时做出的决定,或者它可能是一个错误?

或者它实际上没有被忽视,它实际上完全意味着其他东西?

并不是说它很重要,但这不是全面的正则表达式行为,Rubular符合我的期望。

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

4

7 回答 7

108

IEEE 标准 1003.1说:

多个相邻重复符号(“*”和间隔)的行为会产生未定义的结果。

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

于 2013-09-23T12:10:56.940 回答
76

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

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

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


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

编辑

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

编辑 2 (感谢@fncomp)

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

于 2013-09-23T12:09:10.883 回答
10

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

  • 您已经显示了\d{1}{2}match "1",并且 doesn't match "12",所以我们知道它不会被解释为(?:\d{1}){2}
  • 尽管如此,1 是一个无聊的数字,并且{1} 可能会被优化掉,让我们尝试一些更有趣的东西:
    \d{2}{3}. 这仍然只匹配两个字符(不是六个),{3}被忽略。
  • 好的。有一种简单的方法可以查看正则表达式引擎的功能。它捕捉到了吗?
    让我们试试(\d{1})({2})。奇怪的是,这行得通。第二组,$2,捕获空字符串。
  • 那么为什么我们需要第一组呢?怎么样({1})?仍然有效。
  • 而只是{1}?那里没问题。
    看起来Java在这里有点奇怪。
  • 伟大的!所以{1}是有效的。我们知道Java 可以扩展为*and ,+{0,0x7FFFFFFF}{1,0x7FFFFFFF}所以会*or+工作吗?不:

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

    验证必须先于*并被+扩展。

我在规范中没有找到任何解释这一点的东西,看起来量词至少必须出现在字符、方括号或圆括号之后。

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

于 2013-09-24T07:29:38.497 回答
4

起初我很惊讶这不会抛出PatternSyntaxException.

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

"\\d{1}"    // matches a single digit
"\\d{1}{2}" // matches a single digit followed by two empty strings
于 2013-09-23T12:10:36.070 回答
4

我从未在{m}{n}任何地方看到过这种语法。似乎此 Rubular 页面上的正则表达式引擎将{2}量词应用于之前的最小可能标记 - 即\\d{1}. 要在 Java(或大多数其他正则表达式引擎,看起来)中模仿这一点,您需要对\\d{1}类似的内容进行分组:

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

在这里查看它的实际应用。

于 2013-09-23T12:12:22.047 回答
4

正则表达式的编译结构

Kobi 的回答是关于 Java 正则表达式(Sun/Oracle 实现)对于 case"^\\d{1}{2}$""{1}".

下面是内部编译的结构"^\\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-b132sequence()复制的源代码(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(). 似乎这个错误从一开始就存在。

正则表达式的标准

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

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

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

然而,有一个关于 Unicode 正则表达式的标准,它侧重于 Unicode 正则表达式引擎应该具有的功能。JavaPattern类或多或少地实现了 1 级支持,如UTS #18:Unicode 正则表达式和 RL2.1 中所述(尽管有很多错误)。

于 2015-04-21T11:21:26.073 回答
0

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

无论如何,如果你把它放在括号中,它会按你的预期工作:http ://refiddle.com/gv6 。

于 2013-09-23T12:08:45.377 回答