1.正则表达式(regular expression)
正则表达式(regular expression)是根据字符串集合内每个字符串共享的共同特性来描述字符串集合的一种途径。正则表达式可以用于搜索、编辑或者处理文本和数据。必须了解创建正则表达式的特定语法——这超出了Java编程语言的一般语法。正则表达式的复杂性各不相同。但是了解了如何构造正则表达式的基础之后,就能够解释(或者创建)任何正则表达式。
本章讲解java.util.regex API支持的正则表达式语法,并且提供若干实例以便演示各种对象如何交互。在正则表达式的领域中,有很多形式可供选择,比如grep、Perl、Tcl、Python、PHP和awk。java.util.regex API中的正则表达式语法和Perl最为类似。
2.java.util.regex这个包如何表示正则表达式
java.util.regex包主要由三部分构成:Pattern、Matcher和PatternSyntax- Exception。
l Pattern对象是正则表达式编译后的表达形式。Pattern类没有提供公共构造器。为了创建模式,首先必须调用它的一个public static compile方法,这样会返回一个Pattern对象。这些方法接受正则表达式作为第一个实参;本章下面几页将讲解所需的语法。
l Matcher对象是解释模式和对输入字符串执行匹配操作的引擎。和Pattern类一样,Matcher没有定义公共构造器。通过调用Pattern对象的matcher方法获得Matcher对象。
l PatternSyntaxException对象是不可控异常,它指出正则表达式模式中的语法错误。
本章的最后几节将详细讲解上述每个类。但是首先必须了解如何构造正则表达式。因此,下一小节介绍测试示例,以后将重复地使用它来介绍语法。
3.测试代码
本节定义一个可重用的测试示例RegexTestHarness.java,用于讲解这个API支持的正则表达式结构。运行这段代码的命令是java RegexTestHarness;不接受命令行参数。这个应用程序重复地循环,提示用户输入正则表达式和输入字符串。使用这个测试示例是可选的,但是你会发现使用它分析后面章节讨论的测试案例是很方便的。
4.字符串面量
这个API提供的模式匹配的最基本形式是字符串字面量的匹配。例如,如果正则表达式为foo,输入字符串为foo,那么匹配结果就成功,因为两个字符串相等。试一试下面的测试示例:
Enter your regex: foo
Enter input string to search: foo
I found the text "foo" starting at index 0 and ending at index 3.
这个匹配成功。注意,当输入字符串包含3个字符时,开始索引为0,结束索引为3。按照约定,范围包含开始索引,而不包含结束索引,如图13-1所示。
o
o
f
索引 2
索引 1
单元 2
单元 1
索引 0
单元 0
图13-1 String字面量“foo”,标出了编号的单元和索引值
字符串中的每个字符都在自己的单元(cell)中,索引位置指向每个单元之间。字符串“foo”从索引0开始,到索引3结束,尽管字符本身只占用单元0、1和2。
在后续的匹配中,你会注意到一些重叠;下一个匹配的开始索引和前一个匹配的结束索引相同:
Enter your regex: foo
Enter input string to search: foofoofoo
I found the text "foo" starting at index 0 and ending at index 3.
I found the text "foo" starting at index 3 and ending at index 6.
I found the text "foo" starting at index 6 and ending at index 9.
元字符
这个API还支持很多影响匹配模式的方式的特殊字符。把正则表达式改为cat.,输入字符串改为cats。输出如下:
Enter your regex: cat.
Enter input string to search: cats
I found the text "cats" starting at index 0 and ending at index 4.
这个匹配仍然成功,尽管点号(.)没有出现在输入字符串中。匹配成功的原因在于点号是元字符(metacharacter)——被匹配器解释为具有特殊含义的字符。元字符“.”的含义是“任何一个字符”,这就是这个例子中匹配成功的原因。
这个API支持的元字符有:( [ { / ^ - $ | } ] ) ? * +.。
注意 在某些情况下,前面列出的特殊字符不被当作元字符对待。随着你更多地学习如何构造正则表达式,就会遇到这种情况。但是,你可以使用这个清单检查一个特殊字符是否被当作元字符。例如,字符!、@和#永远都不会具有特殊含义。
有两种方式可以强制元字符作为普通字符:
l 在元字符前面加上反斜线,或者
l 用/Q(开始引用)和/E(结束引用)把元字符括起来。
使用这种技术时,/Q和/E可以放在表达式内的任何位置,前提是/Q先出现。
5.字符类
如果浏览Pattern类规范会看到总结支持正则表达式结构的表。表13-1描述字符类。
左边一列指定正则表达式结构,右边一列描述每个结构在什么情况下匹配。
字符类
[abc] a、b 或 c(简单类)
[^abc] 任何字符,除了 a、b 或 c(否定)
[a-zA-Z] a 到 z 或 A 到 Z,两头的字母包括在内(范围)
[a-d[m-p]] a 到 d 或 m 到 p:[a-dm-p](并集)
[a-z&&[def]] d、e 或 f(交集)
[a-z&&[^bc]] a 到 z,除了 b 和 c:[ad-z](减去)
[a-z&&[^m-p]] a 到 z,而非 m 到 p:[a-lq-z](减去)
注意 短语“字符类”中“类”这个词不表示.class文件。在正则表达式的上下文表述中,字符类是括在方括号内的字符集合。它表示这些字符将和给定输入字符串内的单一字符成功匹配。
简单类
字符类最基本的形式是方括号中简单并排放置的字符集合。例如,正则表达式[bcr]at将和单词“bat”、“cat”或者“rat”匹配,因为它定义一个字符类(接受“b”、“c”或者“r”)作为其第一个字符:
Enter your regex: [bcr]at
Enter input string to search: bat
I found the text "bat" starting at index 0 and ending at index 3.
Enter your regex: [bcr]at
Enter input string to search: cat
I found the text "cat" starting at index 0 and ending at index 3.
Enter your regex: [bcr]at
Enter input string to search: rat
I found the text "rat" starting at index 0 and ending at index 3.
Enter your regex: [bcr]at
Enter input string to search: hat
No match found.
在上面的例子中,只有当第一个字符和字符类定义的字符之一匹配时,整体匹配才成功。
a. 非
为了匹配列出的字符之外的所有字符,需要在字符类的开头插入“^”。这种技术被称为非(negation):
Enter your regex: [^bcr]at
Enter input string to search: bat
No match found.
Enter your regex: [^bcr]at
Enter input string to search: cat
No match found.
Enter your regex: [^bcr]at
Enter input string to search: rat
No match found.
Enter your regex: [^bcr]at
Enter input string to search: hat
I found the text "hat" starting at index 0 and ending at index 3.
只有当输入字符串的第一个字符不包含字符类中定义的任何字符时,匹配才成功。
b. 范围
有时候,你会希望定义一个字符类包含一个范围内的值,比如字母“a”到“h”或者数字“1”到“5”。为了指定范围,只需在要匹配的第一个和最后一个字符之间插入“-”即可,比如[1-5]或者[a-h]。也可以在类中连着放置不同范围,以便进一步扩展匹配的可能性。例如[a-zA-Z]将匹配字母表中的任何字母:a到z(小写)或者A到Z(大写)。
下面是范围和非的一些例子:
Enter your regex: [a-c]
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: [a-c]
Enter input string to search: b
I found the text "b" starting at index 0 and ending at index 1.
Enter your regex: [a-c]
Enter input string to search: c
I found the text "c" starting at index 0 and ending at index 1.
Enter your regex: [a-c]
Enter input string to search: d
No match found.
Enter your regex: foo[1-5]
Enter input string to search: foo1
I found the text "foo1" starting at index 0 and ending at index 4.
Enter your regex: foo[1-5]
Enter input string to search: foo5
I found the text "foo5" starting at index 0 and ending at index 4.
Enter your regex: foo[1-5]
Enter input string to search: foo6
No match found.
Enter your regex: foo[^1-5]
Enter input string to search: foo1
No match found.
Enter your regex: foo[^1-5]
Enter input string to search: foo6
I found the text "foo6" starting at index 0 and ending at index 4.
c. 并
也可以使用并(union)创建由两个或者多个独立字符类构成的单一字符类。为了创建并,只需在一个类中嵌套另一个类,比如[0-4[6-8]]。这个并创建的单一字符类匹配数字0、1、2、3、4、6、7和8。
Enter your regex: [0-4[6-8]]
Enter input string to search: 0
I found the text "0" starting at index 0 and ending at index 1.
Enter your regex: [0-4[6-8]]
Enter input string to search: 5
No match found.
Enter your regex: [0-4[6-8]]
Enter input string to search: 6
I found the text "6" starting at index 0 and ending at index 1.
Enter your regex: [0-4[6-8]]
Enter input string to search: 8
I found the text "8" starting at index 0 and ending at index 1.
Enter your regex: [0-4[6-8]]
Enter input string to search: 9
No match found.
d. 交
为了创建只和其所有嵌套类共有的字符匹配的单一字符类,需要使用&&,比如[0-9&&[345]]。这个交创建只和两个字符类共有的数字(3、4和5)匹配的单一字符类:
Enter your regex: [0-9&&[345]]
Enter input string to search: 3
I found the text "3" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[345]]
Enter input string to search: 4
I found the text "4" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[345]]
Enter input string to search: 5
I found the text "5" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[345]]
Enter input string to search: 2
No match found.
Enter your regex: [0-9&&[345]]
Enter input string to search: 6
No match found.
下面的例子显示两个范围的交:
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 3
No match found.
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 4
I found the text "4" starting at index 0 and ending at index 1.
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 5
I found the text "5" starting at index 0 and ending at index 1.
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 6
I found the text "6" starting at index 0 and ending at index 1.
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 7
No match found.
e. 减
最后,可以使用减(subtraction)去掉一个或者多个嵌套字符类,比如[0-9&&[^345]]。这个例子创建从0到9的所有值,但除3、4和5之外的单一字符类:
Enter your regex: [0-9&&[^345]]
Enter input string to search: 2
I found the text "2" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 3
No match found.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 4
No match found.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 5
No match found.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 6
I found the text "6" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 9
I found the text "9" starting at index 0 and ending at index 1.
现在我们介绍了如何创建字符类,在阅读下一小节之前你可能希望回顾一下表13-1。
6.预定义字符类
Pattern API包含很多有用的预定义字符类(predefined character class),它们提供常用正则表达式便利的简写方式。
在表13-2中,左边一列的每个结构是右边一列的字符类的简写方式。例如,/d表示数字范围(0-9),而/w表示单词字符(任何小写字母、任何大写字母、下划线或者任何数字)。应该尽可能使用预定义类。它们使你的代码更容易阅读,并且排除易混淆的字符类造成的错误。
预定义字符类
. 任何字符(与行结束符可能匹配也可能不匹配)
\d 数字:[0-9]
\D 非数字: [^0-9]
\s 空白字符:[ \t\n\x0B\f\r]
\S 非空白字符:[^\s]
\w 单词字符:[a-zA-Z_0-9]
\W 非单词字符:[^\w]
以反斜线开头的结构被称为转义结构(escaped construct)。我们在3.1.2节的第2小节简单介绍了转义结构,其中提到了用于引用的反斜线、/Q和/E。如果你在字符串字面量中使用转义结构,就必须在反斜线前面再加上一个反斜线,以便能够编译字符串。例如:
private final String REGEX = "//d"; // a single digit
在这个例子中,/d是正则表达式;附加的反斜线是编译代码所必须的。但是,测试示例直接从控制台读取表达式,所以附加的反斜线不是必须的。
下面的例子演示预定义字符类的使用:
Enter your regex: .
Enter input string to search: @
I found the text "@" starting at index 0 and ending at index 1.
Enter your regex: .
Enter input string to search: 1
I found the text "1" starting at index 0 and ending at index 1.
Enter your regex: .
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: /d
Enter input string to search: 1
I found the text "1" starting at index 0 and ending at index 1.
Enter your regex: /d
Enter input string to search: a
No match found.
Enter your regex: /D
Enter input string to search: 1
No match found.
Enter your regex: /D
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: /s
Enter input string to search:
I found the text " " starting at index 0 and ending at index 1.
Enter your regex: /s
Enter input string to search: a
No match found.
Enter your regex: /S
Enter input string to search:
No match found.
Enter your regex: /S
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: /w
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: /w
Enter input string to search: !
No match found.
Enter your regex: /W
Enter input string to search: a
No match found.
Enter your regex: /W
Enter input string to search: !
I found the text "!" starting at index 0 and ending at index 1.
在前三个例子中,正则表达式是简单的.(“点号”元字符表示“一个任何字符”)。因此,前三个例子都匹配成功(其中随机地使用一个@字符、一个数字和一个字母)。其余的例子分别使用表13-2中的正则表达式结构。可以参考这个表来分析每个匹配背后的逻辑:
l /d匹配所有数字。
l /s匹配空白。
l /w匹配单词字符。
另一种方式是使用大写字母表示相反含义:
l /D匹配非数字。
l /S匹配非空白。
l /W匹配非单词字符。
7.量词
量词(quantifier)允许你指定要匹配的出现次数。为了方便起见,Pattern API规范的三个部分描述greedy、reluctant和possessive量词,如表13-3所示。乍看上去,你可能认为量词X?、X??和X?+的功能完全一样,因为它们都匹配“X,一次或者完全没有”。在本节快结束时将解释它们实现的微妙区别。
Greedy 数量词
X? X,一次或一次也没有
X* X,零次或多次
X+ X,一次或多次
X{n} X,恰好 n 次
X{n,} X,至少 n 次
X{n,m} X,至少 n 次,但是不超过 m 次
Reluctant 数量词
X?? X,一次或一次也没有
X*? X,零次或多次
X+? X,一次或多次
X{n}? X,恰好 n 次
X{n,}? X,至少 n 次
X{n,m}? X,至少 n 次,但是不超过 m 次
Possessive 数量词
X?+ X,一次或一次也没有
X*+ X,零次或多次
X++ X,一次或多次
X{n}+ X,恰好 n 次
X{n,}+ X,至少 n 次
X{n,m}+ X,至少 n 次,但是不超过 m 次
通过创建3个不同的正则表达式,我们从greedy量词开始分析:字母a后面是?、*或者+。我们来看一看把这些表达式和空白输入字符串""进行测试会发生什么情况:
Enter your regex: a?
Enter input string to search:
I found the text "" starting at index 0 and ending at index 0.
Enter your regex: a*
Enter input string to search:
I found the text "" starting at index 0 and ending at index 0.
Enter your regex: a+
Enter input string to search:
No match found.
13.6.1 零长度匹配
在前面的例子中,前两个匹配成功,因为表达式a?和a*都允许字母a的出现次数为0。你还会注意到,开始和结束索引都为0,这和我们到目前为止见过的任何例子都不同。空白输入字符串“”没有长度,所以测试简单地和位于索引0的“无内容”匹配。这种类型的匹配被称为零长度匹配(zero-length match)。零长度匹配可能发生在这样几种情况下:在空白输入字符串中、在输入字符串的开头、在输入字符串的最后一个字符之后或者在输入字符串的任何两个字符之间。零长度匹配很容易识别,因为它们总在同一个索引位置开始和结束。
我们再通过几个例子分析零长度匹配。把输入字符串改为单一字母“a”,你会注意到一些有趣的事情:
Enter your regex: a?
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
Enter your regex: a*
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
Enter your regex: a+
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
字母“a”找到了3个量词,但是前两个例子还找到了索引1位置的零长度匹配;就是在输入字符串最后会一个字符之后的位置。记住,匹配器发现字符“a”位于索引0和索引1之间的单元中,并且我们的测试示例会一直循环到再也找不到匹配为止。根据使用的量词,位于最后一个字符之后的索引位置的“无内容”可能触发匹配,也可能不触发匹配。
现在把输入字符串改为一行中有5个字母“a”,你会得到如下结果:
Enter your regex: a?
Enter input string to search: aaaaa
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 1 and ending at index 2.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "a" starting at index 3 and ending at index 4.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.
Enter your regex: a*
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.
Enter your regex: a+
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.
表达式a?寻找每个字符的单独匹配,因为它匹配“a”出现零次或者一次的情况。表达式a*找到两个单独匹配:第一个匹配字母“a”的所有出现,然后是最后一个字符之后位于索引5位置的零长度匹配。最后,a+匹配字母“a”的所有出现,忽略位于最后索引位置出现的“无内容”。
讲到这里,你可能想知道,如果前两个量词遇到不是“a”的字母会得到什么结果。例如,如果遇到字母“b”(比如“ababaaaab”),会怎么样?
我们来看一下:
Enter your regex: a?
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "a" starting at index 5 and ending at index 6.
I found the text "a" starting at index 6 and ending at index 7.
I found the text "a" starting at index 7 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.
Enter your regex: a*
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.
Enter your regex: a+
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.
尽管字母“b”出现在单元1、3和8中,输出也报告了这些位置的零长度匹配。正则表达式a?并不专门搜索字母“b”;它仅仅搜索字母“a”的出现(或者不出现)。如果量词允许匹配“a”零次,那么输入字符串中不是“a”的任何内容的结果都是零长度匹配。按照前面例子中讨论的规则匹配剩余的a。
为了匹配正好出现n次的模式,只需在花括号中指定次数:
Enter your regex: a{3}
Enter input string to search: aa
No match found.
Enter your regex: a{3}
Enter input string to search: aaa
I found the text "aaa" starting at index 0 and ending at index 3.
Enter your regex: a{3}
Enter input string to search: aaaa
I found the text "aaa" starting at index 0 and ending at index 3.
这里,正则表达式a{3}搜索一行中字母“a”的3个出现。第一个测试失败,因为输入字符串中a出现的次数不够匹配要求。第二个测试的输入字符串中正好包含3个a,这就触发了匹配。第三个测试也触发匹配,因为在输入字符串的开头正好有3个a。后面跟着的任何内容都和第一个匹配不相关。如果在这个位置之后再次出现模式,就会触发后续的匹配:
Enter your regex: a{3}
Enter input string to search: aaaaaaaaa
I found the text "aaa" starting at index 0 and ending at index 3.
I found the text "aaa" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
为了要求模式至少出现n次,需要在数字后面加上逗号:
Enter your regex: a{3,}
Enter input string to search: aaaaaaaaa
I found the text "aaaaaaaaa" starting at index 0 and ending at index 9.
对于相同的输入字符串,这个测试只找到一个匹配,因为一行中的9个a满足“至少”3个a的要求。
最后,为了指定出现次数的上限,需要在括号中加上第二个数字:
Enter your regex: a{3,6}
// find at least 3 (but no more than 6) a's in a row
Enter input string to search: aaaaaaaaa
I found the text "aaaaaa" starting at index 0 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
这里,按照6个字符的上限强制停止第一个匹配。第二个匹配包含剩下的任何内容,正好是3个a——匹配允许的最小字符数量。如果输入字符串中少一个字符,就没有第二个匹配,因为只剩下了2个a。
13.6.2 使用量词的捕获组和字符类
到目前为止,我们只对包含一个字符的输入字符串测试了量词。实际上,一次只能把量词附加到一个字符之后,所以正则表达式abc+的含义是“a,后面是b,再后面是c出现一次或者多次”。它的含义不是“abc”一次或者多次。但是,也可以把量词附加到字符类(参见13.4节)和捕获组(见13.7节),比如[abc]+(a或b或c,一次或者多次),或者(abc)+(组“abc”,一次或者多次)。
下面我们演示,指定组(dog)在一行中出现三次:
Enter your regex: (dog){3}
Enter input string to search: dogdogdogdogdogdog
I found the text "dogdogdog" starting at index 0 and ending at index 9.
I found the text "dogdogdog" starting at index 9 and ending at index 18.
Enter your regex: dog{3}
Enter input string to search: dogdogdogdogdogdog
No match found.
第一个例子找到三个匹配,因为量词应用于整个捕获组。但是,删掉括号之后匹配失败,因为现在量词{3}只应用于字母“g”。
类似的,我们可以对整个字符类应用量词:
Enter your regex: [abc]{3}
Enter input string to search: abccabaaaccbbbc
I found the text "abc" starting at index 0 and ending at index 3.
I found the text "cab" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
I found the text "ccb" starting at index 9 and ending at index 12.
I found the text "bbc" starting at index 12 and ending at index 15.
Enter your regex: abc{3}
Enter input string to search: abccabaaaccbbbc
No match found.
在第一个例子中,量词{3}应用于整个字符类,但是第二个例子中只应用于字母“c”。
13.6.3 greedy、reluctant和possessive量词的区别
greedy、reluctant和possessive量词之间有微妙的区别。
greedy量词被看作“贪婪的”,因为它们在试图搜索第一个匹配之前读完(或者说吃掉)整个输入字符串。如果第一个匹配尝试(整个输入字符串)失败,匹配器就会在输入字符串中后退一个字符并且再次尝试,重复这个过程,直到找到匹配或者没有更多剩下的字符可以后退为止。根据表达式中使用的量词,它最后试图匹配的内容是1个或者0个字符。
但是,reluctant量词采取相反的方式:它们从输入字符串的开头开始,然后逐步地一次读取一个字符搜索匹配。它们最后试图匹配的内容是整个输入字符串。
最后,possessive量词总是读完整个输入字符串,尝试一次(而且只有一次)匹配。和greedy量词不同,possessive从不后退,即使这样做能允许整体匹配成功。
为了演示,我们分析输入字符串xfooxxxxxxfoo:
Enter your regex: .*foo // greedy quantifier
Enter input string to search: xfooxxxxxxfoo
I found the text "xfooxxxxxxfoo" starting at index 0 and ending at index 13.
Enter your regex: .*?foo // reluctant quantifier
Enter input string to search: xfooxxxxxxfoo
I found the text "xfoo" starting at index 0 and ending at index 4.
I found the text "xxxxxxfoo" starting at index 4 and ending at index 13.
Enter your regex: .*+foo // possessive quantifier
Enter input string to search: xfooxxxxxxfoo
No match found.
第一个例子使用greedy量词.*搜索“任何内容”零次或者多次,后面是字母f、o、o。因为是greedy量词,所以表达式的.*部分首先读完整个字符串。这样,整个表达式不会成功,因为最后三个字母(“f”“o”“o”)已经被消耗了。所以匹配器缓慢地一次后退一个字母,一直后退到最右侧出现“foo”为止,这里匹配成功并且搜索停止。
但是第二个例子使用的量词是reluctant量词,所以它首先消耗“无内容”。因为“foo”没有出现在字符串的开头,所以迫使它消耗掉第一个字母(x),这样就在索引0和4的位置触发第一个匹配。我们的测试示例继续处理,直到输入字符串耗尽为止。它在索引4和13找到了另一个匹配。
第三个例子找不到匹配,因为是possessive量词。这种情况下,.*+消耗整个输入字符串,在表达式的结尾没有剩下满足“foo”的内容。possessive量词用于处理所有内容,但是从不后退的情况;在没有立即发现匹配的情况下,它的性能优于功能相同的greedy量词。
8.捕获组
前面我们讲过如何把量词一次附加到一个字符、字符类或者捕获组之后。但是到目前为止,我们还没有详细讨论过捕获组的概念。
捕获组(capturing group)是把多个字符当作一个单元对待的一种方式。通过把字符括在括号内创建捕获组。例如,正则表达式(dog)创建包含字母“d”、“o”和“g”的一个组。输入字符串和捕获组匹配的那一部分将被保存在内存中,以便以后通过反向引用再次使用(见13.7.2节的讨论)。
13.7.1 编号
如Pattern API中所述,按照从左到右的顺序计算捕获组的前括号数目,给捕获组编号。例如,在表达式((A)(B(C)))中,有4个这样的组:
(1) ((A)(B(C)))
(2) (A)
(3) (B(C))
(4) (C)
为了查出表达式中有多少个组,可以对匹配器对象调用groupCount方法。groupCount方法返回一个int,表示匹配器模式中出现的捕获组的数量。在这个例子中,groupCount将返回4,显示这个模式包含4个捕获组。
还有一个特殊的组,组0,它总是代表整个表达式。这个组不包括在groupCount报告的总数内。以(?开头的组是纯粹的非捕获组(non-capturing group),它不捕获文本,也不计入组的总数。(后面的13.9节中将给出非捕获组的例子。)
了解组如何编号非常重要,因为一些Matcher方法接受指定特定捕获组编号的int作为参数:
l public int start(int group)——返回前一个匹配操作期间,给定组捕获的子序列的开始索引。
l public int end(int group)——返回前一个匹配操作期间,给定组捕获的子序列的最后一个字符的索引加1。
l public String group(int group)——返回前一个匹配操作期间,给定组捕获的输入子序列。
13.7.2 反向引用
输入字符串中和捕获组匹配的部分被保存在内存中,以便以后通过反向引用(backreference)再次使用。反向引用在正则表达式中被指定为反斜线(/),后面跟着一个数字,指出要再次使用的组的编号。例如,表达式(/d/d)定义匹配一行中两个数字的一个捕获组,以后可以通过反向引用/1再次使用它。
为了匹配任意两个数字,后面跟着完全相同的两个数字,可以使用(/d/d)/1作为正则表达式:
Enter your regex: (/d/d)/1
Enter input string to search: 1212
I found the text "1212" starting at index 0 and ending at index 4.
如果改动最后两个数字,匹配就会失败:
Enter your regex: (/d/d)/1
Enter input string to search: 1234
No match found.
对于嵌套捕获组,反向引用的工作方式完全相同:指定一个反斜线,后面跟着要再次使用的组的编号。
正则表达式(regular expression)是根据字符串集合内每个字符串共享的共同特性来描述字符串集合的一种途径。正则表达式可以用于搜索、编辑或者处理文本和数据。必须了解创建正则表达式的特定语法——这超出了Java编程语言的一般语法。正则表达式的复杂性各不相同。但是了解了如何构造正则表达式的基础之后,就能够解释(或者创建)任何正则表达式。
本章讲解java.util.regex API支持的正则表达式语法,并且提供若干实例以便演示各种对象如何交互。在正则表达式的领域中,有很多形式可供选择,比如grep、Perl、Tcl、Python、PHP和awk。java.util.regex API中的正则表达式语法和Perl最为类似。
2.java.util.regex这个包如何表示正则表达式
java.util.regex包主要由三部分构成:Pattern、Matcher和PatternSyntax- Exception。
l Pattern对象是正则表达式编译后的表达形式。Pattern类没有提供公共构造器。为了创建模式,首先必须调用它的一个public static compile方法,这样会返回一个Pattern对象。这些方法接受正则表达式作为第一个实参;本章下面几页将讲解所需的语法。
l Matcher对象是解释模式和对输入字符串执行匹配操作的引擎。和Pattern类一样,Matcher没有定义公共构造器。通过调用Pattern对象的matcher方法获得Matcher对象。
l PatternSyntaxException对象是不可控异常,它指出正则表达式模式中的语法错误。
本章的最后几节将详细讲解上述每个类。但是首先必须了解如何构造正则表达式。因此,下一小节介绍测试示例,以后将重复地使用它来介绍语法。
3.测试代码
本节定义一个可重用的测试示例RegexTestHarness.java,用于讲解这个API支持的正则表达式结构。运行这段代码的命令是java RegexTestHarness;不接受命令行参数。这个应用程序重复地循环,提示用户输入正则表达式和输入字符串。使用这个测试示例是可选的,但是你会发现使用它分析后面章节讨论的测试案例是很方便的。
Java代码
- import java.io.Console;
- import java.util.regex.Pattern;
- import java.util.regex.Matcher;
- public class RegexTestHarness {
- public static void main(String[] args){
- Console console = System.console();
- if (console == null) {
- System.err.println("No console.");
- System.exit(1);
- }
- while (true) {
- Pattern pattern =
- Pattern.compile(console.readLine("%nEnter your " +
- regex: "));
- Matcher matcher =
- pattern.matcher(console.readLine("Enter input string " +
- to search: "));
- boolean found = false;
- while (matcher.find()) {
- console.format("I found the text /"%s/" starting " +
- "at index %d and ending at " +
- "index %d.%n", matcher.group(),
- matcher.start(), matcher.end());
- found = true;
- }
- if(!found){
- console.format("No match found.%n");
- }
- }
- }
- }
4.字符串面量
这个API提供的模式匹配的最基本形式是字符串字面量的匹配。例如,如果正则表达式为foo,输入字符串为foo,那么匹配结果就成功,因为两个字符串相等。试一试下面的测试示例:
Enter your regex: foo
Enter input string to search: foo
I found the text "foo" starting at index 0 and ending at index 3.
这个匹配成功。注意,当输入字符串包含3个字符时,开始索引为0,结束索引为3。按照约定,范围包含开始索引,而不包含结束索引,如图13-1所示。
o
o
f
索引 2
索引 1
单元 2
单元 1
索引 0
单元 0
图13-1 String字面量“foo”,标出了编号的单元和索引值
字符串中的每个字符都在自己的单元(cell)中,索引位置指向每个单元之间。字符串“foo”从索引0开始,到索引3结束,尽管字符本身只占用单元0、1和2。
在后续的匹配中,你会注意到一些重叠;下一个匹配的开始索引和前一个匹配的结束索引相同:
Enter your regex: foo
Enter input string to search: foofoofoo
I found the text "foo" starting at index 0 and ending at index 3.
I found the text "foo" starting at index 3 and ending at index 6.
I found the text "foo" starting at index 6 and ending at index 9.
元字符
这个API还支持很多影响匹配模式的方式的特殊字符。把正则表达式改为cat.,输入字符串改为cats。输出如下:
Enter your regex: cat.
Enter input string to search: cats
I found the text "cats" starting at index 0 and ending at index 4.
这个匹配仍然成功,尽管点号(.)没有出现在输入字符串中。匹配成功的原因在于点号是元字符(metacharacter)——被匹配器解释为具有特殊含义的字符。元字符“.”的含义是“任何一个字符”,这就是这个例子中匹配成功的原因。
这个API支持的元字符有:( [ { / ^ - $ | } ] ) ? * +.。
注意 在某些情况下,前面列出的特殊字符不被当作元字符对待。随着你更多地学习如何构造正则表达式,就会遇到这种情况。但是,你可以使用这个清单检查一个特殊字符是否被当作元字符。例如,字符!、@和#永远都不会具有特殊含义。
有两种方式可以强制元字符作为普通字符:
l 在元字符前面加上反斜线,或者
l 用/Q(开始引用)和/E(结束引用)把元字符括起来。
使用这种技术时,/Q和/E可以放在表达式内的任何位置,前提是/Q先出现。
5.字符类
如果浏览Pattern类规范会看到总结支持正则表达式结构的表。表13-1描述字符类。
左边一列指定正则表达式结构,右边一列描述每个结构在什么情况下匹配。
字符类
[abc] a、b 或 c(简单类)
[^abc] 任何字符,除了 a、b 或 c(否定)
[a-zA-Z] a 到 z 或 A 到 Z,两头的字母包括在内(范围)
[a-d[m-p]] a 到 d 或 m 到 p:[a-dm-p](并集)
[a-z&&[def]] d、e 或 f(交集)
[a-z&&[^bc]] a 到 z,除了 b 和 c:[ad-z](减去)
[a-z&&[^m-p]] a 到 z,而非 m 到 p:[a-lq-z](减去)
注意 短语“字符类”中“类”这个词不表示.class文件。在正则表达式的上下文表述中,字符类是括在方括号内的字符集合。它表示这些字符将和给定输入字符串内的单一字符成功匹配。
简单类
字符类最基本的形式是方括号中简单并排放置的字符集合。例如,正则表达式[bcr]at将和单词“bat”、“cat”或者“rat”匹配,因为它定义一个字符类(接受“b”、“c”或者“r”)作为其第一个字符:
Enter your regex: [bcr]at
Enter input string to search: bat
I found the text "bat" starting at index 0 and ending at index 3.
Enter your regex: [bcr]at
Enter input string to search: cat
I found the text "cat" starting at index 0 and ending at index 3.
Enter your regex: [bcr]at
Enter input string to search: rat
I found the text "rat" starting at index 0 and ending at index 3.
Enter your regex: [bcr]at
Enter input string to search: hat
No match found.
在上面的例子中,只有当第一个字符和字符类定义的字符之一匹配时,整体匹配才成功。
a. 非
为了匹配列出的字符之外的所有字符,需要在字符类的开头插入“^”。这种技术被称为非(negation):
Enter your regex: [^bcr]at
Enter input string to search: bat
No match found.
Enter your regex: [^bcr]at
Enter input string to search: cat
No match found.
Enter your regex: [^bcr]at
Enter input string to search: rat
No match found.
Enter your regex: [^bcr]at
Enter input string to search: hat
I found the text "hat" starting at index 0 and ending at index 3.
只有当输入字符串的第一个字符不包含字符类中定义的任何字符时,匹配才成功。
b. 范围
有时候,你会希望定义一个字符类包含一个范围内的值,比如字母“a”到“h”或者数字“1”到“5”。为了指定范围,只需在要匹配的第一个和最后一个字符之间插入“-”即可,比如[1-5]或者[a-h]。也可以在类中连着放置不同范围,以便进一步扩展匹配的可能性。例如[a-zA-Z]将匹配字母表中的任何字母:a到z(小写)或者A到Z(大写)。
下面是范围和非的一些例子:
Enter your regex: [a-c]
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: [a-c]
Enter input string to search: b
I found the text "b" starting at index 0 and ending at index 1.
Enter your regex: [a-c]
Enter input string to search: c
I found the text "c" starting at index 0 and ending at index 1.
Enter your regex: [a-c]
Enter input string to search: d
No match found.
Enter your regex: foo[1-5]
Enter input string to search: foo1
I found the text "foo1" starting at index 0 and ending at index 4.
Enter your regex: foo[1-5]
Enter input string to search: foo5
I found the text "foo5" starting at index 0 and ending at index 4.
Enter your regex: foo[1-5]
Enter input string to search: foo6
No match found.
Enter your regex: foo[^1-5]
Enter input string to search: foo1
No match found.
Enter your regex: foo[^1-5]
Enter input string to search: foo6
I found the text "foo6" starting at index 0 and ending at index 4.
c. 并
也可以使用并(union)创建由两个或者多个独立字符类构成的单一字符类。为了创建并,只需在一个类中嵌套另一个类,比如[0-4[6-8]]。这个并创建的单一字符类匹配数字0、1、2、3、4、6、7和8。
Enter your regex: [0-4[6-8]]
Enter input string to search: 0
I found the text "0" starting at index 0 and ending at index 1.
Enter your regex: [0-4[6-8]]
Enter input string to search: 5
No match found.
Enter your regex: [0-4[6-8]]
Enter input string to search: 6
I found the text "6" starting at index 0 and ending at index 1.
Enter your regex: [0-4[6-8]]
Enter input string to search: 8
I found the text "8" starting at index 0 and ending at index 1.
Enter your regex: [0-4[6-8]]
Enter input string to search: 9
No match found.
d. 交
为了创建只和其所有嵌套类共有的字符匹配的单一字符类,需要使用&&,比如[0-9&&[345]]。这个交创建只和两个字符类共有的数字(3、4和5)匹配的单一字符类:
Enter your regex: [0-9&&[345]]
Enter input string to search: 3
I found the text "3" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[345]]
Enter input string to search: 4
I found the text "4" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[345]]
Enter input string to search: 5
I found the text "5" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[345]]
Enter input string to search: 2
No match found.
Enter your regex: [0-9&&[345]]
Enter input string to search: 6
No match found.
下面的例子显示两个范围的交:
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 3
No match found.
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 4
I found the text "4" starting at index 0 and ending at index 1.
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 5
I found the text "5" starting at index 0 and ending at index 1.
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 6
I found the text "6" starting at index 0 and ending at index 1.
Enter your regex: [2-8&&[4-6]]
Enter input string to search: 7
No match found.
e. 减
最后,可以使用减(subtraction)去掉一个或者多个嵌套字符类,比如[0-9&&[^345]]。这个例子创建从0到9的所有值,但除3、4和5之外的单一字符类:
Enter your regex: [0-9&&[^345]]
Enter input string to search: 2
I found the text "2" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 3
No match found.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 4
No match found.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 5
No match found.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 6
I found the text "6" starting at index 0 and ending at index 1.
Enter your regex: [0-9&&[^345]]
Enter input string to search: 9
I found the text "9" starting at index 0 and ending at index 1.
现在我们介绍了如何创建字符类,在阅读下一小节之前你可能希望回顾一下表13-1。
6.预定义字符类
Pattern API包含很多有用的预定义字符类(predefined character class),它们提供常用正则表达式便利的简写方式。
在表13-2中,左边一列的每个结构是右边一列的字符类的简写方式。例如,/d表示数字范围(0-9),而/w表示单词字符(任何小写字母、任何大写字母、下划线或者任何数字)。应该尽可能使用预定义类。它们使你的代码更容易阅读,并且排除易混淆的字符类造成的错误。
预定义字符类
. 任何字符(与行结束符可能匹配也可能不匹配)
\d 数字:[0-9]
\D 非数字: [^0-9]
\s 空白字符:[ \t\n\x0B\f\r]
\S 非空白字符:[^\s]
\w 单词字符:[a-zA-Z_0-9]
\W 非单词字符:[^\w]
以反斜线开头的结构被称为转义结构(escaped construct)。我们在3.1.2节的第2小节简单介绍了转义结构,其中提到了用于引用的反斜线、/Q和/E。如果你在字符串字面量中使用转义结构,就必须在反斜线前面再加上一个反斜线,以便能够编译字符串。例如:
private final String REGEX = "//d"; // a single digit
在这个例子中,/d是正则表达式;附加的反斜线是编译代码所必须的。但是,测试示例直接从控制台读取表达式,所以附加的反斜线不是必须的。
下面的例子演示预定义字符类的使用:
Enter your regex: .
Enter input string to search: @
I found the text "@" starting at index 0 and ending at index 1.
Enter your regex: .
Enter input string to search: 1
I found the text "1" starting at index 0 and ending at index 1.
Enter your regex: .
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: /d
Enter input string to search: 1
I found the text "1" starting at index 0 and ending at index 1.
Enter your regex: /d
Enter input string to search: a
No match found.
Enter your regex: /D
Enter input string to search: 1
No match found.
Enter your regex: /D
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: /s
Enter input string to search:
I found the text " " starting at index 0 and ending at index 1.
Enter your regex: /s
Enter input string to search: a
No match found.
Enter your regex: /S
Enter input string to search:
No match found.
Enter your regex: /S
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: /w
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
Enter your regex: /w
Enter input string to search: !
No match found.
Enter your regex: /W
Enter input string to search: a
No match found.
Enter your regex: /W
Enter input string to search: !
I found the text "!" starting at index 0 and ending at index 1.
在前三个例子中,正则表达式是简单的.(“点号”元字符表示“一个任何字符”)。因此,前三个例子都匹配成功(其中随机地使用一个@字符、一个数字和一个字母)。其余的例子分别使用表13-2中的正则表达式结构。可以参考这个表来分析每个匹配背后的逻辑:
l /d匹配所有数字。
l /s匹配空白。
l /w匹配单词字符。
另一种方式是使用大写字母表示相反含义:
l /D匹配非数字。
l /S匹配非空白。
l /W匹配非单词字符。
7.量词
量词(quantifier)允许你指定要匹配的出现次数。为了方便起见,Pattern API规范的三个部分描述greedy、reluctant和possessive量词,如表13-3所示。乍看上去,你可能认为量词X?、X??和X?+的功能完全一样,因为它们都匹配“X,一次或者完全没有”。在本节快结束时将解释它们实现的微妙区别。
Greedy 数量词
X? X,一次或一次也没有
X* X,零次或多次
X+ X,一次或多次
X{n} X,恰好 n 次
X{n,} X,至少 n 次
X{n,m} X,至少 n 次,但是不超过 m 次
Reluctant 数量词
X?? X,一次或一次也没有
X*? X,零次或多次
X+? X,一次或多次
X{n}? X,恰好 n 次
X{n,}? X,至少 n 次
X{n,m}? X,至少 n 次,但是不超过 m 次
Possessive 数量词
X?+ X,一次或一次也没有
X*+ X,零次或多次
X++ X,一次或多次
X{n}+ X,恰好 n 次
X{n,}+ X,至少 n 次
X{n,m}+ X,至少 n 次,但是不超过 m 次
通过创建3个不同的正则表达式,我们从greedy量词开始分析:字母a后面是?、*或者+。我们来看一看把这些表达式和空白输入字符串""进行测试会发生什么情况:
Enter your regex: a?
Enter input string to search:
I found the text "" starting at index 0 and ending at index 0.
Enter your regex: a*
Enter input string to search:
I found the text "" starting at index 0 and ending at index 0.
Enter your regex: a+
Enter input string to search:
No match found.
13.6.1 零长度匹配
在前面的例子中,前两个匹配成功,因为表达式a?和a*都允许字母a的出现次数为0。你还会注意到,开始和结束索引都为0,这和我们到目前为止见过的任何例子都不同。空白输入字符串“”没有长度,所以测试简单地和位于索引0的“无内容”匹配。这种类型的匹配被称为零长度匹配(zero-length match)。零长度匹配可能发生在这样几种情况下:在空白输入字符串中、在输入字符串的开头、在输入字符串的最后一个字符之后或者在输入字符串的任何两个字符之间。零长度匹配很容易识别,因为它们总在同一个索引位置开始和结束。
我们再通过几个例子分析零长度匹配。把输入字符串改为单一字母“a”,你会注意到一些有趣的事情:
Enter your regex: a?
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
Enter your regex: a*
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
Enter your regex: a+
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
字母“a”找到了3个量词,但是前两个例子还找到了索引1位置的零长度匹配;就是在输入字符串最后会一个字符之后的位置。记住,匹配器发现字符“a”位于索引0和索引1之间的单元中,并且我们的测试示例会一直循环到再也找不到匹配为止。根据使用的量词,位于最后一个字符之后的索引位置的“无内容”可能触发匹配,也可能不触发匹配。
现在把输入字符串改为一行中有5个字母“a”,你会得到如下结果:
Enter your regex: a?
Enter input string to search: aaaaa
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 1 and ending at index 2.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "a" starting at index 3 and ending at index 4.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.
Enter your regex: a*
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.
Enter your regex: a+
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.
表达式a?寻找每个字符的单独匹配,因为它匹配“a”出现零次或者一次的情况。表达式a*找到两个单独匹配:第一个匹配字母“a”的所有出现,然后是最后一个字符之后位于索引5位置的零长度匹配。最后,a+匹配字母“a”的所有出现,忽略位于最后索引位置出现的“无内容”。
讲到这里,你可能想知道,如果前两个量词遇到不是“a”的字母会得到什么结果。例如,如果遇到字母“b”(比如“ababaaaab”),会怎么样?
我们来看一下:
Enter your regex: a?
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "a" starting at index 5 and ending at index 6.
I found the text "a" starting at index 6 and ending at index 7.
I found the text "a" starting at index 7 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.
Enter your regex: a*
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.
Enter your regex: a+
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.
尽管字母“b”出现在单元1、3和8中,输出也报告了这些位置的零长度匹配。正则表达式a?并不专门搜索字母“b”;它仅仅搜索字母“a”的出现(或者不出现)。如果量词允许匹配“a”零次,那么输入字符串中不是“a”的任何内容的结果都是零长度匹配。按照前面例子中讨论的规则匹配剩余的a。
为了匹配正好出现n次的模式,只需在花括号中指定次数:
Enter your regex: a{3}
Enter input string to search: aa
No match found.
Enter your regex: a{3}
Enter input string to search: aaa
I found the text "aaa" starting at index 0 and ending at index 3.
Enter your regex: a{3}
Enter input string to search: aaaa
I found the text "aaa" starting at index 0 and ending at index 3.
这里,正则表达式a{3}搜索一行中字母“a”的3个出现。第一个测试失败,因为输入字符串中a出现的次数不够匹配要求。第二个测试的输入字符串中正好包含3个a,这就触发了匹配。第三个测试也触发匹配,因为在输入字符串的开头正好有3个a。后面跟着的任何内容都和第一个匹配不相关。如果在这个位置之后再次出现模式,就会触发后续的匹配:
Enter your regex: a{3}
Enter input string to search: aaaaaaaaa
I found the text "aaa" starting at index 0 and ending at index 3.
I found the text "aaa" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
为了要求模式至少出现n次,需要在数字后面加上逗号:
Enter your regex: a{3,}
Enter input string to search: aaaaaaaaa
I found the text "aaaaaaaaa" starting at index 0 and ending at index 9.
对于相同的输入字符串,这个测试只找到一个匹配,因为一行中的9个a满足“至少”3个a的要求。
最后,为了指定出现次数的上限,需要在括号中加上第二个数字:
Enter your regex: a{3,6}
// find at least 3 (but no more than 6) a's in a row
Enter input string to search: aaaaaaaaa
I found the text "aaaaaa" starting at index 0 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
这里,按照6个字符的上限强制停止第一个匹配。第二个匹配包含剩下的任何内容,正好是3个a——匹配允许的最小字符数量。如果输入字符串中少一个字符,就没有第二个匹配,因为只剩下了2个a。
13.6.2 使用量词的捕获组和字符类
到目前为止,我们只对包含一个字符的输入字符串测试了量词。实际上,一次只能把量词附加到一个字符之后,所以正则表达式abc+的含义是“a,后面是b,再后面是c出现一次或者多次”。它的含义不是“abc”一次或者多次。但是,也可以把量词附加到字符类(参见13.4节)和捕获组(见13.7节),比如[abc]+(a或b或c,一次或者多次),或者(abc)+(组“abc”,一次或者多次)。
下面我们演示,指定组(dog)在一行中出现三次:
Enter your regex: (dog){3}
Enter input string to search: dogdogdogdogdogdog
I found the text "dogdogdog" starting at index 0 and ending at index 9.
I found the text "dogdogdog" starting at index 9 and ending at index 18.
Enter your regex: dog{3}
Enter input string to search: dogdogdogdogdogdog
No match found.
第一个例子找到三个匹配,因为量词应用于整个捕获组。但是,删掉括号之后匹配失败,因为现在量词{3}只应用于字母“g”。
类似的,我们可以对整个字符类应用量词:
Enter your regex: [abc]{3}
Enter input string to search: abccabaaaccbbbc
I found the text "abc" starting at index 0 and ending at index 3.
I found the text "cab" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
I found the text "ccb" starting at index 9 and ending at index 12.
I found the text "bbc" starting at index 12 and ending at index 15.
Enter your regex: abc{3}
Enter input string to search: abccabaaaccbbbc
No match found.
在第一个例子中,量词{3}应用于整个字符类,但是第二个例子中只应用于字母“c”。
13.6.3 greedy、reluctant和possessive量词的区别
greedy、reluctant和possessive量词之间有微妙的区别。
greedy量词被看作“贪婪的”,因为它们在试图搜索第一个匹配之前读完(或者说吃掉)整个输入字符串。如果第一个匹配尝试(整个输入字符串)失败,匹配器就会在输入字符串中后退一个字符并且再次尝试,重复这个过程,直到找到匹配或者没有更多剩下的字符可以后退为止。根据表达式中使用的量词,它最后试图匹配的内容是1个或者0个字符。
但是,reluctant量词采取相反的方式:它们从输入字符串的开头开始,然后逐步地一次读取一个字符搜索匹配。它们最后试图匹配的内容是整个输入字符串。
最后,possessive量词总是读完整个输入字符串,尝试一次(而且只有一次)匹配。和greedy量词不同,possessive从不后退,即使这样做能允许整体匹配成功。
为了演示,我们分析输入字符串xfooxxxxxxfoo:
Enter your regex: .*foo // greedy quantifier
Enter input string to search: xfooxxxxxxfoo
I found the text "xfooxxxxxxfoo" starting at index 0 and ending at index 13.
Enter your regex: .*?foo // reluctant quantifier
Enter input string to search: xfooxxxxxxfoo
I found the text "xfoo" starting at index 0 and ending at index 4.
I found the text "xxxxxxfoo" starting at index 4 and ending at index 13.
Enter your regex: .*+foo // possessive quantifier
Enter input string to search: xfooxxxxxxfoo
No match found.
第一个例子使用greedy量词.*搜索“任何内容”零次或者多次,后面是字母f、o、o。因为是greedy量词,所以表达式的.*部分首先读完整个字符串。这样,整个表达式不会成功,因为最后三个字母(“f”“o”“o”)已经被消耗了。所以匹配器缓慢地一次后退一个字母,一直后退到最右侧出现“foo”为止,这里匹配成功并且搜索停止。
但是第二个例子使用的量词是reluctant量词,所以它首先消耗“无内容”。因为“foo”没有出现在字符串的开头,所以迫使它消耗掉第一个字母(x),这样就在索引0和4的位置触发第一个匹配。我们的测试示例继续处理,直到输入字符串耗尽为止。它在索引4和13找到了另一个匹配。
第三个例子找不到匹配,因为是possessive量词。这种情况下,.*+消耗整个输入字符串,在表达式的结尾没有剩下满足“foo”的内容。possessive量词用于处理所有内容,但是从不后退的情况;在没有立即发现匹配的情况下,它的性能优于功能相同的greedy量词。
8.捕获组
前面我们讲过如何把量词一次附加到一个字符、字符类或者捕获组之后。但是到目前为止,我们还没有详细讨论过捕获组的概念。
捕获组(capturing group)是把多个字符当作一个单元对待的一种方式。通过把字符括在括号内创建捕获组。例如,正则表达式(dog)创建包含字母“d”、“o”和“g”的一个组。输入字符串和捕获组匹配的那一部分将被保存在内存中,以便以后通过反向引用再次使用(见13.7.2节的讨论)。
13.7.1 编号
如Pattern API中所述,按照从左到右的顺序计算捕获组的前括号数目,给捕获组编号。例如,在表达式((A)(B(C)))中,有4个这样的组:
(1) ((A)(B(C)))
(2) (A)
(3) (B(C))
(4) (C)
为了查出表达式中有多少个组,可以对匹配器对象调用groupCount方法。groupCount方法返回一个int,表示匹配器模式中出现的捕获组的数量。在这个例子中,groupCount将返回4,显示这个模式包含4个捕获组。
还有一个特殊的组,组0,它总是代表整个表达式。这个组不包括在groupCount报告的总数内。以(?开头的组是纯粹的非捕获组(non-capturing group),它不捕获文本,也不计入组的总数。(后面的13.9节中将给出非捕获组的例子。)
了解组如何编号非常重要,因为一些Matcher方法接受指定特定捕获组编号的int作为参数:
l public int start(int group)——返回前一个匹配操作期间,给定组捕获的子序列的开始索引。
l public int end(int group)——返回前一个匹配操作期间,给定组捕获的子序列的最后一个字符的索引加1。
l public String group(int group)——返回前一个匹配操作期间,给定组捕获的输入子序列。
13.7.2 反向引用
输入字符串中和捕获组匹配的部分被保存在内存中,以便以后通过反向引用(backreference)再次使用。反向引用在正则表达式中被指定为反斜线(/),后面跟着一个数字,指出要再次使用的组的编号。例如,表达式(/d/d)定义匹配一行中两个数字的一个捕获组,以后可以通过反向引用/1再次使用它。
为了匹配任意两个数字,后面跟着完全相同的两个数字,可以使用(/d/d)/1作为正则表达式:
Enter your regex: (/d/d)/1
Enter input string to search: 1212
I found the text "1212" starting at index 0 and ending at index 4.
如果改动最后两个数字,匹配就会失败:
Enter your regex: (/d/d)/1
Enter input string to search: 1234
No match found.
对于嵌套捕获组,反向引用的工作方式完全相同:指定一个反斜线,后面跟着要再次使用的组的编号。