[英]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.