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


77

因此,以某种方式(玩转),我发现自己使用了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开发人员即时做出的决定,还是可能是一个错误?

还是实际上不被忽略而实际上意味着完全其他的东西?

并不是很重要,但这不是全面的正则表达式行为,Rubular会执行我所期望的。

注意-标题主要是为希望了解其工作原理(而非原因)的用户提供可搜索性。


57
您的模式表示(一个数字正好一次)后跟(什么都不正好两次)。
GOTO

3
如果有帮助,则pcregrepMathematica都为regex这样的正则表达式给出错误pcregrep: Error in command-line regex at offset 8: nothing to repeat。我要么只使用{m*n},要么我会使用(?:\\d{1}){2},这是明确的。
杰里米2013年

1
我不明白你为什么不能只使用\d{2}?您要达到的目标有什么不同?
卡洛斯·坎德罗斯

5
@CarlosCampderrós好吧,我真正想要实现的唯一一件事就是更好地了解正则表达式。这个问题是理论上更多的问题,我有兴趣了解为什么它会以这种方式工作,而不是找到一个适合该示例的正则表达式。
Bernhard Barker

2
@Kaz完全没有:Java中的卷曲重复仅适用于单个节点(包括空节点)或组,不适用于其他重复。matchRoot如果您不相信我,可以创建该模式并使用调试器进行检查。查看该方法的源代码Pattern.closure还将为您提供一些见解。
GOTO

Answers:


76

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

量词之前必须带有可重复«{2}»的标记

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


我假设java regex引擎只是忽略了错误/表达式,并且可以使用到目前为止已编译的东西。

编辑

在参考IEEE-标准@ piet.t的答案似乎支持这一假设。

编辑2 (对@fncomp表示感谢)

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


如果\d{1}{2}不是意味着(\d{1}){2}什么,那是什么意思呢?如果关联性不是从左到右,则它必须是从右到左,因此它表示\d({1}{2}),除非我们定义将这两个括号运算符合并在一起的含义,否则它是没有意义的。
卡兹(Kaz)2013年

@Kaz-OP的测试表明,没有使用Java的正则表达式引擎评估第二个重复符号。我相信piet.t是正确的,每个实现都可以按照自己的意愿做。
Lieven Keersmaekers 2013年

4
会不会^(:?\d{1}){2}$更精确地再现意图?(为了避免捕获。)
fncomp

1
@fncomp-的确如此,这也是我所使用的。小错别字-应该是(?: )
Kobi

@fncomp-我一直在自己摸索。明智的选择是更好的性能,但是不够简洁。按照意图,结果是相同的,所以没有打扰我。为了完整性,我已将您的评论添加到答案中。
Lieven Keersmaekers

108

IEEE标准1003.1说:

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

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


1
+1,但是您知道Java是否正式符合此标准?
Bernhard Barker 2013年

2
是,因为输出结果在标准上是有效的,即:它可以做任何事情。
STT LCU 2013年

2
@Dukeling我也相信。通知也System.out.println("".matches("^{1}$"));将返回true。我敢打赌,如果Java无法找到有效的模式来重复,它将重复null而不是抛出错误(该错误与字符串中的任何地方匹配)。此外,您还为Java使用了基于Ruby的正则表达式测试器!
杰里

3
@STTLCU好吧,正式与不正式或不遵守之间有区别。正式合规意味着可以引用它作为源,否则它仍然是不错的参考,但不一定解释Java为什么要这样做。
Bernhard Barker 2013年

3
我非常确定此标准适用于POSIX BRE和ERE,并且与Java正则表达式无关。Java甚至不声称支持ERE或BRE!如果有的话,应在此处引用Unicode正则表达式unicode.org/reports/tr18
nhahtdh 2015年

10

科学方法:
单击模式以查看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
    +
    ^附近悬挂元字符'+'

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

我在规范中没有找到任何可以解释的内容,看起来量词至少必须在字符,方括号或括号之后。

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


4

起初我很惊讶这并没有引发PatternSyntaxException

我不能以任何事实为根据,所以这只是有根据的猜测:

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

4

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

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

看到它在这里行动


4

正则表达式的编译结构

KOBI的答案是当场就有关Java正则表达式的情况下的行为(太阳/ Oracle实施)"^\\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

看源代码

根据我的调查,该错误可能是由于{未在private方法中正确检查这一事实造成的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(),编译过程将按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的新切片(长度为from first,它为0)。

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

比较Java 1.4.0,Java 5和Java 8的源代码,sequence()和的源代码似乎没有太大变化atom()。从一开始似乎就存在此错误。

正则表达式的标准

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

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

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

但是,存在Unicode正则表达式的标准,该标准着重于Unicode正则表达式引擎应具有的功能。JavaPattern类或多或少实现了UTS#18:Unicode正则表达式和RL2.1中描述的1级支持(尽管存在很多错误)。


0

我猜想在定义中{}类似“回头寻找有效的表达式(不包括我- {}),因此在您的示例中,}和之间没有任何关系{

无论如何,如果将其用括号括起来,它将按预期工作:http : //refiddle.com/gv6

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.