【问题标题】:How does {m}{n} ("exactly n times" twice) work?{m}{n}(“恰好 n 次”两次)如何工作?
【发布时间】:2013-09-28 07:40:18
【问题描述】:

所以,不知何故(玩弄),我发现自己有一个像\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 符合我的预期。

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

【问题讨论】:

  • 你的模式意味着(一个数字恰好一次)后跟(没有正好两次)。
  • 如果有帮助,pcregrepMathematica 都会为这个正则表达式提供错误,例如 pcregrep: Error in command-line regex at offset 8: nothing to repeat。我要么只使用{m*n},要么使用(?:\\d{1}){2},这是明确的。
  • 我不明白你为什么不能直接使用\d{2}?你想要达到的目标有什么不同吗?
  • @CarlosCampderrós 好吧,我真正想要实现的唯一目标是更好地理解正则表达式。这个问题更具理论性,我有兴趣找出它为什么以它的方式工作,而不是找到一个适用于该示例的正则表达式。
  • @Kaz 一点也不:Java 中的卷曲重复仅适用于单个节点(包括空节点)或组,不适用于其他重复。如果您不相信我,您可以创建该模式并使用调试器检查其matchRoot。看看Pattern.closure方法的源码,也会给你一些启示。

标签: java regex


【解决方案1】:

IEEE-Standard 1003.1 说:

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

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

【讨论】:

  • +1,但是你知道Java官方是否符合这个标准吗?
  • 是的,因为输出结果在标准上是有效的,即:它可以做任何事情。
  • @Dukeling 我也这么认为。注意System.out.println("".matches("^{1}$")); 也返回true。我敢打赌,如果 Java 找不到要重复的有效模式,它将重复 null 而不是抛出错误(匹配字符串中的任何位置)。另外,您使用了基于 Ruby 的 Java 正则表达式测试器!?
  • @STTLCU 好吧,官方和非官方遵守或不遵守之间是有区别的。官方遵守意味着它可以被引用为来源,否则它仍然是一个很好的参考,但不一定能解释为什么 Java 会这样做。
  • 我很确定这个标准适用于 POSIX BRE 和 ERE,它与 Java 正则表达式没有任何关系。 Java 甚至没有声称支持 ERE 或 BRE!如果有的话,这里应该引用 Unicode 正则表达式 unicode.org/reports/tr18
【解决方案2】:

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

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

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


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

编辑

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

编辑 2 (感谢@fncomp)

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

【讨论】:

  • 如果\d{1}{2} 不代表(\d{1}){2},那是什么意思呢?如果关联性不是从左到右,那么它一定是从右到左,所以它的意思是\d({1}{2}),除非我们定义聚集两个括号运算符的含义,否则这是没有意义的。
  • @Kaz - OP 的测试表明第二个重复符号没有使用 Java 的正则表达式引擎进行评估。我相信 piet.t 是正确的,因为每个实现都可以随心所欲。
  • ^(:?\d{1}){2}$ 不是更精确地再现了意图吗? (为了避免被捕获。)
  • @fncomp - 会的,这也是我使用的。虽然是小错字 - 应该是 (?: )
  • @fncomp - 我自己一直在涉足这个问题。它的性能更好,但并不简洁。按照意图,结果是一样的,所以这并没有打扰我。为了完整起见,我已将您的评论添加到答案中。
【解决方案3】:

科学方法:
点击模式查看 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 expands * and + to {0,0x7FFFFFFF} and {1,0x7FFFFFFF},那么*+ 会起作用吗?没有:

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

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

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

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

【讨论】:

    【解决方案4】:

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

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

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

    【讨论】:

      【解决方案5】:

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

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

      in action here

      【讨论】:

        【解决方案6】:

        正则表达式的编译结构

        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*$ 的编译节点并将它们链接在一起。

        带着这个想法,让我们看看sequence()的源代码,复制自OpenJDK 8-b132(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列表中并不存在,编译过程会通过defaultcase直接到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 类或多或少地实现了 1 级支持,如 UTS #18: Unicode Regular Expression 和 RL2.1 中所述(尽管有很多错误)。

        【讨论】:

          【解决方案7】:

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

          不管怎样,如果你把它放在括号里,它会像你预期的那样工作:http://refiddle.com/gv6

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-11-25
            • 1970-01-01
            • 2012-12-02
            • 2018-09-20
            • 2015-01-18
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多