[英]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做了我所期望的。
注意 - 標題主要用於想要了解其工作原理(不是為什么)的用戶的可搜索性。
當我使用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}
可能會被優化掉,讓我們嘗試更有趣的事情: \\d{2}{3}
。 這仍然只匹配兩個字符(不是六個), {3}
被忽略。 (\\d{1})({2})
。 奇怪的是,這是有效的。 第二組$2
捕獲空字符串。 ({1})
怎么樣? 仍然有效。 {1}
? 沒問題。 大! 所以{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()
,因為它遇到{
立即,它從switch
和for
循環斷開,並且創建了一個長度為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.