首页 > 编程笔记 > Java笔记 阅读:4

Java正则表达式完全攻略(非常详细,附带实例)

正则表达式用于指定字符串模式,可以使用正则表达式来定位与特定模式匹配的字符串。

例如,假设你想在 HTML 文件中查找超链接,需要查找 <a href="..."> 模式的字符串。但请注意,这个字符串内可能会有多余的空格,或者 URL 可能被单引号括起来,这些都会影响对字符串的查找。但是正则表达式为你提供了一个更加精确的语法,用于判断哪些字符序列是合法匹配的。

接下来,你将看到 Java API 中的正则表达式语法,以及如何使用正则表达式。

Java正则表达式语法

在正则表达式中,字符表示其自身,除非它是保留字符之一:
. * + ? { | () [ \ ^ $
例如:
字符类(character class)是一组用方括号括起来的可选择的字符集合,例如 [Jj]、 [0-9]、[A-Za-z] 或 [^0-9]。在字符类中,字符 - 表示一个范围(Unicode 值位于两个边界之间的所有字符)。然而如果符号 - 是字符类中的第一个或最后一个字符,则表示其自身( - )。如果字符 ^ 是字符类中的第一个字符,则表示补集(除指定的字符外的所有字符)。

此外还有许多预定义的字符类(predefined character class),例如 \d(数字)或 \p{Sc}(Unicode 货币符号)。参见表 1。

表 1 正则表达式语法
表达式 功能描述 范例
字符
c, 除 .\*+?{|()[\<^$ 之外的字符 字符 c J
. 表示除行终止符之外的其他任意字符,或者在 DOTALL 标志设置后的任意字符  
\x{p} 十六进制码为 p 的 Unicode 码点 \x{1D546}
\uhhhh, \xhh, \0o, \0oo, \0ooo 具有给定十六进制或八进制值的 UTF-16 码元 \uFEFF
\a, \e, \f, \n, \r, \t 响铃符 (\x{7})、转义符 (\x{1B})、换页符 (\x{B})、换行符 (\x{A})、回车符 (\x{D})、制表符 (\x{9}) \n
\cc 其中 c 在 [A-Z] 范围内,或者是 @[\]^_? 其中之一 字符 c 的控制字符 \cH 是一个退格符 (\x{8})
\c,c 不在 [A-Za-z0-9] 的范围内 字符 c \\
\Q ... \E 引号内的所有内容 \Q(...)\E 匹配字符串 (...)
字符类
[C1 C2 ...] , 其中 Ci 是 c-d 的多个字符,或者是字符类 任意由 C1、C2... 表示的字符 [0-9+-]
[^...] 字符类的补集 [^\d\s]
[...&&...] 字符类的交集 [\p{L}&&[^A-Za-z]]
\p{...}, \P{...} 预定义字符类(见表 2);它的补集 \p{L} 匹配一个 Unicode 字母,同时 \pL 也匹配该字母,可以忽略单个字母情况下的括号
\d, \D 数字 ([0-9],或者在设置了 UNICODE_CHARACTER_CLASS 标记时表示 \p {Digit};它的补集 \d+ 是一个数字序列
\w, \W 单词字符 [a-zA-Z0-9_],或者在设置了 UNICODE_CHARACTER_CLASS 标记时表示 Unicode 单词字符;它的补集  
\s, \S 空格 ([\n\r\t\f\x{B}],或者在设置了 UNICODE_CHARACTER_CLASS 标记时表示 \p{IsWhite_Space});它的补集 \s*,\s* 是一个由可选的空格字符包围的逗号
\h, \v, \H, \V 水平空白字符、垂直空白字符、它们的补集  
序列和选择
XY 任意从 X 开始,后面跟随 Y 的字符串 [1-9][0-9]* 是一个非 0 开头的正整数
X|Y 任意 X 或者 Y 的字符串 http|ftp
分组
(X) 捕获 X 的匹配 '([^’]*)' 捕获被引用的文本
\n 第 n 组 (['"]).*\1 匹配 'Fred' 或者 "Fred",但是不匹配 "Fred'
(?<name>X) 捕获与给定名称匹配的 X '(?<id>[A-Za-z0-9]+)' 捕获名称为 id 的匹配
\k<name> 给定名称的组 \k<id> 匹配名称为 id 的分
(?:X) 仅使用括号,不捕获 X 在 (?:http|ftp)://(.*) 中,在 :// 之后的匹配是 \1
(?f1f2...:X), (?f1...-fk...:X) 其中 fi 在 [dimsux] 中 匹配但是不捕获给定标志开或关(在 - 之后) (?i:jpe?g) 是一个不区分大小写的匹配
其他 (?.. .) 查阅 Pattern API 手册  
量词
X? X 是可选的 \+? 是可选的 + 号
X*, X+ 0 或多个 X;1 或多个 X [1-9][0-9]+ 表示大于等于 10 的整数
X{n}, X{n,}, X{m,n} n 个 X;至少 n 个 X;m 到 n 个 X [0-7]{1,3} 表示 1 到 3 位的八进制数
Q?, Q 是一个量词表达式 勉强量词,先尝试最短匹配,再尝试最长匹配 .*(<.+?>).* 捕获尖括号内的最短序列
Q+, Q 是一个量词表达式 独占量词,在不回溯的情况下获取最长匹配 '[^']*+' 匹配单引号内的字符串,并且在字符串中没有右侧单引号的情况下立即匹配失败
边界匹配
^ $ 输入的开头和结尾(或者多行模式中的开头行和结尾行) ^Java$ 匹配输入中的 Java 或者单行的 Java
\A \Z \z 输入的开头、输入的结尾、输入的绝对结尾(在多行模式中不变)  
\b \B 单词边界,非单词边界 \bJava\b 匹配单词 Java
\R Unicode 行分隔符  
\G 上一个匹配的结尾  


表 2 预定义的字符类 \p{...}
名字 功能描述
posixClass posixClass 是 Lower、Upper、Alpha、Digit、Alnum、Punct、Graph、Print、Cntrl、XDigit、Space、Blank、ASCII之一,它会依据UNICODE_CHARACTER_CLASS标志的值而被解释为POSIX或者Unicode类
IsScript, sc=Script, script=Script 能够被 Character.UnicodeScript.forName 接受的脚本
InBlock, blk = Block, block = Block 能够被 Character.UnicodeBlock.forName 接受的块
Category, InCategory, gc=Category, general_category=Category Unicode 通用类别中的单字母或者双字母的名字
IsProperty Property 是以下种类之一:Alphabetic、Ideographic、Letter、Lowercase、Uppercase、Titlecase、Punctuation、Control、White_Space、Digit、Hex_Digit、Join_Control、Noncharacter_Code_Point、Assigned
javaMethod 调用 Character.isMethod() 方法(必须是没有被废弃的方法)

字符 ^ 和 $ 分别匹配输入的开头和结尾。

如果需要使用. * + ? { | () [ \ ^ $ 这些符号的字面意思,那么需要在这些符号前加一个反斜杠。在字符类中,只需要转译 [ 和 \ ,前提是仔细处理 ] - ^ 的位置。例如, []^-] 是一个包含这 3 个字符的类。

或者也可以使用 \Q 和 \E 将字符串括起来。例如,\(\$0\.99\) 和 \Q($0.99)\E 都与字符串 ($0.99) 匹配。

如果字符串包含较多的正则表达式语法中的特殊字符,则可以通过调用 Parse.quote(str) 将它们全部转义。这样做会直接用 \Q 和 \E 把字符串括起来,而且可以处理好 str 内包含 \E 的特殊情况。

Java正则表达式检测匹配

通常使用正则表达式的方式有两种:检测字符串是否与正则表达式匹配,要么查找出字符串中正则表达式的所有匹配。

在第一种情况下,可以直接使用静态 matches() 方法:
String regex = "[+-]?\\d+";
CharSequence input = ...;
if (Pattern.matches(regex, input)) {
    ...
}

如果需要多次使用同一个正则表达式,那么编译它能更好地提高运行效率。然后,为每个输入都创建一个 Matcher:
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) ...
如果匹配成功,那么能够获取匹配组的位置。

如果想要测试输入是否包含匹配,可以使用 find() 方法来替代:
if (matcher.find()) ...

Java正则表达式查找所有匹配

考虑正则表达式的另一个常见用法——在输入中查找所有匹配。为此,我们可以使用下面的循环:
String input = ...;
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
   String match = matcher.group();
   int matchStart = matcher.start();
   int matchEnd = matcher.end();
   ...
}
通过这种方式,可以依次处理每个匹配。正如上面的代码片段所示,可以获取匹配的字符串及其在输入字符串中的位置。

更优雅的方式是,可以调用 results() 方法来获取一个 Stream<MatchResult>。MatchResult 接口就像 Matcher 类一样,有 group、start() 和 end() 方法。(事实上,Matcher 类实现了这个接口。)

下面展示了如何获取所有匹配的列表:
List<String> matches = pattern.matcher(input)
.results()
.map(Matcher::group)
.toList();

如果在文件中保存了数据,则可以使用 Scanner.findAll() 方法来获取一个 Stream< MatchResult>,这样就无须将先将内容读取到字符串中。可以将 Pattern 或模式字符串作为参数传递:
var in = new Scanner(path, StandardCharsets.UTF_8);
Stream<String> words = in.findAll("\\pL+")
.map(MatchResult::group);

Java正则表达式分组

通常可以使用分组来提取匹配的组件。例如,假设在发票中有一个行项目,包括商品名称、数量和单价等信息,如下所示:
Blackwell Toaster USD29.95

以下是一个正则表达式,它可以用分组的形式来处理数据中每个部分:
(\p{Alnum}+(\s+\p{Alnum}+)*)\s+([A-Z]{3})([0-9.]*)

在匹配后,可以从匹配器中提取第 n 个分组:
String contents = matcher.group(n);

分组按照它们的左括号排序,编号从 1 开始(分组 0 表示整个输入)。下列代码片段演示了如何将输入拆分:
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) {
   item = matcher.group(1);
   currency = matcher.group(3);
   price = matcher.group(4);
}

这里我们对分组2并不感兴趣,因为它由重复产生的括号组成。为了更加清楚地表示,我们可以使用非捕获组来处理:
?:
或者按名字捕获:
<item><currency><price>

然后,就可以使用名称来获取商品了:
item = matcher.group("item");

通过 start() 和 end() 方法,可以获得分组在输入中的位置:
int itemStart = matcher.start("item");
int itemEnd = matcher.end("item");

按名称获取分组只适用于 Matcher,而不适用于 MatchResult。

注意,如果分组中有重复,比如上面的例子中的 (\s+\p{Alnum}+)*,那么将不能得到它的所有匹配。group() 方法只产生最后一个匹配,这基本是无意义的。这时,需要用另一个分组来捕获整个表达式。

Java正则表达式按分隔符拆分

有时,你希望按照匹配的分隔符拆分输入,并保持其他内容不变。Pattern.split() 方法将会自动完成这项工作。你可以获得一组去掉分隔符的字符串数组:
String input = ...;
Pattern commas = Pattern.compile("\\s*,\\s*");
String[] tokens = commas.split(input);
// "1, 2, 3" turns into ["1", "2", "3"]

如果有很多标记,则可以通过以下代码惰性地获取它们:
Stream<String> tokens = commas.splitAsStream(input);

如果不关心预编译模式或惰性获取,那么可以使用String.split() 方法:
String[] tokens = input.split("\\s*,\\s*");

如果输入数据在文件中,那么需要使用扫描器:
var in = new Scanner(path, StandardCharsets.UTF_8);
in.useDelimiter("\\s*,\\s*");
Stream<String> tokens = in.tokens();

Java正则表达式替换匹配

如果希望用字符串来替换正则表达式的所有匹配,那么可以调用匹配器的 replaceAll() 方法:
Matcher matcher = commas.matcher(input);
String result = matcher.replaceAll(",");
// Normalizes the commas

如果不关注预编译模式,那么可以使用 String 类的 replaceAll() 方法:
String result = input.replaceAll("\\s*,\\s*", ",");

替换字符串可以包含分组编号 $n 或名称 ${name},它们将被替换为对应的捕获组的内容:
String result = "3:45".replaceAll(
"(\\d{1,2}):(?<minutes>\\d{2})",
"$1 hours and ${minutes} minutes");
// Sets result to "3 hours and 45 minutes"

可以使用 \ 来转义替换字符串中的 $ 和 \,也可以调用 Matcher.quoteReplacement() 方法来进行便捷处理:
matcher.replaceAll(Matcher.quoteReplacement(str))

如果想执行比按照分组匹配拼接更复杂的操作,那么可以使用替换函数来取代替换字符串。该函数接受 MatchResult 作为参数并生成字符串。例如,下面的例子中我们用大写字母来替换所有的至少包含 4 个字母的单词:
String result = Pattern.compile("\\pL{4,}")
.matcher("Mary had a little lamb")
.replaceAll(m -> m.group().toUpperCase());
// Yields "MARY had a LITTLE LAMB"
replaceFirst() 方法将只替换模式匹配中第一次出现的匹配。

Java正则表达式的标志

标志(flag)可以改变正则表达式的行为。可以在编译模式时指定它们:
Pattern pattern = Pattern.compile(regex,
Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CHARACTER_CLASS);

以下是一些常用标志:
最后两个标志不能在正则表达式内部指定。

相关文章