繁体   English   中英

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

[英]How does {m}{n} (“exactly n times” twice) work?

所以,某种方式(玩弄),我发现自己有一个像\\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做了我所期望的。

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

IEEE标准1003.1说:

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

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

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

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

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


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

编辑

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

编辑2 (感谢@fncomp)

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

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

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

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

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

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

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

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

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

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

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

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

在这里看到它。

正则表达式的编译结构

对于案例"^\\\\d{1}{2}$""{1}"Kobi的答案是关于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()将量词附加到atom,并将所有atoms-with-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()中的case列表中不存在,并且编译过程将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从一开始就存在。

正则表达的标准

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM