所以,某种方式(玩弄),我发现自己有一个像\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符合我的预期。
注意 - 标题主要用于想要了解其工作原理的用户(不是为什么)的可搜索性。
答案 0 :(得分:108)
答案 1 :(得分:76)
当我使用Java正则表达式语法在RegexBuddy中输入正则表达式时,它会显示以下消息
量词必须前面有一个可以重复«{2}»
的标记
更改正则表达式以明确使用分组^(\d{1}){2}
可以解决该错误并按预期工作。
我认为java正则表达式引擎只是忽略了错误/表达式,并使用到目前为止编译的内容。
修改强>
IEEE-Standard中对@piet.t's answer的引用似乎支持这一假设。
编辑2 (感谢@fncomp)
为了完整性,通常会使用(?:)
来避免捕获该组。完整的正则表达式然后变为^(?:\d{1}){2}
答案 2 :(得分:10)
科学方法:
单击模式以查看regexplanet.com上的示例,并单击绿色Java按钮。
\d{1}{2}
次匹配"1"
,但与"12"
不匹配,因此我们知道它不会被解释为{{1 }}。(?:\d{1}){2}
可能被优化掉,让我们尝试更有趣的事情:\d{2}{3}
。这仍然只匹配两个字符(而不是六个),{1}
被忽略。(\d{1})({2})
。奇怪的是,这是有效的。第二组{3}
捕获空字符串。({1})
怎么样?仍然有效。{1}
?没问题。大!所以$2
是有效的。我们知道Java expands *
and +
to {0,0x7FFFFFFF}
and {1,0x7FFFFFFF}
,{1}
或*
也会工作吗?号:
在索引0附近悬挂元字符'+' +
^
必须在+
和*
展开之前进行验证。
我没有在规范中找到任何解释的内容,看起来就像量词必须至少在一个字符,括号或括号之后出现。
大多数这些模式被其他正则表达式的味道视为无效,并且有充分的理由 - 它们没有意义。
答案 3 :(得分:4)
起初我很惊讶这不会引发PatternSyntaxException
。
我无法根据任何事实得出答案,所以这只是一个有根据的猜测:
"\\d{1}" // matches a single digit
"\\d{1}{2}" // matches a single digit followed by two empty strings
答案 4 :(得分:4)
我从未在任何地方看到{m}{n}
语法。看来,此Rubular页面上的正则表达式引擎将{2}
量词应用于此前最小的可能令牌 - 即\\d{1}
。要在Java(或其他大多数其他正则表达式引擎)中模仿这个,你需要将\\d{1}
分组,如下所示:
^(\\d{1}){2}$
答案 5 :(得分:4)
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*
的已编译节点,$
并将它们链接在一起。
考虑到这个想法,让我们看一下从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()
中的案例列表中没有它,编译过程将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从一开始就存在。
top-voted answer引用IEEE-Standard 1003.1(或POSIX标准)与讨论无关,因为Java 未实现 BRE和ERE。
根据标准,有许多语法导致未定义的行为,但是在许多其他正则表达式风格中是明确定义的行为(尽管它们是否同意是另一个问题)。例如,\d
根据标准未定义,但它匹配许多正则表达式中的数字(ASCII / Unicode)。
可悲的是,正则表达式语法没有其他标准。
然而,Unicode正则表达式有一个标准,它关注Unicode正则表达式引擎应该具有的功能。 Java Pattern
类或多或少地实现了UTS #18: Unicode Regular Expression和RL2.1中描述的Level 1支持(虽然非常错误)。
答案 6 :(得分:0)
我猜测{}
的定义类似于“回头查找有效表达式(不包括我自己 - {}
”,所以在您的示例中}
和{
之间没有任何内容{{1}}。
无论如何,如果你将它包装在括号中,它将按预期工作:http://refiddle.com/gv6。