正则表达式是一种强大而灵活的文本处理工具。使用正则表达式,我们能够以编程的形式,构造复杂的文本模式,并对输入的字符串进行搜索。一旦找到了匹配这些模式的部分,你就能够随心所欲地对它们进行处理。初学正则表达式时,其语法是一个难点,但它确实是一种简洁动态的语言。正则表达式提供了一种安全通用的方式,能够解决各种字符串处理相关的问题:匹配、选择、编辑以及验证。
一、基础
一般来说,正则表达式就是以某种方式来描述字符串,因此你可以说:“如果一个字符串含有这些东西,那么它就是我正在找的东西”。例如,要找一个数字,它可能有一个负号在最前面,那你就写一个负号加上一个问号,就像这样:-?
要描述一个整数,你可以说它有一位或多位阿拉伯数字。在正则表达式中,用\d表示一位数字。如果在其他语言中使用过正则表达式,那你立刻就能发现java对反斜线\的不同处理。其他语言中,\\表示“我想要在正则表达式中插入一个普通的(字面上的)反斜线,请不要给它特殊的意义。”而在java中,\\意思是“我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。”例如,如果你想表达一位数字,那么正则表达式应该是\\d。如果你想插入一个普通的反斜线,则应该这样\\\\。不过换行和制表符之类的东西只需使用单反斜线:\n\t。
要表示“一个或多个之前的表达式”,应该使用+。所以,如果要表示“可能有一个负号,后面跟着一位或多位数字”,可以 这样:
-?\\d+
应用正则表达式的最简单的途径,就是利用String类内建的功能。例如,你可以检查一个String是否匹配如上所述的正则表达式:
public class IntegerMatch {
public static void main(String[] args) {
System.out.println("-1234".matches("-?\\d+"));
System.out.println("5678".matches("-?\\d+"));
System.out.println("+911".matches("-?\\d+"));
System.out.println("+911".matches("(-|\\+)?\\d+"));
}
}
前两个字符串满足对应的正则表达式,匹配成功。第三个字符串开头有一个+,它也是一个合法的整数,但与对应的正则表达式却不匹配。因此,我们的正则表达式应该描述为:“可能以一个加号或减号开头”。在正则表达式中,括号有着表达式分组的效果,而竖直线|则表示或操作。也就是:(-|\\+)?
这个正则表达式表示字符串的起始字符可能是一个-或+,或二者皆没有(因为后面跟着?修饰符)。因为字符+在正则表达式中有特殊意义,所以必须使用\\将其转义,使之成为表达式中的一个普通字符。
String类型还自带了一个非常有用的正则表达式工具split()方法,其功能是“将字符串从正则表达式匹配的地方切开。”
import java.util.Arrays;
public class Splitting {
public static String knights = "Then, when you have found the shrubbery, you must cut down the mightiest tree in the forest... with... a herring!";
public static void split(String regex) {
System.out.println(Arrays.toString(knights.split(regex)));
}
public static void main(String[] args) {
split(" ");
split("\\W+");
split("n\\W+");
}
}
首先看第一个语句,注意这里用的是普通的字符作为正则表达式,其中并不包含任何特殊的字符。因此第一个split()只是按空格来划分字符串。
第二个和第三个split()都用到了\W,它的意思是非单词字符(如果W小写,\w,则表示一个单词字符)。通过第二个例子可以看到,它将标点字符删除了。第三个split()表示“字母n后面跟着一个或多个非单词字符。”可以看到,在原始字符串中,与正则表达式匹配的部分,在最终结果中都不存在了。
String的split()还有一个重载的版本,它允许你限制字符串分割的次数。
String类自带的最后一个正则表达式工具是“替换”。你可以只替换正则表达式第一个匹配的子串,或是替换所有匹配的地方。
public class Replacing {
static String s = Splitting.knights;
public static void main(String[] args) {
System.out.println(s.replaceFirst("f\\w+", "located"));
System.out.println(s.replaceAll("shrubbery|tree|herring", "banana"));
}
}
第一个表达式要匹配的是,以字母f开头,后面跟着一个或多个字母(注意这里的w是小写的)。并且只替换掉第一个匹配的部分,所以“found”被替换成“located”。
第二个表达式要匹配的是三个单词中的任意一个,因为它们以竖直线分隔表示“或”,并且替换所有匹配的部分。
稍后你会看到,String之外的正则表达式还有更强大的替换工具,例如,可以通过方法调用执行替换。而且,如果正则表达式不是只使用一次的话,非String对象的正则表达式明显具备更佳性能。
二、创建正则表达式
我们首先从正则表达式可能存在的构造集中选取一个很有用的子集,以此开始学习正则表达式。正则表达式的完整构造子列表,请参考JDK文档java.util.regex包中的Pattern类。
字符 | |
B | 指定字符B |
\xhh | 十六进制值为oxhh的字符 |
\uhhhh | 十六进制表示为oxhhhh的Unicode字符 |
\t | 制表符 |
\n | 换行符 |
\r | 回车 |
\f | 换页 |
\e | 转义(Escape) |
当你学会了使用字符类(character classes)之后,正则表达式的威力才能真正显现出来。以下是一些创建字符类的典型方式,以及一些预定义的类:
字符类 | |
. | 任意字符 |
[abc] | 包含a、b和c的任何字符(和a|b|c作用相同) |
[^abc] | 除了a、b和c之外的任何字符(否定) |
[a-zA-Z] | 从a到z或从A到Z的任何字符(范围) |
[abc[hij]] | 任意a、b、c、h、i和j字符(与a|b|c|h|i|j作用相同)(合并) |
[a-z&&[hij]] | 任意h、i或j(交) |
\s | 空白符(空格、tab、换行、换页和回车) |
\S | 非空白符([^\s]) |
\d |
数字[0-9] |
\D | 非数字[^0-9] |
\w | 词字符[a-zA-Z0-9_] |
\W | 非词字符[^\w] |
这里只列出了部分常用的表达式,你应该将JDK文档中java.util.regex.Pattern那一页加入浏览器书签中,以便在需要的时候方便查询。
逻辑操作符 | |
XY | Y跟在X后面 |
X|Y | X或Y |
(X) | 捕获组(capturing group)。可以在表达式中用\i引用第i个捕获组 |
边界操作符 | |
^ | 一行的开始 |
$ | 一行的结束 |
\b | 词的边界 |
\B | 非词的边界 |
\G | 前一个匹配的结束 |
作为演示,下面的每一个正则表达式都能成功匹配字符序列“Rudolph”:
public class Rudolph {
public static void main(String[] args) {
for (String pattern : new String[] { "Rudolph", "[rR]udolph", "[rR][aeiou][a-z]ol.*", "R.*" })
System.out.println("Rudolph".matches(pattern));
}
}
当然了,我们的目的并不是编写最难理解的正则表达式,而是尽量编写能够完成任务的最简单以及最必要的正则表达式。一旦真正开始使用正则表达式了,你就会发现,在编写新的表达式之前,你通常会参考代码中已经用到的正则表达式。
三、量词
量词描述了一个模式吸收输入文本的方式:
- 贪婪型:量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多个匹配。导致此问题的一个典型理由就是假定我们的模式仅能匹配第一个可能的字符组,如果它是贪婪的,那么它就会继续往下匹配。
- 勉强型:用问号来指定,这个量词匹配满足模式所需的最少字符数。因此也称作懒惰的、最少匹配的、非贪婪的或不贪婪的。
- 占有型:目前,这种类型的量词只有在java语言中才可用(在其他语言中不可用),并且也更高级,因此我们大概不会立刻用到它。当正则表达式被应用于字符串时,它会产生相当多的状态,以便在匹配失败时可以回溯。而“占有型”量词并不保存这些中间状态,因此它们可以防止回溯。它们常常用于防止正则表达式失控,因此可以使正则表达式执行起来更有效。
贪婪型 | 勉强型 | 占有型 | 如何匹配 |
X? | X?? | X?+ | 一个或零个X |
X* | X*? | X*+ | 零个或多个X |
X+ | X+? | X++ | 一个或多个X |
X{n} | X{n}? | X{n}+ | 恰好n次X |
X{n,} | X{n,}? | X{n,}+ | 至少n次X |
X{n,m} | X{n,m}? | X{n,m}+ | X至少n次,且不超过m次 |
你应该非常清楚的意识到,表达式X通常必须要用圆括号括起来,以便它能够按照我们期望的效果去执行。例如:abc+。
看起来它似乎应该匹配1个或多个abc序列,如果我们把它应用于输出字符串abcabcabc,则实际上会得到3个匹配。然而,这个表达式实际上表示的是:匹配ab,后面跟随1个或多个c。要表明匹配1个或多个完整的abc字符串,我们必须这样表示:(abc)+
你会发现,使用正则表达式时很容易混淆,因此是一种java之上的新语言。
接口CharSequence从CharBuffer、String、StringBuffer、StringBuilder类之中抽象出了字符序列的一般化定义:
public interface CharSequence {
int length();
char charAt(int index);
CharSequence subSequence(int start, int end);
public String toString();
public default IntStream chars() {
class CharIterator implements PrimitiveIterator.OfInt {
int cur = 0;
public boolean hasNext() {
return cur < length();
}
public int nextInt() {
if (hasNext()) {
return charAt(cur++);
} else {
throw new NoSuchElementException();
}
}
@Override
public void forEachRemaining(IntConsumer block) {
for (; cur < length(); cur++) {
block.accept(charAt(cur));
}
}
}
return StreamSupport.intStream(() ->
Spliterators.spliterator(
new CharIterator(),
length(),
Spliterator.ORDERED),
Spliterator.SUBSIZED | Spliterator.SIZED | Spliterator.ORDERED,
false);
}
public default IntStream codePoints() {
class CodePointIterator implements PrimitiveIterator.OfInt {
int cur = 0;
@Override
public void forEachRemaining(IntConsumer block) {
final int length = length();
int i = cur;
try {
while (i < length) {
char c1 = charAt(i++);
if (!Character.isHighSurrogate(c1) || i >= length) {
block.accept(c1);
} else {
char c2 = charAt(i);
if (Character.isLowSurrogate(c2)) {
i++;
block.accept(Character.toCodePoint(c1, c2));
} else {
block.accept(c1);
}
}
}
} finally {
cur = i;
}
}
public boolean hasNext() {
return cur < length();
}
public int nextInt() {
final int length = length();
if (cur >= length) {
throw new NoSuchElementException();
}
char c1 = charAt(cur++);
if (Character.isHighSurrogate(c1) && cur < length) {
char c2 = charAt(cur);
if (Character.isLowSurrogate(c2)) {
cur++;
return Character.toCodePoint(c1, c2);
}
}
return c1;
}
}
return StreamSupport.intStream(() ->
Spliterators.spliteratorUnknownSize(
new CodePointIterator(),
Spliterator.ORDERED),
Spliterator.ORDERED,
false);
}
}
因此,这些都实现了该接口,多数正则表达式操作都接受CharSequence类型的参数。
未完待续。如果本文对您有很大的帮助,还请点赞关注一下。