计算机诞生初期处理的信息几乎都是数值,但是时过境迁,今天我们使用计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符串匹配和处理的时候最为强大的工具。在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码,绝大多数语言都提供了对正则表达式的支持。
Java中的String类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java中可以用Pattern类表示正则表达式对象,它提供了丰富的API进行各种正则表达式操作。
下面列出了部分的正则匹配规则,完整的匹配规则请查看相关文档。
字符
B | 指定字符B |
\xhh | 十六进制值为oxhh的字符 |
\uhhhh | 十六进制表示为oxhhhh的Unicode字符 |
\t | 制表符Tab |
\n | 换行符 |
\r | 回车 |
\f | 换页 |
\e | 转义(Escape) |
字符类
. | 任意字符 |
[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]] | 任意a到z中除了h、i和j的字符 |
\s | 空白符(空格、tab、换行、换页和回车) |
\S | 非空白符([^\s]) |
\d | 数字[0-9] |
\D |
非数字[^0-9] |
\w | 词字符[a-zA-Z0-9] |
\W | 非词字符[^\w] |
逻辑操作符
XY | Y跟在X后面 |
X|Y | X或Y |
(X) | 捕获组(capturing group) |
String s = "Hello World! Hello Java! Hello Regular Expression!";
String regex = "(Hello(.*?))\\s*(Hello\\s*\\w+.)\\s*(.*)";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(s);
if (m.find()) {
System.out.println(m.group(0));
System.out.println(m.group(1));
System.out.println(m.group(2));
System.out.println(m.group(3));
System.out.println(m.group(4));
}
运行结果:
Hello World! Hello Java! Hello Regular Expression!
Hello World!
World!
Hello Java!
Hello Regular Expression!
边界匹配符
^ | 一行的起始 |
$ | 一行的结束 |
\b | 词的边界 |
\B | 非词的边界 |
\G | 前一个匹配的结束 |
量词
贪婪型 | 勉强型 | 占有型 | 如何匹配 |
---|---|---|---|
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次 |
量词描述了一个模式吸收输入文本的方式:
- 贪婪型:量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。导致此问题的一个典型理由就是假定我们的模式仅能匹配第一个可能的字符组,如果它是贪婪的,那么它就会继续往下匹配。
- 勉强型:用问号来指定,这个量词匹配满足模式所需的最少字符数。因此也称作懒惰的、最少匹配的、非贪婪的、或不贪婪的。
- 占有型:目前,这种类型的量词只有在Java语言中才可用(在其他语言中不可用),并且也更高级,因此我们大概不会立刻用到它。当正则表达式被应用于字符串时,它会产生相当多的状态,以便在匹配失败时可以回溯。而“占有的”量词并不保存这些中间状态,因此它们可以防止回溯。它们常常用于防止正则表达式失控,因此可以使正则表达式执行起来更有效。
如下面的正则表达式所示:
private static final String SPLIT_SQL = "INSERT\\s+INTO(.*?)\\s*(WITH\\s*(.*?)\\)\\s+AS\\s+\\((.*?)\\))?\\s*(SELECT\\s+.*)";
该正则是笔者在实际项目中所写的正则表达式,目的是为了匹配db2 insert中带with语法的sql,sql如下:
INSERT INTO SCHEMA1.TABLE1 (
A,
B,
) WITH TEMP_TABLE1 (
C,
D
) AS (
SELECT DISTINCT
C,
D
FROM
SCHEMA1.TABLE2
WHERE
E = '001'
) SELECT
//DO SOMETHING...
可以看到,使用了勉强型,即惰性匹配。如果不使用惰性匹配,会出现匹配错误的情况:正则表达式默认是贪婪匹配,即尽可能多的匹配。所以,如果一条sql的后面(即在do something...里)出现“) select...”的部分,会一起被匹配进“(SELECT\\s+.*)”里。之前最开始的select会在insert into后面的.*中被匹配(应该在“(SELECT\\s+.*)”中被匹配)。读者可自行尝试体会。
警惕回溯问题
前面说过,正则表达式默认是贪婪匹配的,所以可能会存在回溯问题。考虑下面的例子:
long start = System.currentTimeMillis();
Pattern r = Pattern.compile("^((\\d+,\\d+)*)$");
Matcher m = r.matcher("12345,12345,12345,12345,12345,12345,12345,12345,12345,12345");
while (m.find()) {
System.out.println(m.group(2));
}
long end = System.currentTimeMillis();
System.out.println(end - start);
该正则能成功匹配出来,在笔者电脑上实测,运行时间为1毫秒。但假如现在匹配的字符串最后加上一个“,”,即
Matcher m = r.matcher("12345,12345,12345,12345,12345,12345,12345,12345,12345,12345,");
则匹配失败(这是肯定的),但运行时间却大幅增加,大概为93毫秒左右。并且随着字符串中12345的数量逐渐增多,解析时间会越来越慢。
为什么匹配失败却还要花费这么多时间?这里就涉及到了回溯的问题。回溯指的是因为贪婪特性,正则表达式匹配尽可能多的字符后,发现后面的匹配字符失败。所以此时将之前已经匹配过的字符中“吐”出来一个,来进行匹配。如果还不能匹配,则接着“吐”出一个字符来匹配,以此类推。
上面的例子中首次匹配的最后两次结果为“5,1234”和“5,12345”(尽可能多的匹配),但是因为最后还有一个逗号,所以之前匹配的结果中吐出来一个5,现在剩下了“5,”,但是不匹配,继续回溯,剩下“45,”,仍不匹配,继续回溯...最后回溯到“5,1”,剩下“2345,”,此时依然不匹配。所以现在只能回溯倒数第二个匹配组,此时最后两次匹配组变成了“5,123”和“45,12345”。然后继续像上面一样回溯最后一次匹配组,直到匹配成功终止。
可以见到,程序匹配上述正则时回溯了很多次,这对于字符串很长的情况下几乎是灾难性的。上述问题可以将正则作如下修改,以防止回溯出现:
Pattern r = Pattern.compile("^((\\d+,\\d+)*).$");
当然也有很多其他的解决方案。这里只是提醒读者,要注意回溯引起的正则匹配灾难问题。
前瞻后瞻
正向前瞻 string(?=pattern)
正则:"jdk (?=6|7|8)";字符串:"jdk 8";匹配结果:"jdk"负向前瞻 string(?!pattern)
正向后瞻 (?<=pattern)string
负向后瞻 (?<!pattern)string
正则:"(?<!j|d|k)8";字符串:"jdk 1.8";匹配结果:"8"
当然,前瞻后瞻也可以用捕获组的方式来实现,只不过前瞻后瞻的方式更简练。注意:JavaScript不支持后瞻。
还有一些其他常用的方法,例如:start()返回先前匹配的起始位置的索引,而end()返回所匹配的最后字符的索引加一的值。replaceFirst(String replacement)以参数字符串replacement替换掉第一个匹配成功的部分。replaceAll(String replacement)以参数字符串replacement替换掉所有匹配成功的部分。
在java中转义字符的使用需要用两个反斜杠来表示。例如我现在要匹配小括号,因为小括号在正则表达式中有别的含义(捕获组),所以需要转义,即\(。但是在java的字符串中反斜杠也有别的含义,表示转义,所以需要两个反斜杠。即两个反斜杠在java字符串中被翻译成一个反斜杠,即\(,然后再被正则翻译成小括号。正确写法是“\\(”。
上述概念不需要强记,但是一定要知道有这些方法,等需要的时候去查就行了。正则表达式使好了,可以匹配很多复杂的场景。例如下面是笔者在实际开发中所写的正则,目的是为了匹配sql中带括号的where子句:
private static final String WHERE = "WHERE\\s*((\\()?\\s*[0-9a-zA-Z_]*(\\.)?[0-9a-zA-Z_]+\\.[0-9a-zA-Z_]+\\s*(,\\s*[0-9a-zA-Z_]*(\\.)?[0-9a-zA-Z_]+\\.[0-9a-zA-Z_]+)*(\\))?\\s*IN\\s*)?(\\(.*)";