簡體   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