Java正则表达式完全攻略(非常详细,附带实例)
正则表达式用于指定字符串模式,可以使用正则表达式来定位与特定模式匹配的字符串。
例如,假设你想在 HTML 文件中查找超链接,需要查找 <a href="..."> 模式的字符串。但请注意,这个字符串内可能会有多余的空格,或者 URL 可能被单引号括起来,这些都会影响对字符串的查找。但是正则表达式为你提供了一个更加精确的语法,用于判断哪些字符序列是合法匹配的。
接下来,你将看到 Java API 中的正则表达式语法,以及如何使用正则表达式。
字符类(character class)是一组用方括号括起来的可选择的字符集合,例如 [Jj]、 [0-9]、[A-Za-z] 或 [^0-9]。在字符类中,字符 - 表示一个范围(Unicode 值位于两个边界之间的所有字符)。然而如果符号 - 是字符类中的第一个或最后一个字符,则表示其自身( - )。如果字符 ^ 是字符类中的第一个字符,则表示补集(除指定的字符外的所有字符)。
此外还有许多预定义的字符类(predefined character class),例如 \d(数字)或 \p{Sc}(Unicode 货币符号)。参见表 1。
字符 ^ 和 $ 分别匹配输入的开头和结尾。
如果需要使用. * + ? { | () [ \ ^ $ 这些符号的字面意思,那么需要在这些符号前加一个反斜杠。在字符类中,只需要转译 [ 和 \ ,前提是仔细处理 ] - ^ 的位置。例如, []^-] 是一个包含这 3 个字符的类。
或者也可以使用 \Q 和 \E 将字符串括起来。例如,\(\$0\.99\) 和 \Q($0.99)\E 都与字符串 ($0.99) 匹配。
如果字符串包含较多的正则表达式语法中的特殊字符,则可以通过调用 Parse.quote(str) 将它们全部转义。这样做会直接用 \Q 和 \E 把字符串括起来,而且可以处理好 str 内包含 \E 的特殊情况。
在第一种情况下,可以直接使用静态 matches() 方法:
如果需要多次使用同一个正则表达式,那么编译它能更好地提高运行效率。然后,为每个输入都创建一个 Matcher:
如果想要测试输入是否包含匹配,可以使用 find() 方法来替代:
更优雅的方式是,可以调用 results() 方法来获取一个 Stream<MatchResult>。MatchResult 接口就像 Matcher 类一样,有 group、start() 和 end() 方法。(事实上,Matcher 类实现了这个接口。)
下面展示了如何获取所有匹配的列表:
如果在文件中保存了数据,则可以使用 Scanner.findAll() 方法来获取一个 Stream< MatchResult>,这样就无须将先将内容读取到字符串中。可以将 Pattern 或模式字符串作为参数传递:
以下是一个正则表达式,它可以用分组的形式来处理数据中每个部分:
在匹配后,可以从匹配器中提取第 n 个分组:
分组按照它们的左括号排序,编号从 1 开始(分组 0 表示整个输入)。下列代码片段演示了如何将输入拆分:
这里我们对分组2并不感兴趣,因为它由重复产生的括号组成。为了更加清楚地表示,我们可以使用非捕获组来处理:
然后,就可以使用名称来获取商品了:
通过 start() 和 end() 方法,可以获得分组在输入中的位置:
如果有很多标记,则可以通过以下代码惰性地获取它们:
如果不关心预编译模式或惰性获取,那么可以使用String.split() 方法:
如果输入数据在文件中,那么需要使用扫描器:
如果不关注预编译模式,那么可以使用 String 类的 replaceAll() 方法:
替换字符串可以包含分组编号 $n 或名称 ${name},它们将被替换为对应的捕获组的内容:
可以使用 \ 来转义替换字符串中的 $ 和 \,也可以调用 Matcher.quoteReplacement() 方法来进行便捷处理:
如果想执行比按照分组匹配拼接更复杂的操作,那么可以使用替换函数来取代替换字符串。该函数接受 MatchResult 作为参数并生成字符串。例如,下面的例子中我们用大写字母来替换所有的至少包含 4 个字母的单词:
以下是一些常用标志:
最后两个标志不能在正则表达式内部指定。
例如,假设你想在 HTML 文件中查找超链接,需要查找 <a href="..."> 模式的字符串。但请注意,这个字符串内可能会有多余的空格,或者 URL 可能被单引号括起来,这些都会影响对字符串的查找。但是正则表达式为你提供了一个更加精确的语法,用于判断哪些字符序列是合法匹配的。
接下来,你将看到 Java API 中的正则表达式语法,以及如何使用正则表达式。
Java正则表达式语法
在正则表达式中,字符表示其自身,除非它是保留字符之一:. * + ? { | () [ \ ^ $例如:
- 正则表达式 Java 只能匹配字符串 Java;
- 符号 . 匹配任意单个字符。例如,.a.a 能够匹配 Java 和 data;
- 符号 * 则表示前面的构造可以重复出现 0 次或任意多次,符号 +则表示前面的构造重复 1 次或任意多次。后缀 ? 表示前面的构造是可选的(0 次或 1 次)。例如,be+s? 能够匹配 be、bee 和 bees。可以使用 { } 来指定其他类型的重复次数 (见表 1);
- 符号 | 表示选择:.(oo|ee)f 能够匹配 beef 或 woof。注意其中的圆括号,如果没有它们,那么.oo|eef 将表示 .oo 或者 eef。圆括号也可以用于分组。
字符类(character class)是一组用方括号括起来的可选择的字符集合,例如 [Jj]、 [0-9]、[A-Za-z] 或 [^0-9]。在字符类中,字符 - 表示一个范围(Unicode 值位于两个边界之间的所有字符)。然而如果符号 - 是字符类中的第一个或最后一个字符,则表示其自身( - )。如果字符 ^ 是字符类中的第一个字符,则表示补集(除指定的字符外的所有字符)。
此外还有许多预定义的字符类(predefined character class),例如 \d(数字)或 \p{Sc}(Unicode 货币符号)。参见表 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 | 上一个匹配的结尾 |
名字 | 功能描述 |
---|---|
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");
注意,如果分组中有重复,比如上面的例子中的 (\s+\p{Alnum}+)*,那么将不能得到它的所有匹配。group() 方法只产生最后一个匹配,这基本是无意义的。这时,需要用另一个分组来捕获整个表达式。按名称获取分组只适用于 Matcher,而不适用于 MatchResult。
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);
以下是一些常用标志:
- Pattern.CASE_INSENSTIVE 或 i:匹配字符时忽略字母大小写。默认情况下,此标志仅考虑 US ASCII 字符。
- Pattern.UNICODE_CASE 或 u:当与 CASE_INSENSITIVE 组合使用时,使用 Unicode 字母大小写进行匹配。
- Pattern.UNICODE_CHARACTER_CLASS 或 U:使用 Unicode 字符类代替 POSIX。该标志隐含了UNICODE_CASE。
- Pattern.MULTILINE 或 m:使 ^ 和 $ 匹配行的开头和结尾,不是整个输入的开头和结果。
- Pattern.UNIX_LINES 或 d:当在多行模式中匹配 ^ 和 $ 时,仅 '\n' 表示行终止符。
- Pattern.DOTALL 或 s:使 . 符号匹配所有字符,包括行终止符。
- Pattern.COMMENTS 或 x:空白字符和注释(从 # 到行末尾)都会被忽略。
- Pattern.LITERAL:除大小写字母外,该模式将被逐字采纳,并且必须精确匹配。
- Pattern.CANON_EQ:考虑 Unicode 字符的规范等效性。例如,u 后跟 ¨ (分音符)匹配 ü。
最后两个标志不能在正则表达式内部指定。