![Java核心技术·卷Ⅱ:高级特性(原书第10版)](https://wfqqreader-1252317822.image.myqcloud.com/cover/937/34339937/b_34339937.jpg)
2.7 正则表达式
正则表达式(regular expression)用于指定字符串的模式,你可以在任何需要定位匹配某种特定模式的字符串的情况下使用正则表达式。例如,我们有一个示例程序就是用来定位HTML文件中的所有超链接的,它是通过查找<a href="...">模式的字符串来实现此目的的。
当然,在指定模式时,...标记法并不够精确。你需要精确地指定什么样的字符序列才是合法的匹配,这就要求无论何时,当你要描述一个模式时,都需要使用某种特定的语法。
下面是一个简单的示例,正则表达式
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/106-i.jpg?sign=1738864612-BA3ZnrFlvFrgAKuW6HCX8jx9el37g0bj-0-29ebcb4aebf7d90ae86a8effb894f596)
匹配下列形式的所有字符串:
·第一个字母是J或j。
·接下来的三个字母是ava。
·字符串的其余部分由一个或多个任意的字符构成。
例如,字符串“javanese”就匹配这个特定的正则表达式,但是字符串“core java”就不匹配。
正如你所见,你需要了解一点这种语法,以理解正则表达式的含义。幸运的是,对于大多数情况,一小部分很直观的语法结构就足够用了。
·字符类(character class)是一个括在括号中的可选择的字符集,例如,[Jj]、[0-9]、[A-Za-z]或[^0-9]。这里“-”表示是一个范围(所有Unicode值落在两个边界范围之内的字符),而^表示补集(除了指定字符之外的所有字符)。
·如果字符类中包含“-”,那么它必须是第一项或最后一项;如果要包含“[”,那么它必须是第一项;如果要包含“^”,那么它可以是除开始位置之外的任何位置。其中,你只需要转义“[”和“\”。
·有许多预定的字符类,例如\d(数字)和\p{Sc}(Unicode货币符号)。请查看表2-6和表2-7。
表2-6 正则表达式语法
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/2b6-i.jpg?sign=1738864612-qRQVFmwEsYlesjX6nOkyYJfq6vOw8XGz-0-89797600d6bbf0d7b23a0a3aad429d0a)
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/2b6-1-i.jpg?sign=1738864612-JxIDTY7O0bUpp8Ca21LPv9r3rmPY8NUj-0-2c231d36eaa9e0b613491d771c54f2f9)
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/2b6-2-i.jpg?sign=1738864612-mSzowIKKWqapI7qssNKMQf1ROAGN0G6n-0-6fccde5e30e3493b87c453e685fc6019)
表2-7 与\p一起使用的预定义字符类名字
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/2b7-i.jpg?sign=1738864612-tln5JOWSIyBi5vWiORljmk1FzRJY1Bfv-0-a983e47eeb035c715b108860b5f2174a)
·大部分字符都可以与它们自身匹配,例如在前面示例中的ava字符。
·.符号可以匹配任何字符(有可能不包括行终止符,这取决于标志的设置)。
·使用\作为转义字符,例如,\.匹配句号而\\匹配反斜线。
·^和$分别匹配一行的开头和结尾。
·如果X和Y是正则表达式,那么XY表示“任何X的匹配后面跟随Y的匹配”,X|Y表示“任何X或Y的匹配”。
·你可以将量词运用到表达式X:X+(1个或多个)、X*(0个或多个)与X?(0个或1个)。
·默认情况下,量词要匹配能够使整个匹配成功的最大可能的重复次数。你可以修改这种行为,方法是使用后缀?(使用勉强或吝啬匹配,也就是匹配最小的重复次数)或使用后缀+(使用占有或贪婪匹配,也就是即使让整个匹配失败,也要匹配最大的重复次数)。
例如,字符串cab匹配[a-z]*ab,但是不匹配[a-z]*+ab。在第一种情况中,表达式[a-z]*只匹配字符c,使得字符ab匹配该模式的剩余部分;但是贪婪版本[a-z]*+将匹配字符cab,模式的剩余部分将无法匹配。
·我们使用群组来定义子表达式,其中群组用括号()括起来。例如,([+-]?)([0-9]+)。然后你可以询问模式匹配器,让其返回每个组的匹配,或者用\n来引用某个群组,其中n是群组号(从\1开始)。
例如,下面是一个有些复杂但是却可能很有用的正则表达式,它描述了十进制和十六进制整数:
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/110-i.jpg?sign=1738864612-xlTHKBYTsVINAwf7JXZkjG81T0SPgWiM-0-24d41e61e5fdc918af5d1befb2a376c7)
遗憾的是,在使用正则表达式的各种程序和类库之间,表达式语法并未完全标准化。尽管在基本结构上达成了一致,但是它们在细节上仍旧存在着许多令人抓狂的差异。Java正则表达式类使用的语法与Perl语言使用的语法十分相似,但是并不完全一样。表2-6展示的是Java语法中的所有结构。关于正则表达式语法的更多信息,可以求教于Pattern类的API文档和Jeffrey E.F.Friedl的《Mastering Regular Expressions》(O’Reilly and Associates,2006)。
正则表达式的最简单用法就是测试某个特定的字符串是否与它匹配。下面展示了如何用Java来编写这种测试,首先用表示正则表达式的字符串构建一个Pattern对象。然后从这个模式中获得一个Matcher,并调用它的matches方法:
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/110-2-i.jpg?sign=1738864612-Psq3uqkn5kGdhe6hpJ2VwFAorzomrdq4-0-8c044cb877b57bc898cc7096244cceec)
这个匹配器的输入可以是任何实现了CharSequence接口的类的对象,例如String、StringBuilder和CharBuffer。
在编译这个模式时,你可以设置一个或多个标志,例如:
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/110-3-i.jpg?sign=1738864612-Tiawm795SVf545Vwh9xlJb571nlAF19l-0-34c79774321741d57cf68f2299e03240)
或者可以在模式中指定它们:
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/110-4-i.jpg?sign=1738864612-96fWDZkaiH6dbSi2wq8K06qjMHiLPb5j-0-321dc597119966b1f94c2f2ccbe56f60)
下面是各个标志。
·Pattern.CASE_INSENSITIVE或r:匹配字符时忽略字母的大小写,默认情况下,这个标志只考虑US ASCII字符。
·Pattern.UNICODE_CASE或u:当与CASE_INSENSITIVE组合使用时,用Unicode字母的大小写来匹配。
·Pattern.UNICODE_CHARACTER_CLASS或U:选择Unicode字符类代替POSIX,其中蕴含了UNICODE_CASE。
·Pattern.MULTILINE或m:^和$匹配行的开头和结尾,而不是整个输入的开头和结尾。
·Pattern.UNIX_LINES或d:在多行模式中匹配^和$时,只有'\n'被识别成行终止符。
·Pattern.DOTALL或s:当使用这个标志时,.符号匹配所有字符,包括行终止符。
·Pattern.COMMENTS或x:空白字符和注释(从#到行末尾)将被忽略。
·Pattern.LITERAL:该模式将被逐字地采纳,必须精确匹配,因字母大小写而造成的差异除外。
·Pattern.CANON_EQ:考虑Unicode字符规范的等价性,例如,u后面跟随¨(分音符号)匹配ü。
最后两个标志不能在正则表达式内部指定。
如果想要在集合或流中匹配元素,那么可以将模式转换为谓词:
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/111-i.jpg?sign=1738864612-pZ0w5S7PehQM90rx8tX8aQ5ak820MKLi-0-239647346a0e3b7460b566397812f9a4)
其结果中包含了匹配正则表达式的所有字符串。
如果正则表达式包含群组,那么Matcher对象可以揭示群组的边界。下面的方法
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/111-2-i.jpg?sign=1738864612-WAnpDmcx36MpgZbbCL6sYZ1mLehOklLA-0-ba1f5307512dbbbf21347b69a83ca840)
将产生指定群组的开始索引和结束之后的索引。
可以直接通过调用下面的方法抽取匹配的字符串:
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/111-3-i.jpg?sign=1738864612-QlIQWciiwcPVlaIPsRaYJuyaLkw6mA8n-0-db9b3b3f1a62e2580452eb7370ac3e96)
群组0是整个输入,而用于第一个实际群组的群组索引是1。调用groupCount方法可以获得全部群组的数量。对于具名的组,使用下面的方法
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/111-4-i.jpg?sign=1738864612-y5jfNvBUc1o216hXXM8M15P6N6y94Fu4-0-b580523b612c29d7ab123afb5f88d64b)
嵌套群组是按照前括号排序的,例如,假设我们有下面的模式
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/111-5-i.jpg?sign=1738864612-75TBNIpERMGQiban95cgdp9sUZDvdsGR-0-a864c419bd90c13d7f3e2f051195192e)
和下面的输出
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/111-7-i.jpg?sign=1738864612-h8QA6PNR01UpxZaRh9bSErZvzIESZ2oH-0-9962f8b0e6809257f1cf5d3f3665259e)
那么,匹配器会报告下面的群组:
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/111-6-i.jpg?sign=1738864612-wR06AEDjwiEjenAj72LhkYyUugyGDNPf-0-86d915a8e27ec8eddea7eea3f518a8a3)
程序清单2-6的程序提示输入一个模式,然后提示输入用于匹配的字符串,随后将打印出输入是否与模式相匹配。如果输入匹配模式,并且模式包含群组,那么这个程序将用括号打印出群组边界,例如
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/112-i.jpg?sign=1738864612-pvCWvRWTaW3QnFEaasqDmDK9V7dBxg6B-0-8be106f702b5c22b0f1b43a6653e6ea7)
程序清单2-6 regex/RegexTest.java
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/112-2-i.jpg?sign=1738864612-W0P5Y9bGX57bYwXHQtyWuvKIUSwBAAcf-0-5f091181a6101df348969dd63afaffa0)
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/113-i.jpg?sign=1738864612-LkmZOAtwhN1HsR4yJkwnDHVNcY8X0Nm1-0-8ca6fe2aa23feab0924554baf3e55ce4)
通常,你不希望用正则表达式来匹配全部输入,而只是想找出输入中一个或多个匹配的子字符串。这时可以使用Matcher类的find方法来查找匹配内容,如果返回true,再使用start和end方法来查找匹配的内容,或使用不带引元的group方法来获取匹配的字符串。
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/113-2-i.jpg?sign=1738864612-lC3pWaEArquWBS2GnMvxZy1gfT6BQm2T-0-0595fcd460328401822495a634ea287b)
程序清单2-7对这种机制进行了应用,它定位一个Web页面上的所有超文本引用,并打印它们。为了运行这个程序,你需要在命令行中提供一个URL,例如
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/113tt1.jpg?sign=1738864612-FwjocFWIUB7W9cYdFh8NJ6ey2KjFwRmY-0-761323d77c0211ba4371a0a1e9148446)
程序清单2-7 match/HrefMatch.java
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/113-3-i.jpg?sign=1738864612-mpyRfWgnglcPKlWXOTXJskBzk78CIskO-0-1d181c969c834e4504e28b0010cacd41)
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/114-i.jpg?sign=1738864612-O2FbT4w4wlQKv5FKO2GpHS9zq1Bv9aaa-0-84b8b258ef1792ec7a09e7ad74714f82)
Matcher类的replaceAll方法将正则表达式出现的所有地方都用替换字符串来替换。例如,下面的指令将所有的数字序列都替换成#字符。
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/114-2-i.jpg?sign=1738864612-bScNxXyMb27hGNFqsWGpXbhTRmOMBpFB-0-5f1280f3aaf58ed6f56c15d06b84da42)
替换字符串可以包含对模式中群组的引用:$n表示替换成第n个群组,${name}被替换为具有给定名字的组,因此我们需要用\$来表示在替换文本中包含一个$字符。
如果字符串中包含$和\,但是又不希望它们被解释成群组的替换符,那么就可以调用matcher.replaceAll(Matcher.quoteReplacement(str))。
replaceFirst方法将只替换模式的第一次出现。
最后,Pattern类有一个split方法,它可以用正则表达式来匹配边界,从而将输入分割成字符串数组。例如,下面的指令可以将输入分割成标记,其中分隔符是由可选的空白字符包围的标点符号。
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/114-3-i.jpg?sign=1738864612-j4Dr03CzT6jMh3nbLy60mVc11UMw1hgF-0-067ffb8216adcad8e6f8f10f6494e077)
如果有多个标记,那么可以惰性地获取它们:
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/115-i.jpg?sign=1738864612-O0cK89gRuK8WHNFiWCgAOrBl2bMjqkyp-0-0a647d8b58ed3819923c4763ab834e86)
如果不关心预编译模式和惰性获取,那么可以使用String.split方法:
![](https://epubservercos.yuewen.com/F21227/18365861501241106/epubprivate/OEBPS/Images/115-2-i.jpg?sign=1738864612-yRnxehvTYG4n5JYCL9LwqRx97dBV8alw-0-9a9d8c3ab59ea91963b1a546105bc8a7)
java.util.regex.Pattern 1.4
·static Pattern compile(String expression)
·static Pattern compile(String expression,int flags)
把正则表达式字符串编译到一个用于快速处理匹配的模式对象中。
参数:expression 正则表达式
flags CASE_INSENSITIVE、UNICODE_CASE、MULTILINE、UNIX_LINES、DOTALL和CANON_EQ标志中的一个
·Matcher matcher(CharSequence input)
返回一个matcher对象,你可以用它在输入中定位模式的匹配。
·String[]split(CharSequence input)
·String[]split(CharSequence input,int limit)
·StreamsplitAsStream(CharSequence input)8
将输入分割成标记,其中模式指定了分隔符的形式。返回标记数组,分隔符并非标记的一部分。
参数:input 要分割成标记的字符串
limit 所产生的字符串的最大数量。如果已经发现了limit-1个匹配的分隔符,那么返回的数组中的最后一项就包含所有剩余未分割的输入。如果limit≤0,那么整个输入都被分割;如果limit为0,那么坠尾的空字符串将不会置于返回的数组中。
java.util.regex.Matcher 1.4
·boolean matches()
如果输入匹配模式,则返回true。
·boolean lookingAt()
如果输入的开头匹配模式,则返回true。
·boolean find()
·boolean find(int start)
尝试查找下一个匹配,如果找到了另一个匹配,则返回true。
参数:start 开始查找的索引位置
·int start()
·int end()
返回当前匹配的开始索引和结尾之后的索引位置。
·String group()
返回当前的匹配。
·int groupCount()
返回输入模式中的群组数量。
·int start(int groupIndex)
·int end(int groupIndex)
返回当前匹配中给定群组的开始和结尾之后的位置。
参数:groupIndex 群组索引(从1开始),或者表示整个匹配的0
·String group(int groupIndex)
返回匹配给定群组的字符串。
参数:groupIndex 群组索引(从1开始),或者表示整个匹配的0
·String replaceAll(String replacement)
·String replaceFirst(String replacement)
返回从匹配器输入获得的通过将所有匹配或第一个匹配用替换字符串替换之后的字符串。
参数:replacement 替换字符串,它可以包含用$n表示的对群组的引用,这时需要用\$来表示字符串中包含一个$符号
·static String quoteReplacement(String str)5.0
引用str中的所有\和$。
·Matcher reset()
·Matcher reset(CharSequence input)
复位匹配器的状态。第二个方法将使匹配器作用于另一个不同的输入。这两个方法都返回this。
你现在已经看到了在Java中输入输出操作是如何实现的,也对正则表达式有了概略的了解。在下一章中,我们将转而研究对XML数据的处理。