使用正则表达式(regular expression)进行范式匹配(pattern matching)可以实现诸多文本处理操作的自动化,如:搜索替换、输入验证、文本转换以及过滤。强大的正则表达式处理引擎使得代码量大大减少,往常需要大量代码的处理而今只需要几行正则表达式就可以实现。有些编程语言(如Perl)和系统工具(如grep)多年前就已经支持正则表达式。但在J2SE 1.4以前的JDK并不支持正则表达式,人们不得不依赖于第三方开发包,如Jakarta Regexp和IBM的com.ibm.regex(这是一个商业化产品)。java.util.regex的诞生改写了历史,它提供了正则表达式引擎的标准实现。本文介绍了如何快速使用java.util.regex实现基于范式的搜索。首先介绍正则表达式的基本概念,然后详细讨论了该包的使用,并演示了几个简单应用。
如果你有在其他语言中使用正则表达式的经验,那么下面的内容为你介绍了它在Java中的应用以及一些新功能。如果你刚开始使用正则表达式,对它还不太熟悉,那么你将很快学会有效地使用正则表达式进行文本处理,这种处理方式对你来说也许是匪夷所思的。
正则表达式是一种根据给定的范式从给定的文本序列中发现匹配的机制。也就是说,它是一种范式语言(pattern language)。一个正则表达式通常由两种字符组成:字面量(literal)和元字符(meta-character)。字面量是普通字符,如a、b、c、1、2;元字符(如*,$,等等)则向正则表达式引擎(详见下文)传达了特殊含义。引擎的工作是解释正则表达式,进行范式匹配避,并对结果进行处理。这门范式语言和处理引擎相结合,使正则表达式成为简化范式匹配的强大工具。像java.util.regex和JRegex这些实现提供了额外的查询处理功能(如替换、分割等),这些功能为修改目标文本提供了便利。关于正则表达式的其他Java实现以及在其他语言的实现,请查看本文末尾列出的参考文献。
元字符为正则表达式提供了高级表达能力。本文将介绍Java支持的常用元字符,关于完整的元字符列表,请参考Sun的API文档(在java.util.regex.Pattern类的文档中)。
锚是指目标文本中预定义的位置。锚的概念和引用点(reference point)相似,用于确定正则表达式中其他元素的相对位置。虽然锚也可以匹配任意其他位置(通过使用清单一中列出的查找元件),但其典型应用是匹配字符串、行、词等的边界位置。查找元件(lookaround construct)匹配符合给定条件的位置。一个正向前查找(positive lookahead,例如?=Neo)匹配紧跟着Neo的位置,而负向前查找(negative lookahead,例如?!Neo)匹配不以Neo结尾的位置。向后查找元件(lookbehind construct,正的形如?<=...,负的形如?<!...)的工作方式和向前查找刚好相反。
字符类元件“[...]”用于指定一个正则表达式中包含的字符的清单,而元件“[^...]”则用于指定不包含字符的清单。使用“[...]”时,如果在目标文本中发现该清单列出的任意一个字符,那么认为匹配成功一次。例如,正则表达式“[cw]ould”匹配“could”和“would”。字符类隐含了逻辑或运算(也称为元素之间的Alternation)。轮询(alternation)指在“(x|y)”条件下,“x”或“y”都是成功匹配。因此,刚才的正则表达式也可以写作“(c|w)ould”。
字符类中的特殊元字符“-”可以用于指定一个范围,所以“[a-z]”匹配字符所有的小写英文字母。类速记符(class shorthand)是常用字符类的简化形式,如数字,单词,空白,等等。清单一列出了Java中可用的类速记。
限量符用于指出一次成功匹配必须满足的元素实例的个数。Java支持的三个限量符分别称为:贪婪(greedy)、勉强(relunctant)和占有(possessive)。贪婪限量符试图进行尽可能多的匹配,勉强限量符则试图进行尽可能少的匹配。这就是说,无论是否发现成功匹配,贪婪限量符都会继续搜索直到输入行的末尾。当然,如果目标文本比较大,那么性能上的开销也会比较大。勉强限量符(又称懒限量符)则是一发现成功匹配就结束查找,而不会继续直到行末。占有限量符用于优化匹配操作,使用占有限定符时将不保存前次匹配的状态。清单一列出了这三个限量符。
有些元件用于开启或者关闭特定的强大功能。默认情况下这些功能都是关闭的,因为它们要求额外的开销。例如,在正则表达式使用“(?i)”会开启大小写无关的匹配功能。Java支持在编译时就指定方式修饰符,即使用java.util.regex.Pattern的静态成员。Pattern类将于下文介绍。
现在请看一个使用刚讨论的元字符来进行Zion中需要的密码验证的例子。Zion公司的安全标准要求密码只能包含字母和数字,而且要求最少有一个数字,密码长度为6-32个字符。
清单二和清单三列出了两种可能的解决方案。第一种办法(清单二)使用了java.lang.String的matches()方法内置(built-in)的正则表达式支持。第二种办法使用java.util.regex包提供的类。这两种办法的底层机制是完全一样的,这将在下文讨论。下一节将讨论API规范。
且看解决方案是如何满足要求的。清单二中第三行的正则表达式和清单三中的Pattern pContent定义的是一样的。这个范式使用了一个元字符组合,即字符类“[a-z]”、类速记符(“/d”表示字符类“[0-9]”)和贪婪限量符(*和+)。进行匹配时,范式“//b(?i)([a-z]*//d+[a-z]*)//b”成功匹配单词边界,它的意思是,0个或多个字母,跟上1个或多个数字,然后紧跟0个或多个字母。修饰符“?i”用于指出搜索是大小写无关的。注意,两个清单中的正则表达式有些不同。显著的差异是清单三使用了注释。另一个差异比较不起眼,但很重要,你找到了吗?答案就在下一节中。
清单二第四行规定了密码长度,它使用{min, max}限定了成功匹配的最小次数和最大次数。此时,若单词边界之间有6到32个字母或数字,则“//b(?i)([a-z0-9]){6,32}//b”与之匹配。注意,清单三中使用Pattern类的final变量指定了大小写无关选项,提高了正则表达式的可读性。这些变量将于本文后续部分讨论。
这两个正则表达式一起使用就可以认定“010101”和“m0rpheus”是合法的,而“agentsmith”是非法的。除了正则表达式,也有其他办法来验证密码,但是,你可以看到,使用正则表达式是多么简洁雅致。
正则表达式中圆括号的作用有二:分组和捕捉。
圆括号“()”将元素分组,捕捉相应的子匹配。后向引用(/1,/2,等等)允许在同一个正则表达式中引用相应的组捕捉的文本。圆括号的计算从左至右进行,它在正则表达式中出现的位置(左起)决定了相应的后向引用内容。Java同时支持从正则表达式外部访问捕捉的文本,这是通过元件“$1”“$2”等实现的,其中元件“$1”就是指向“/1”包含的内容或者java.util.regex.Matcher类的group(n)方法的句柄。
另一种类型的圆括号“(?:)”只进行分组,不捕捉任何文本。但不需要进行后向引用时,这就是一个有用的元件。它不保留匹配状态,从而提高匹配操作的速度。这也就是上文提到的清单二和清单三的不同之处。注意,清单三由于使用了不捕捉型的圆括号,因而效率更高。
这个包相当小,只由两个final类,即java.util.regex.Pattern和java.util.regex.Matcher,和一个异常类java.util.regex.PatternSyntaxException组成。这几个类形成了Java的正则表达式框架。
Pattern类的实例就是对正则表达式字符串进行编译的结果。Matcher对象负责在给定的字符序列上的匹配操作,并提供访问和使用匹配结果的额外功能。Pattern类还提供一个静态的matches(String pattern, String text)方法,用于该范式(和以及匹配)不需要保留并复用的情况。注意,清单三中范式pContent和pLength都在方法外定义,这使得范式对象可以在多次方法调用中复用,从而提高了效率。如果使用java.lang.String的matches()方法,那么你就无法使用这个选项,正如清单二所示。
Pattern类的compile()方法接收一个待编译的正则表达式字符串,它将对该表达式进行语法检查。如果发现语法错误,那么它将抛出一个非检查异常(unchecked exception)java.util.regex.PatternSyntaxException。上文讨论的修饰符可在编译时加入,Pattern类以标记形式表征这些修饰符的static final变量(CASE_INSENSITIVE,DOTALL,等等)。多个标记可用或运算符“|”指定,如清单三23行的pLength范式中所示。
类似地,还有许多进行结果查询的方法。group(),group(int i)是最常用的两个。group()方法用于访问上一次匹配中符合匹配的文本,而group(int i)则返回第i组捕捉的文本(使用捕捉型圆括号)。例如,正则表达式“(//d)([A-C])”应用于字符串“2140AD”时,group(1)将返回“0”,group(2)将返回“A”,group(0)将返回“0A”。注意,group(0)总是返回整个匹配文本。
最后,让我们讨论一下Matcher类的文本替换功能,然后看看两个例子。replaceAll(String newText)方法将所有匹配的文本替换为给定的新文本,而replaceFirst(String newText)则只替换掉第一次匹配的文本。高级的替换操作apendReplacement()和appendTail()提供了对替换操作的细粒度控制。清单四演示了它们的使用。
到目前为止,我们已经讲了输入字符串验证的例子。现在,让我们一起看看对密码验证例子的扩展,在本例中,目标文本被修改以符合规定。Zion的安全策略要求密码在传输前以某种方式进行加密。请看清单四中的一个简单的文本转换工具,它使用正则表达式将文本中数字颠倒过来。本例的目的不是要编写一个成熟的加密算法,而是演示正则表达式的一些高级特性。
本程序使用了一个简单的正则表达式(第5行)来匹配密码中的数字。find()方法被用于搜索数字,它也在匹配结果的迭代中起了作用(第7行)。group(1)方法返回捕捉型圆括号“(//d)”捕捉的文本,即字符类的所有数字。然后根据一个数字数组中,将捕捉的数字颠倒大小,再添加到表示新密码的StringBuffer中。appendReplacement方法负责插入替换文本,而appendTail则负责追加余下的文本。密码“010101”将被转换为“989898”。
清单五中的范式演示了正则表达式在匹配email和Web地址上的应用。email范式(清单五,行1)匹配所有以matrix.com、matrix.net或matrix.org结尾的地址。它也匹配了紧跟着的人名。例如,该范式匹配紧跟在email后面出现的单词“Trinity”,tn@matrix.com(Trinity)。URL范式匹配主机和可选的路径 http://www.zion.com/antimatrix.html 。注意,额外指定的Pattern.MULTILINE选项指出,URL可能延续多行。默认的行为是匹配当前行。
正则表达式方便了诸如表单验证、转换文本为HTML或反之、解析文本、帮助程序等范式匹配程序的编写。随着正则表达式包在Java中的引入,正则表达式的广泛使用变得更为方便,不再依赖于外部包。我们可以看到,常用的正则表达式元件的使用都在Java中得到支持,本文也演示了其使用。然而,由于篇幅限制,本文不可能对元件进行更多讨论。详细讨论请参考本文末尾列出的文献。
注意,在其他语言中(如Perl,.NET,等等),Java中的正则表达式元件可能意义略有变化,因此正则表达式可能不是完全可移植的。
清单一列出了Java支持的正则表达式元字符的一个子集,清单二演示了使用java.lang.String的密码验证。清单三演示了使用java.util.regex的密码验证。清单四演示了使用java.util.regex进行文本转换。
什么是正则表达式,它为什么这么重要
如果你有在其他语言中使用正则表达式的经验,那么下面的内容为你介绍了它在Java中的应用以及一些新功能。如果你刚开始使用正则表达式,对它还不太熟悉,那么你将很快学会有效地使用正则表达式进行文本处理,这种处理方式对你来说也许是匪夷所思的。
正则表达式是一种根据给定的范式从给定的文本序列中发现匹配的机制。也就是说,它是一种范式语言(pattern language)。一个正则表达式通常由两种字符组成:字面量(literal)和元字符(meta-character)。字面量是普通字符,如a、b、c、1、2;元字符(如*,$,等等)则向正则表达式引擎(详见下文)传达了特殊含义。引擎的工作是解释正则表达式,进行范式匹配避,并对结果进行处理。这门范式语言和处理引擎相结合,使正则表达式成为简化范式匹配的强大工具。像java.util.regex和JRegex这些实现提供了额外的查询处理功能(如替换、分割等),这些功能为修改目标文本提供了便利。关于正则表达式的其他Java实现以及在其他语言的实现,请查看本文末尾列出的参考文献。
元字符(Meta-character)
元字符为正则表达式提供了高级表达能力。本文将介绍Java支持的常用元字符,关于完整的元字符列表,请参考Sun的API文档(在java.util.regex.Pattern类的文档中)。
锚(Anchor)
锚是指目标文本中预定义的位置。锚的概念和引用点(reference point)相似,用于确定正则表达式中其他元素的相对位置。虽然锚也可以匹配任意其他位置(通过使用清单一中列出的查找元件),但其典型应用是匹配字符串、行、词等的边界位置。查找元件(lookaround construct)匹配符合给定条件的位置。一个正向前查找(positive lookahead,例如?=Neo)匹配紧跟着Neo的位置,而负向前查找(negative lookahead,例如?!Neo)匹配不以Neo结尾的位置。向后查找元件(lookbehind construct,正的形如?<=...,负的形如?<!...)的工作方式和向前查找刚好相反。
字符类以及速记符号(Character Classes, Class Shorthands and Alternation)
字符类元件“[...]”用于指定一个正则表达式中包含的字符的清单,而元件“[^...]”则用于指定不包含字符的清单。使用“[...]”时,如果在目标文本中发现该清单列出的任意一个字符,那么认为匹配成功一次。例如,正则表达式“[cw]ould”匹配“could”和“would”。字符类隐含了逻辑或运算(也称为元素之间的Alternation)。轮询(alternation)指在“(x|y)”条件下,“x”或“y”都是成功匹配。因此,刚才的正则表达式也可以写作“(c|w)ould”。
字符类中的特殊元字符“-”可以用于指定一个范围,所以“[a-z]”匹配字符所有的小写英文字母。类速记符(class shorthand)是常用字符类的简化形式,如数字,单词,空白,等等。清单一列出了Java中可用的类速记。
限量符(Quantifier)
限量符用于指出一次成功匹配必须满足的元素实例的个数。Java支持的三个限量符分别称为:贪婪(greedy)、勉强(relunctant)和占有(possessive)。贪婪限量符试图进行尽可能多的匹配,勉强限量符则试图进行尽可能少的匹配。这就是说,无论是否发现成功匹配,贪婪限量符都会继续搜索直到输入行的末尾。当然,如果目标文本比较大,那么性能上的开销也会比较大。勉强限量符(又称懒限量符)则是一发现成功匹配就结束查找,而不会继续直到行末。占有限量符用于优化匹配操作,使用占有限定符时将不保存前次匹配的状态。清单一列出了这三个限量符。
修饰符(Mode modifiers)
有些元件用于开启或者关闭特定的强大功能。默认情况下这些功能都是关闭的,因为它们要求额外的开销。例如,在正则表达式使用“(?i)”会开启大小写无关的匹配功能。Java支持在编译时就指定方式修饰符,即使用java.util.regex.Pattern的静态成员。Pattern类将于下文介绍。
示例一:输入验证
现在请看一个使用刚讨论的元字符来进行Zion中需要的密码验证的例子。Zion公司的安全标准要求密码只能包含字母和数字,而且要求最少有一个数字,密码长度为6-32个字符。
清单二和清单三列出了两种可能的解决方案。第一种办法(清单二)使用了java.lang.String的matches()方法内置(built-in)的正则表达式支持。第二种办法使用java.util.regex包提供的类。这两种办法的底层机制是完全一样的,这将在下文讨论。下一节将讨论API规范。
且看解决方案是如何满足要求的。清单二中第三行的正则表达式和清单三中的Pattern pContent定义的是一样的。这个范式使用了一个元字符组合,即字符类“[a-z]”、类速记符(“/d”表示字符类“[0-9]”)和贪婪限量符(*和+)。进行匹配时,范式“//b(?i)([a-z]*//d+[a-z]*)//b”成功匹配单词边界,它的意思是,0个或多个字母,跟上1个或多个数字,然后紧跟0个或多个字母。修饰符“?i”用于指出搜索是大小写无关的。注意,两个清单中的正则表达式有些不同。显著的差异是清单三使用了注释。另一个差异比较不起眼,但很重要,你找到了吗?答案就在下一节中。
清单二第四行规定了密码长度,它使用{min, max}限定了成功匹配的最小次数和最大次数。此时,若单词边界之间有6到32个字母或数字,则“//b(?i)([a-z0-9]){6,32}//b”与之匹配。注意,清单三中使用Pattern类的final变量指定了大小写无关选项,提高了正则表达式的可读性。这些变量将于本文后续部分讨论。
这两个正则表达式一起使用就可以认定“010101”和“m0rpheus”是合法的,而“agentsmith”是非法的。除了正则表达式,也有其他办法来验证密码,但是,你可以看到,使用正则表达式是多么简洁雅致。
深入元字符 ----分组和捕捉(grouping, capturing)
正则表达式中圆括号的作用有二:分组和捕捉。
圆括号“()”将元素分组,捕捉相应的子匹配。后向引用(/1,/2,等等)允许在同一个正则表达式中引用相应的组捕捉的文本。圆括号的计算从左至右进行,它在正则表达式中出现的位置(左起)决定了相应的后向引用内容。Java同时支持从正则表达式外部访问捕捉的文本,这是通过元件“$1”“$2”等实现的,其中元件“$1”就是指向“/1”包含的内容或者java.util.regex.Matcher类的group(n)方法的句柄。
另一种类型的圆括号“(?:)”只进行分组,不捕捉任何文本。但不需要进行后向引用时,这就是一个有用的元件。它不保留匹配状态,从而提高匹配操作的速度。这也就是上文提到的清单二和清单三的不同之处。注意,清单三由于使用了不捕捉型的圆括号,因而效率更高。
java.util.regex包
这个包相当小,只由两个final类,即java.util.regex.Pattern和java.util.regex.Matcher,和一个异常类java.util.regex.PatternSyntaxException组成。这几个类形成了Java的正则表达式框架。
Pattern类的实例就是对正则表达式字符串进行编译的结果。Matcher对象负责在给定的字符序列上的匹配操作,并提供访问和使用匹配结果的额外功能。Pattern类还提供一个静态的matches(String pattern, String text)方法,用于该范式(和以及匹配)不需要保留并复用的情况。注意,清单三中范式pContent和pLength都在方法外定义,这使得范式对象可以在多次方法调用中复用,从而提高了效率。如果使用java.lang.String的matches()方法,那么你就无法使用这个选项,正如清单二所示。
Pattern类的compile()方法接收一个待编译的正则表达式字符串,它将对该表达式进行语法检查。如果发现语法错误,那么它将抛出一个非检查异常(unchecked exception)java.util.regex.PatternSyntaxException。上文讨论的修饰符可在编译时加入,Pattern类以标记形式表征这些修饰符的static final变量(CASE_INSENSITIVE,DOTALL,等等)。多个标记可用或运算符“|”指定,如清单三23行的pLength范式中所示。
类似地,还有许多进行结果查询的方法。group(),group(int i)是最常用的两个。group()方法用于访问上一次匹配中符合匹配的文本,而group(int i)则返回第i组捕捉的文本(使用捕捉型圆括号)。例如,正则表达式“(//d)([A-C])”应用于字符串“2140AD”时,group(1)将返回“0”,group(2)将返回“A”,group(0)将返回“0A”。注意,group(0)总是返回整个匹配文本。
最后,让我们讨论一下Matcher类的文本替换功能,然后看看两个例子。replaceAll(String newText)方法将所有匹配的文本替换为给定的新文本,而replaceFirst(String newText)则只替换掉第一次匹配的文本。高级的替换操作apendReplacement()和appendTail()提供了对替换操作的细粒度控制。清单四演示了它们的使用。
示例二:文本转换
到目前为止,我们已经讲了输入字符串验证的例子。现在,让我们一起看看对密码验证例子的扩展,在本例中,目标文本被修改以符合规定。Zion的安全策略要求密码在传输前以某种方式进行加密。请看清单四中的一个简单的文本转换工具,它使用正则表达式将文本中数字颠倒过来。本例的目的不是要编写一个成熟的加密算法,而是演示正则表达式的一些高级特性。
本程序使用了一个简单的正则表达式(第5行)来匹配密码中的数字。find()方法被用于搜索数字,它也在匹配结果的迭代中起了作用(第7行)。group(1)方法返回捕捉型圆括号“(//d)”捕捉的文本,即字符类的所有数字。然后根据一个数字数组中,将捕捉的数字颠倒大小,再添加到表示新密码的StringBuffer中。appendReplacement方法负责插入替换文本,而appendTail则负责追加余下的文本。密码“010101”将被转换为“989898”。
示例三
清单五中的范式演示了正则表达式在匹配email和Web地址上的应用。email范式(清单五,行1)匹配所有以matrix.com、matrix.net或matrix.org结尾的地址。它也匹配了紧跟着的人名。例如,该范式匹配紧跟在email后面出现的单词“Trinity”,tn@matrix.com(Trinity)。URL范式匹配主机和可选的路径 http://www.zion.com/antimatrix.html 。注意,额外指定的Pattern.MULTILINE选项指出,URL可能延续多行。默认的行为是匹配当前行。
结论
正则表达式方便了诸如表单验证、转换文本为HTML或反之、解析文本、帮助程序等范式匹配程序的编写。随着正则表达式包在Java中的引入,正则表达式的广泛使用变得更为方便,不再依赖于外部包。我们可以看到,常用的正则表达式元件的使用都在Java中得到支持,本文也演示了其使用。然而,由于篇幅限制,本文不可能对元件进行更多讨论。详细讨论请参考本文末尾列出的文献。
注意,在其他语言中(如Perl,.NET,等等),Java中的正则表达式元件可能意义略有变化,因此正则表达式可能不是完全可移植的。
清单一列出了Java支持的正则表达式元字符的一个子集,清单二演示了使用java.lang.String的密码验证。清单三演示了使用java.util.regex的密码验证。清单四演示了使用java.util.regex进行文本转换。