25.2 Java API
25.2 Java API
正则表达式相关的类位于包java.util.regex下,有两个主要的类,一个是Pattern,另一个是Matcher。Pattern表示正则表达式对象,它与要处理的具体字符串无关。Matcher表示一个匹配,它将正则表达式应用于一个具体字符串,通过它对字符串进行处理。
字符串类String也是一个重要的类,我们之前专门介绍过String,其中提到,它有一些方法,接受的参数不是普通的字符串,而是正则表达式。此外,正则表达式在Java中是需要先以字符串形式表示的。
下面,我们先来介绍如何表示正则表达式,然后探讨如何利用它实现一些常见的文本处理任务,包括切分、验证、查找和替换。
1.表示正则表达式
正则表达式由元字符和普通字符组成,字符’\
‘是一个元字符,要在正则表达式中表示’\
‘本身,需要使用它转义,即’\\
‘。
在Java中,没有什么特殊的语法能直接表示正则表达式,需要用字符串表示,而在字符串中,’\
‘也是一个元字符,为了在字符串中表示正则表达式的’\
‘,就需要使用两个’\
‘,即’\\
‘,而要匹配’\
‘本身,就需要4个’\
‘,即’\\\\
‘。比如,如下表达式:
1 | <(\w+)>(.*)</\1> |
对应的字符串表示就是:
1 | "<(\\w+)>(.*)</\\1>" |
一个简单规则是:**正则表达式中的任何一个’\
‘,在字符串中,需要替换为两个’\
‘**。
字符串表示的正则表达式可以被编译为一个Pattern对象,比如:
1 | String regex = "<(\\w+)>(.*)</\\1>"; |
Pattern是正则表达式的面向对象表示,所谓编译,简单理解就是将字符串表示为了一个内部结构,这个结构是一个有穷自动机。关于有穷自动机的理论比较深入,我们就不探讨了。
编译有一定的成本,而且Pattern对象只与正则表达式有关,与要处理的具体文本无关,它可以安全地被多线程共享,所以,在使用同一个正则表达式处理多个文本时,应该尽量重用同一个Pattern对象,避免重复编译。
Pattern的compile方法接受一个额外参数,可以指定匹配模式:
1 | public static Pattern compile(String regex, int flags) |
上节,我们介绍过三种匹配模式:单行模式(点号模式)、多行模式和大小写无关模式,它们对应的常量分别为:Pattern.DOTALL
、Pattern.MULTILINE
和Pattern.CASE_INSENSI-TIVE
,多个模式可以一起使用,通过’|’连起来即可,如下所示:
1 | Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.DOTALL) |
还有一个模式Pattern.LITERAL
,在此模式下,正则表达式字符串中的元字符将失去特殊含义,被看作普通字符。Pattern有一个静态方法:
1 | public static String quote(String s) |
quote()的目的是类似的,它将s中的字符都看作普通字符。我们在上节介绍过\Q
和\E
, \Q
和\E
之间的字符会被视为普通字符。quote()基本上就是在字符串s的前后加了\Q
和\E
,比如,如果s为”\\d{6}
“,则quote()的返回值就是”\\Q\\d{6}\\E
“。
2.切分
文本处理的一个常见需求是根据分隔符切分字符串,比如在处理CSV文件时,按逗号分隔每个字段,这个需求听上去很容易满足,因为String类有如下方法:
1 | public String[] split(String regex) |
比如:
1 | String str = "abc, def, hello"; |
不过,有一些重要的细节,我们需要注意。
split将参数regex看作正则表达式,而不是普通的字符,如果分隔符是元字符,比如. $|()[{^?*+\
,就需要转义。比如按点号’.
‘分隔,需要写为:
1 | String[] fields = str.split("\\."); |
如果分隔符是用户指定的,程序事先不知道,可以通过Pattern.quote()
将其看作普通字符串。
既然是正则表达式,分隔符就不一定是一个字符,比如,可以将一个或多个空白字符或点号作为分隔符,如下所示:
1 | String str = "abc def hello.\n world"; |
fields内容为:
1 | [abc, def, hello, world] |
需要说明的是,尾部的空白字符串不会包含在返回的结果数组中,但头部和中间的空白字符串会被包含在内,比如:
1 | String str = ", abc, , def, , "; |
输出为:
1 | field num: 4 |
如果字符串中找不到匹配regex的分隔符,返回数组长度为1,元素为原字符串。
Pattern也有split方法,与String方法的定义类似:
1 | public String[] split(CharSequence input) |
与String方法的区别如下。
1)Pattern接受的参数是CharSequence,更为通用,我们知道String、StringBuilder、StringBuffer、CharBuffer等都实现了该接口。
2)如果regex长度大于1或包含元字符,String的split方法必须先将regex编译为Pattern对象,再调用Pattern的split方法,这时,为避免重复编译,应该优先采用Pattern的方法。
3)如果regex就是一个字符且不是元字符,String的split方法会采用更为简单高效的实现,所以,这时应该优先采用String的split方法。
3.验证
验证就是检验输入文本是否完整匹配预定义的正则表达式,经常用于检验用户的输入是否合法。String有如下方法:
1 | public boolean matches(String regex) |
比如:
1 | String regex = "\\d{8}"; |
检查输入是否是8位数字,输出为true。
String的matches实际调用的是Pattern的如下方法:
1 | public static boolean matches(String regex, CharSequence input) |
这是一个静态方法,它的代码为:
1 | public static boolean matches(String regex, CharSequence input) { |
就是先调用compile编译regex为Pattern对象,再调用Pattern的matcher方法生成一个匹配对象Matcher, Matcher的matches方法返回是否完整匹配。
4.查找
查找就是在文本中寻找匹配正则表达式的子字符串,看个例子:
1 | public static void find(){ |
代码寻找所有类似”2017-06-02”这种格式的日期,输出为:
1 | find 2017-06-02 position: 9-19 |
Matcher的内部记录有一个位置,起始为0, find方法从这个位置查找匹配正则表达式的子字符串,找到后,返回true,并更新这个内部位置,匹配到的子字符串信息可以通过如下方法获取:
1 | //匹配到的完整子字符串 |
group()其实调用的是group(0),表示获取匹配的第0个分组的内容。我们在上节介绍过捕获分组的概念,分组0是一个特殊分组,表示匹配的整个子字符串。除了分组0, Matcher还有如下方法,获取分组的更多信息:
1 | //分组个数 |
比如:
1 | public static void findGroup() { |
输出为:
1 | year:2017, month:06, day:02 |
5.替换
查找到子字符串后,一个常见的后续操作是替换。String有多个替换方法:
1 | public String replace(char oldChar, char newChar) |
第一个replace方法操作的是单个字符,第二个是CharSequence,它们都是将参数看作普通字符。而replaceAll和replaceFirst则将参数regex看作正则表达式,它们的区别是, replaceAll替换所有找到的子字符串,而replaceFirst则只替换第一个找到的。看个简单的例子,将字符串中的多个连续空白字符替换为一个:
1 | String regex = "\\s+"; |
输出为:
1 | hello world good |
在replaceAll和replaceFirst中,参数replacement也不是被看作普通的字符串,可以使用美元符号加数字的形式(比如$
1)引用捕获分组。我们看个例子:
1 | String regex = "(\\d{4})-(\\d{2})-(\\d{2})"; |
输出为:
1 | today is 2017/06/02. |
这个例子将找到的日期字符串的格式进行了转换。所以,字符’$’在replacement中是元字符,如果需要替换为字符’$’本身,需要使用转义。看个例子:
1 | String regex = "#"; |
如果替换字符串是用户提供的,为避免元字符的干扰,可以使用Matcher的如下静态方法将其视为普通字符串:
1 | public static String quoteReplacement(String s) |
String的replaceAll和replaceFirst调用的其实是Pattern和Matcher中的方法。比如, replaceAll的代码为:
1 | public String replaceAll(String regex, String replacement) { |
replaceAll和replaceFirst都定义在Matcher中,除了一次性的替换操作外,Matcher还定义了边查找、边替换的方法:
1 | public Matcher appendReplacement(StringBuffer sb, String replacement) |
这两个方法用于和find()一起使用,我们先看个例子:
1 | public static void replaceCat() { |
在这个例子中,我们将前两个”cat”替换为了”dog”,其他”cat”不变,输出为:
1 | one dog, two dog, three cat |
StringBuffer类型的变量sb存放最终的替换结果,Matcher内部除了有一个查找位置,还有一个append位置,初始为0,当找到一个匹配的子字符串后,appendReplacement()做了三件事情:
1)将append位置到当前匹配之前的子字符串append到sb中,在第一次操作中,为”one “,第二次为”, two “。
2)将替换字符串append到sb中。
3)更新append位置为当前匹配之后的位置。
appendTail将append位置之后所有的字符append到sb中。
至此,正则表达式相关的主要Java API就介绍完了。我们讨论了如何在Java中表示正则表达式,如何利用它实现文本的切分、验证、查找和替换,对于替换,下面我们演示一个简单的模板引擎。