# 量词

量词允许您指定要在匹配中出现的次数。为方便起见,Pattern API 规范描述的贪婪,不情愿,和占有量词如下所示。 乍一看,它可能会出现的量词 X?,X?? 和 X?+ 做同样的事,因为他们都承诺匹配 ”X,一次或根本没有”。 有细微的差异,将在本节结尾附近加以解释

贪婪 不情愿的 占有 含义
X? X?? X?+ X,一次或根本没有
X* X*? X*+ X,零次或多次
X+ X+? X++ X,一次或多次
X{n} X{n}? X{n}+ X,正好是n时间
X{n,} X{n,}? X{n,}+ X,至少n时间
X{n,m} X{n,m}? X{n,m}+ X,至少n但不超过m次数

让我们从创建三个不同的正则表达式开始我们看看贪婪量词:字母 “A”,然后要么?,* 或 +。 让我们看看当这些表达式对输入空字符("")串进行测试时会发生什么:

---- Test code ----
System.out.println("=== a?");
regexTest("a?", "");
System.out.println("=== a*");
regexTest("a*", "");
System.out.println("=== a+");
regexTest("a+", "");

---- Output ----
=== a?
我发现文本中的 "" 在开始索引 0 和 结束索引 0.
=== a*
我发现文本中的 "" 在开始索引 0 和 结束索引 0.
=== a+
No match found.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 零长度匹配

在上述示例中,匹配在前两种情况下成功,因为表达式 a? 和 a* 两者都允许零出现该字母 a。 您还会注意到,起始和结束索引都为零,这与我们迄今为止看到的任何示例不同。空输入字符串 "" 没有长度, 因此测试在索引 0 上完全不匹配。这种匹配被称为零长度匹配。在几个情况下可能会发生零长度匹配:

  1. 在输入字符串的开头,
  2. 输入字符串的最后一个字符之后
  3. 输入字符串的任意两个字符之间的空输入字符串。

零长度匹配很容易识别,因为它们总是在相同的索引位置开始和结束。

我们再来一些例子来探索零长度的匹配。将输入字符串更改为单个字母 “a”,您会注意到有趣的东西:

---- Test code ----
System.out.println("=== a?");
regexTest("a?", "a");
System.out.println("=== a*");
regexTest("a*", "a");
System.out.println("=== a+");
regexTest("a+", "a");

---- Output ----
=== a?
我发现文本中的 "a" 在开始索引 0 和 结束索引 1.
我发现文本中的 "" 在开始索引 1 和 结束索引 1.
=== a*
我发现文本中的 "a" 在开始索引 0 和 结束索引 1.
我发现文本中的 "" 在开始索引 1 和 结束索引 1.
=== a+
我发现文本中的 "a" 在开始索引 0 和 结束索引 1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

所有三个量词都发现字母 “a”,但前两个也在索引 1 处找到零长度匹配; 也就是说,在输入字符串的最后一个字符之后。 记住,匹配器将字符 “a” 看作是在索引 0 和索引 1 之间的单元格中,并且我们的测试工具循环, 直到它不能再找到匹配项。根据所使用的量词,在最后一个字符之后的索引处的 “无” 存在可能触发或可能不触发匹配。

现在连续五次将输入字符串更改为字母 “a”,您将得到以下内容:

---- Test code ----
System.out.println("=== a?");
regexTest("a?", "aaaaa");
System.out.println("=== a*");
regexTest("a*", "aaaaa");
System.out.println("=== a+");
regexTest("a+", "aaaaa");

---- Output ----
=== a?
我发现文本中的 "a" 在开始索引 0 和 结束索引 1.
我发现文本中的 "a" 在开始索引 1 和 结束索引 2.
我发现文本中的 "a" 在开始索引 2 和 结束索引 3.
我发现文本中的 "a" 在开始索引 3 和 结束索引 4.
我发现文本中的 "a" 在开始索引 4 和 结束索引 5.
我发现文本中的 "" 在开始索引 5 和 结束索引 5.
=== a*
我发现文本中的 "aaaaa" 在开始索引 0 和 结束索引 5.
我发现文本中的 "" 在开始索引 5 和 结束索引 5.
=== a+
我发现文本中的 "aaaaa" 在开始索引 0 和 结束索引 5.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

表达式 a? 为每个字符找到一个单独的匹配,因为当 “a” 出现零或一次时,它匹配。 该表达式 a* 找到两个单独的匹配:第一个匹配中的所有字母 “a”,然后在索引 5 后的最后一个字符之后的零长度匹配。 最后,a+ 匹配所有出现的字母“a”,忽略在最后一个索引处存在 “无”。

在这一点上,您可能会想知道如果前两个量词符遇到除 “a” 以外的字母,结果将会如何。例如,如果遇到字母 “b”,如 “ababaaaab” 那样会发生什么?

---- Test code ----
System.out.println("=== a?");
regexTest("a?", "ababaaaab");
System.out.println("=== a*");
regexTest("a*", "ababaaaab");
System.out.println("=== a+");
regexTest("a+", "ababaaaab");

---- Output ----
=== a?
我发现文本中的 "a" 在开始索引 0 和 结束索引 1.
我发现文本中的 "" 在开始索引 1 和 结束索引 1.
我发现文本中的 "a" 在开始索引 2 和 结束索引 3.
我发现文本中的 "" 在开始索引 3 和 结束索引 3.
我发现文本中的 "a" 在开始索引 4 和 结束索引 5.
我发现文本中的 "a" 在开始索引 5 和 结束索引 6.
我发现文本中的 "a" 在开始索引 6 和 结束索引 7.
我发现文本中的 "a" 在开始索引 7 和 结束索引 8.
我发现文本中的 "" 在开始索引 8 和 结束索引 8.
我发现文本中的 "" 在开始索引 9 和 结束索引 9.
=== a*
我发现文本中的 "a" 在开始索引 0 和 结束索引 1.
我发现文本中的 "" 在开始索引 1 和 结束索引 1.
我发现文本中的 "a" 在开始索引 2 和 结束索引 3.
我发现文本中的 "" 在开始索引 3 和 结束索引 3.
我发现文本中的 "aaaa" 在开始索引 4 和 结束索引 8.
我发现文本中的 "" 在开始索引 8 和 结束索引 8.
我发现文本中的 "" 在开始索引 9 和 结束索引 9.
=== a+
我发现文本中的 "a" 在开始索引 0 和 结束索引 1.
我发现文本中的 "a" 在开始索引 2 和 结束索引 3.
我发现文本中的 "aaaa" 在开始索引 4 和 结束索引 8.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

即使字母 “b” 出现在单元格 1,3 和 8 中,输出报告在这些位置的零长度匹配。正则表达式 a? 不是专门寻找字母 “b”; 它只是寻找一个字母 “a” 的存在(或缺乏)。如果量词器允许 “a” 为零的匹配,则输入字符串中不是 “a” 的任何内容将显示为零长度匹配。 剩余的 a 根据前面的例子中讨论的规则进行匹配。

要匹配一个模式正好 n 次,只需指定一组括号内的数字:

---- Test code ----
System.out.println("=== a{3}");
regexTest("a{3}", "aa");
System.out.println("=== a{3}");
regexTest("a{3}", "aaa");
System.out.println("=== a{3}");
regexTest("a{3}", "aaaa");

---- Output ----
=== a{3}
No match found.
=== a{3}
我发现文本中的 "aaa" 在开始索引 0 和 结束索引 3.
=== a{3}
我发现文本中的 "aaa" 在开始索引 0 和 结束索引 3.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这里,正则表达式 a{3} 正在搜索一行中的三个字母 “a”。第一个测试失败,因为没有足够的输入字符串来匹配。 第二个测试在输入字符串中恰好包含 3 个 a,它触发一个匹配项。第三个测试也触发一个匹配,因为在输入字符串的开头恰好有 3 个 a。

以下任何事情与第一场匹配无关。如果该模式在该点之后再次出现,则会触发后续的匹配:

---- Test code ----
regexTest("a{3}", "aaaaaaaaa");

---- Output ----
我发现文本中的 "aaa" 在开始索引 0 和 结束索引 3.
我发现文本中的 "aaa" 在开始索引 3 和 结束索引 6.
我发现文本中的 "aaa" 在开始索引 6 和 结束索引 9.
1
2
3
4
5
6
7

要求模式至少出现 n 次,在数字后添加一个逗号:

---- Test code ----
regexTest("a{3,}", "aaaaaaaaa");

---- Output ----
我发现文本中的 "aaaaaaaaa" 在开始索引 0 和 结束索引 9.
1
2
3
4
5

使用相同的输入字符串,此测试仅发现一个匹配,因为连续的 9 个 a 满足 “至少” 3 a 的需要。

最后,要指定出现次数的上限,请在大括号内添加第二个数字:

---- Test code ----
regexTest("a{3,6}", "aaaaaaaaa");

---- Output ----
我发现文本中的 "aaaaaa" 在开始索引 0 和 结束索引 6.
我发现文本中的 "aaa" 在开始索引 6 和 结束索引 9.
1
2
3
4
5
6

这里第一场匹配被迫停在 6 个角色的上限。第二场匹配包括剩下的任何东西,恰好是三个 - 这个匹配允许的最少的字符数 3。 如果输入字符串的字符短一个,则不会有第二个匹配,因为只剩下两个 a 了。

# 捕获组和字符类与量词

到目前为止,我们只测试了包含一个字符的输入字符串上的量词。事实上,量词只能一次附加到一个字符, 所以正则表达式 “abc+” 将意味着“a,其后是 b,后跟 c 一次或多次”。这不意味着 “abc” 一次或多次。 然而,量词也可以附加到 字符类捕获组,例如 [abc]+(a 或 b 或 c,一次或多次)或 (abc)+(组“abc”,一次或多次)。

---- Test code ----
System.out.println("===  (dog){3}");
regexTest("(dog){3}", "dogdogdogdogdogdog");
System.out.println("===  dog{3}");
regexTest("dog{3}", "dogdogdogdogdogdog");

---- Output ----
===  (dog){3}
我发现文本中的 "dogdogdog" 在开始索引 0 和 结束索引 9.
我发现文本中的 "dogdogdog" 在开始索引 9 和 结束索引 18.
===  dog{3}
No match found.
1
2
3
4
5
6
7
8
9
10
11
12

这里第一个例子找到三个匹配,因为量词适用于整个捕获组。删除括号,但匹配失败,因为量词 {3} 现在只适用于字母 “g”。

类似地,我们可以对整个字符类应用量词:

---- Test code ----
System.out.println("===  [abc]{3}");
regexTest("[abc]{3}", "abccabaaaccbbbc");
System.out.println("===  abc{3}");
regexTest("abc{3}", "abccabaaaccbbbc");

---- Output ----
===  [abc]{3}
我发现文本中的 "abc" 在开始索引 0 和 结束索引 3.
我发现文本中的 "cab" 在开始索引 3 和 结束索引 6.
我发现文本中的 "aaa" 在开始索引 6 和 结束索引 9.
我发现文本中的 "ccb" 在开始索引 9 和 结束索引 12.
我发现文本中的 "bbc" 在开始索引 12 和 结束索引 15.
===  abc{3}
No match found.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这里,量词 {3} 适用于第一个例子中的整个字符类,但仅适用于第二个字母的“c”。

# 贪婪,不情愿和占有量词之间的差异

贪心,不情愿和占有量词之间存在微妙的差异。

贪婪的量词被认为是 “贪心的”,因为它们在尝试第一次匹配之前迫使匹配器读取或者吃掉整个输入字符串。 如果第一个匹配尝试(整个输入字符串)失败,匹配器将输入字符串退出一个字符,然后再次尝试,重复该过程, 直到找到一个匹配,或者没有更多的字符可以退出。根据表达式中使用的量词,匹配的最后一件事是 1 或 0 个字符。

然而,不情愿的量词采取相反的方法:它们从输入字符串的开头开始,然后一次不愿意吃一个字符寻找一个匹配。 他们尝试的最后一件事是整个输入字符串。

最后,占有量词总是吃整个输入字符串,尝试一次(只有一次)进行匹配。与贪婪的量词不同,占有量词不会退回,即使这样做可以使整体匹配成功。

为了说明,考虑输入字符串 xfooxxxxxxfoo.

---- Test code ----
System.out.println("===  .*foo");
regexTest(".*foo", "xfooxxxxxxfoo");
System.out.println("===  .*?foo");
regexTest(".*?foo", "xfooxxxxxxfoo");
System.out.println("===  .*+foo");
regexTest(".*+foo", "xfooxxxxxxfoo");

---- Output ----
===  .*foo
我发现文本中的 "xfooxxxxxxfoo" 在开始索引 0 和 结束索引 13.
===  .*?foo
我发现文本中的 "xfoo" 在开始索引 0 和 结束索引 4.
我发现文本中的 "xxxxxxfoo" 在开始索引 4 和 结束索引 13.
===  .*+foo
No match found.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

第一个例子使用贪心量词 .* 来查找“任何东西”,零次或多次,后跟字母 "f" "o" "o"。因为量词是贪心的, .* 表达式的部分首先吃整个输入字符串。在这一点上,整个表达式不能成功,因为最后三个字母("f" "o" "o")已经被消耗了。 所以,匹配器一次缓慢地退出一个字母,直到最右边的“foo”发生了反流,此时匹配成功,搜索结束。

然而,第二个例子是不情愿的,所以它首先消耗“没有”。因为 “foo” 没有出现在字符串的开始处,所以它被迫吞下第一个字母(一个“x”), 它触发第一个匹配在 0 和 4.我们的测试工具继续进程,直到输入的字符串为累。在 4 和 13 发现另一场匹配。

第三个例子找不到匹配,因为量词是占有性的。在这种情况下,整个输入字符串被消耗. *+, 没有剩下任何东西来满足表达式末尾的 “foo”。对于想要抓住所有东西而无需退缩的情况,使用占有量词的量词; 在没有立即找到匹配的情况下,它将胜过相同的贪心量词。

上面的描述下面简单总结下: 下面的例子都是表示: 任意字符出现一次或多次后面跟随 foo。

  • 贪心 .*foo

    贪心量词 .* 首先会吃掉整个字符串,foo 也被包含在内,所以不满足要求,然后开始退格,直到把 foo 放出来,匹配成功

  • 不情愿 .*?foo

    不情愿量词 .*? 从开始处开始,foo 不在开头,然后一个一个吃掉,直到遇到结尾为 foo 的单词,满足之后, 剩余的则又重复一次,直到匹配完成,所以这里匹配到了两条

  • 占有 .*+foo

    占有量词 .*+ 整个字符串被 消耗,没有剩下任何东西来匹配 foo,所以失败