文章目录
- 第十八章 字符串
- 6. 常规表达式
- 6.1 基础
- 6.2 创建正则表达式
- 6.3 量词
- 6.4 CharSequence
- 6.5 Pattern 和 Matcher
- 6.5.1 find()
- 6.5.2 组(Groups)
- 6.5.3 start() 和 end()
- 6.5.4 split()
- 6.5.5 替换操作
- 6.5.6 reset()
- 7. 扫描输入
- 7.1 Scanner 分隔符
- 7.2 用正则表达式扫描
- 8. StringTokenizer 类
- 9. 本章小结
第十八章 字符串
6. 常规表达式
正则表达式是一种强大而灵活的文本处理工具。正则表达式提供了一种完全通用的方式,能够解决各种 String 处理相关的问题:匹配、选择、编辑以及验证。
6.1 基础
一般来说,正则表达式就是以某种方式来描述字符串,因此你可以说:“如果一个字符串含有这些东西,那么它就是我正在找的东西。
在正则表达式中,用 \d 表示一位数字。而在Java中, \ 的意思是“我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。”例如,如果你想表示一位数字,那么正则表达式应该是 \d 。如果你想插入一个普通的反斜线,应该这样写 \\ 。不过换行符和制表符之类的东西只需要使用单反斜线: \n\t 。
要表示“一个或多个之前的表达式”,应该使用 + 。所以,如果要表示“可能有一个负号,后面跟着一位或多位数字”,可以这样:
-?\\d+
应用正则表达式最简单的途径,就是利用 String 类内建的功能。
public class IntegerMatch {
public static void main(String[] args) {
// PS: -? 以0或1个 - 开始
System.out.println("-1234".matches("-?\\d+"));
System.out.println("5678".matches("-?\\d+"));
System.out.println("+911".matches("-?\\d+"));
// PS:竖线 | 表示或操作
System.out.println("+911".matches("(-|\\+)?\\d+"));
}
}
输出:
true
true
false
true
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(" "); // 按空格来划分字符串
System.out.println("------");
split("\\W+"); // 按 一个非单词字符 来划分字符串
System.out.println("------");
split("n\\W+"); // 字母 n 后面跟着一个或多个非单词字符
}
}
输出:
[Then,, when, you, have, found, the, shrubbery,, you, must, cut, down, the, mightiest, tree, in, the, forest...with..., a, herring!]
------
[Then, when, you, have, found, the, shrubbery, you, must, cut, down, the, mightiest, tree, in, the, forest, with, a, herring]
------
[The, whe, you have found the shrubbery, you must cut dow, the mightiest tree i, the forest...with... a herring!]
用正则表达式进行替换操作时,你可以只替换第一处匹配,也可以替换所有的匹配:
public class Replacing {
static String s = Splitting.knights;
public static void main(String[] args) {
// 以字母 f 开头,后面跟一个或多个字母(注意这里的 w 是小写的)。
// 并且只替换掉第一个匹配的部分,所以 “found” 被替换成 “located”。
// \\W 非字符, \\w 字符
System.out.println(s.replaceFirst("f\\w+", "located"));
// 要匹配的是三个单词中的任意一个
System.out.println(s.replaceAll("shrubbery|tree|herring", "banana"));
}
}
输出:
Then, when you have located the shrubbery, you must cut down the mightiest tree in the forest...with... a herring!
Then, when you have found the banana, you must cut down the mightiest banana in the forest...with... a banana!
6.2 创建正则表达式
表达式 | 含义 | |
---|---|---|
B | 指定字符 B | |
\xhh | 十六进制值为 0xhh 的字符 | |
\uhhhh | 十六进制表现为 0xhhhh 的Unicode字符 | |
\t | 制表符 Tab | |
\n | 换行符 | |
\r | 回车 | |
\f | 换页 | |
\e | 转义(Escape) | |
. | 任意字符 | |
[abc] | 包含 a 、 b 或 c 的任何字符 | |
[^abc] | 除 a 、 b 和 c 之外的任何字符 (否定) | |
[a-zA-Z] | 从 a 到 z 或从 A 到 Z 的任何字 符(范围) | |
[abc[hij]] | a 、 b 、 c 、 h 、 i 、 j 中的任意字符 | |
[a-z&& [hij]] | 任意 h 、 i 或 j (交) | |
\s | 空白符(空格、tab、换行、换页、 回车) | |
\S | 非空白符 | |
\d | 数字( [0-9] ) | |
\D | 非数字 | |
\w | 词字符( [a-zA-Z_0-9] ) | |
^ | 一行的开始 | |
$ | 一行的结束 | |
\b | 词的边界 | |
\B | 非词的边界 | |
\G | 前一个匹配的结束 |
public class Rudolph {
public static void main(String[] args) {
for (String pattern : new String[]{
"Rudolph", // 全匹配
"[rR]udolph", // r 或 R,接着是 udolph
"[rR][aeiou][a-z]ol.*", // r 或 R,接着是 aeiou 的任意一个,接着是 a-z 的任意一个,接着是 ol,接着是
"R.*" // .(任意字符) * 0个或多个
})
System.out.println("Rudolph".matches(pattern));
}
}
输出:
true
true
true
true
6.3 量词
量词描述了一个模式捕获输入文本的方式:
-
贪婪型: 量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。导致此问题的一个典型理由就是假定我们的模式仅能匹配第一个可能的字符组,如果它是贪婪的,那么它就会继续往下匹配。
-
勉强型: 用问号来指定,这个量词匹配满足模式所需的最少字符数。因此也被称作懒惰的、最少匹配的、非贪婪的或不贪婪的。
-
占有型: 目前,这种类型的量词只有在 Java 语言中才可用(在其他语言中不可用),并且也更高级,因此我们大概不会立刻用到它。当正则表达式被应用于 String 时,它会产生相当多的状态,以便在匹配失败时可以回溯。而“占有的”量词并不保存这些中间状态,因此它们可以防止回溯。它们常常用于防止正则表达式失控,因此可以使正则表达式执行起来更高效。
贪婪型 | 勉强型 | 占有型 | 如何匹配 |
---|---|---|---|
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+ :匹配 ab ,后面跟随1个或多个 c
(abc)+ :匹配1个或多个完整的字符串 abc
6.4 CharSequence
接口 CharSequence 从 CharBuffer 、 String 、 StringBuffer 、 StringBuilder 类中抽象出了字符序列的一般化定义:
interface CharSequence {
char charAt(int i);
int length();
CharSequence subSequence(int start, int end);
String toString();
}
因此,这些类都实现了该接口。多数正则表达式操作都接受 CharSequence 类型参数。
6.5 Pattern 和 Matcher
通常,比起功能有限的 String 类,我们更愿意构造功能强大的正则表达式对象。只需导入 java.util.regex 包,然后用 static Pattern.compile() 方法来编译你的正则表达式即可。它会 根据你的 String 类型的正则表达式生成一个 Pattern 对象。接下来,把你想要检索的字符串传入 Pattern 对象的 matcher() 方法。 matcher() 方法会生成一个 Matcher 对象,它有很多功能可用。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TestRegularExpression {
public static void main(String[] args) {
String s = "abc1293adfabc3230abc";
String regex = "(abc)+";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(s);
while (m.find()) {
System.out.println("Match \"" + m.group() + "\" at positions " +
m.start() + "-" + (m.end() - 1));
}
}
}
输出:
Match "abc" at positions 0-2
Match "abc" at positions 10-12
Match "abc" at positions 17-19
6.5.1 find()
Matcher.find() 方法可用来在 CharSequence 中查找多个匹配。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Finding {
public static void main(String[] args) {
Pattern p = Pattern.compile("\\w+"); // \\w+ 将字符串划分为词
Matcher m = p.matcher("Evening is full of the linnet's wings");
// PS: find() 方法像迭代器那样向前遍历输入字符串
while (m.find()) {
// PS:把找到的打印出来,不换行
System.out.print(m.group() + " ");
}
System.out.println("-------");
int i = 0;
// PS:重载的 find() 接收一个整型参数,该整数表示字符串中字符的位置,并以其作为搜索的起点。
while (m.find(i)) {
System.out.print(m.group() + " ");
i++;
}
}
}
输出:
Evening is full of the linnet s wings -------
Evening vening ening ning ing ng g is is s full full ull ll l of of f the the he e linnet linnet innet nnet net et t s s wings wings ings ngs gs s
6.5.2 组(Groups)
组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为 0 表示整个表达式,组号 1 表示被第一对括号括起来的组,以此类推。因此,下面这个表达式,
A(B(C))D
中有三个组:组 0 是 ABCD ,组 1 是 BC ,组 2 是 C 。
Matcher 对象提供了一系列方法,用以获取与组相关的信息:
- public int groupCount() 返回该匹配器的模式中的分组数目,组 0 不包括在内。
- public String group() 返回前一次匹配操作(例如 find() )的第 0 组(整个匹配)
- public String group(int i) 返回前一次匹配操作期间指定的组号,如果匹配成功,但是指定的组没有匹配输入字符串的任何部分,则将返回 null。
6.5.3 start() 和 end()
在匹配操作成功之后, start() 返回先前匹配的起始位置的索引,而 end() 返回所匹配的最后字符的索引加一的值。
6.5.4 Pattern 标记
Pattern 类的 compile() 方法还有另一个版本,它接受一个标记参数,以调整匹配行为:
Pattern Pattern.compile(String regex, int flag)
其中的 flag 来自以下 Pattern 类中的常量
编译标记 | 效果 |
---|---|
Pattern.CANON_EQ | 当且仅当两个字符的完全规范分解相匹配时,才认为它们是匹配的。例如,如果我们指定这个标记,表达式 \u003F 就会匹配字符串 ? 。默认情况下,匹配不考虑规范的等价性 |
Pattern.CASE_INSENSITIVE(?i) | 默认情况下,大小写不敏感的匹配假定只有US-ASCII字符集 中的字符才能进行。这个标记允许模式匹配不考虑大小写 (大写或小写)。通过指定 UNICODE_CASE 标记及结合此标 记。基于Unicode的大小写不敏感的匹配就可以开启了 |
Pattern.COMMENTS(?x) | 在这种模式下,空格符将被忽略掉,并且以 # 开始直到行末 的注释也会被忽略掉。通过嵌入的标记表达式也可以开启 Unix的行模式 |
Pattern.DOTALL(?s) | 在dotall模式下,表达式 . 匹配所有字符,包括行终止符。默 认情况下, . 不会匹配行终止符 |
Pattern.MULTILINE(?m) | 在多行模式下,表达式 ^ 和 $ 分别匹配一行的开始和结 束。 ^ 还匹配输入字符串的开始,而 $ 还匹配输入字符串 的结尾。默认情况下,这些表达式仅匹配输入的完整字符串 的开始和结束 |
Pattern.UNICODE_CASE(?u) | 当指定这个标记,并且开启 CASE_INSENSITIVE 时,大小写 不敏感的匹配将按照与Unicode标准相一致的方式进行。默认 情况下,大小写不敏感的匹配假定只能在US-ASCII字符集中 的字符才能进行 |
Pattern.UNIX_LINES(?d) | 在这种模式下,在 . 、 ^ 和 $ 的行为中,只识别行终止 符 \n |
在这些标记中, Pattern.CASE_INSENSITIVE 、 Pattern.MULTILINE 以及 Pattern.COMMENTS (对声明或文档有用)特别有用。可以通过“或”( | )操作符组合多个标记的功能:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ReFlags {
public static void main(String[] args) {
// 将匹配所有以“java”、“Java”和“JAVA”等开头的行,并且是在设置了多行标记的状态下,
// 对每一行(从字符序列的第一个字符开始,至每一个行终止符)都进行匹配。
Pattern p = Pattern.compile("^java",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher(
"java has regex\nJava has regex\n" +
"JAVA has pretty good regular expressions\n" +
"Regular expressions are in Java");
while (m.find()) {
System.out.println(m.group());
}
}
}
输出:
java
Java
JAVA
6.5.4 split()
split() 方法将输入 String 断开成 String 对象数组,断开边界由正则表达式确定:
String[] split(CharSequence input)
String[] split(CharSequence input, int limit)
import java.util.Arrays;
import java.util.regex.Pattern;
public class SplitDemo {
public static void main(String[] args) {
String input = "This!!unusual use!!of exclamation!!points";
System.out.println(Arrays.toString(input.split("!!")));
System.out.println(Arrays.toString(
Pattern.compile("!!").split(input)
));
System.out.println(Arrays.toString(
// 第二种形式的 split() 方法可以限制将输入分割成字符串的数量
// PS:就是将字符串切割为 3 段
Pattern.compile("!!").split(input, 3)
));
}
}
输出:
[This, unusual use, of exclamation, points]
[This, unusual use, of exclamation, points]
[This, unusual use, of exclamation!!points]
—PS:从输出结果看 Pattern.compile(“!!”).split(str) 等于 str.split(“!!”)
6.5.5 替换操作
正则表达式在进行文本替换时特别方便,它提供了许多方法:
-
replaceFirst(String replacement) 以参数字符串 replacement 替换掉第一个匹配成功的部分。
-
replaceAll(String replacement) 以参数字符串 replacement 替换所有匹配成功的部分。
-
appendReplacement(StringBuffer sbuf, String replacement) 执行渐进式的替换,而不是像 replaceFirst() 和 replaceAll() 那样只替换第一个匹配或全部匹配。
-
appendTail(StringBuffer sbuf) 在执行了一次或多次 appendReplacement() 之后,调用此方法可以将输入字符串余下的部分复制到 sbuf 中。
—PS:前两个方法效果与 str.replaceFirst/replaceAll 一样
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TheReplacements {
public static void main(String[] args) {
String s = "Here's a block of text to use as input to the regular expression matcher.";
StringBuffer sbuf = new StringBuffer();
Pattern p = Pattern.compile("[aeiou]");
Matcher m = p.matcher(s);
while (m.find()) {
m.appendReplacement(sbuf, m.group().toUpperCase());
}
m.appendTail(sbuf);
System.out.println(sbuf);
}
}
输出:
HErE's A blOck Of tExt tO UsE As InpUt tO thE rEgUlAr ExprEssIOn mAtchEr.
6.5.6 reset()
通过 reset() 方法,可以将现有的 Matcher 对象应用于一个新的字符序列:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Resetting {
public static void main(String[] args) {
Matcher m = Pattern.compile("[frb][aiu][gx]")
.matcher("fix the rug with bags");
while (m.find()) {
System.out.println(m.group()+" ");
}
System.out.println("------");
// PS:上面已经匹配过了,再次匹配什么都没有
while (m.find()) {
System.out.println(m.group()+" ");
}
System.out.println("------");
// PS:对象重新设置到当前字符序列的起始位置
m.reset();
while (m.find()) {
System.out.println(m.group()+" ");
}
System.out.println("------");
// PS:将现有的 Matcher 对象应用于一个新的字符序列
m.reset("fix with bags");
while (m.find()) {
System.out.println(m.group()+" ");
}
}
}
输出:
fix
rug
bag
------
------
fix
rug
bag
------
fix
bag
7. 扫描输入
到目前为止,从文件或标准输入读取数据还是一件相当痛苦的事情。一般的解决办法就是读入一行文本,对其进行分词,然后使用 Integer 、 Double 等类的各种解析方法来解析数据:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
public class SimpleRead {
public static BufferedReader input =
new BufferedReader(new StringReader(
"Sir Robin of Camelot\n22 1.61803"
));
public static void main(String[] args) {
try {
System.out.println("What is your name?");
String name = input.readLine();
System.out.println(name);
System.out.println("How old are you? " + "What is your favorite double?");
System.out.println("(input: <age> <double>)");
String numbers = input.readLine();
String[] numArray = numbers.split(" ");
int age = Integer.parseInt(numArray[0]);
double favorite = Double.parseDouble(numArray[1]);
System.out.format("Hi %s.%n", name);
System.out.format("In 5 years you will be %d.%n", age + 5);
System.out.format("My favorite double is %f.", favorite / 2);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出:
What is your name?
Sir Robin of Camelot
How old are you? What is your favorite double?
(input: <age> <double>)
Hi Sir Robin of Camelot.
In 5 years you will be 27.
My favorite double is 0.809015.
Java SE5 新增了 Scanner 类,它可以大大减轻扫描输入的工作负担:
import java.util.Scanner;
public class BetterRead {
public static void main(String[] args) {
Scanner stdin = new Scanner(SimpleRead.input);
System.out.println("What is your name?");
String name = stdin.nextLine();
System.out.println(name);
System.out.println("How old are you? What is your favorite double?");
System.out.println("(input: <age> <double>)");
int age = stdin.nextInt();
double favorite = stdin.nextDouble();
System.out.println(age);
System.out.println(favorite);
System.out.format("Hi %s.%n", name);
System.out.format("In 5 years you will be %d.%n", age + 5);
System.out.format("My favorite double is %f.", favorite / 2);
}
}
输出:
What is your name?
Sir Robin of Camelot
How old are you? What is your favorite double?
(input: <age> <double>)
22
1.61803
Hi Sir Robin of Camelot.
In 5 years you will be 27.
My favorite double is 0.809015.
7.1 Scanner 分隔符
默认情况下, Scanner 根据空白字符对输入进行分词,但是你可以用正则表达式指定自己所需的分隔符:
import java.util.Scanner;
public class ScannerDelimiter {
public static void main(String[] args) {
Scanner scanner = new Scanner("12, 42, 78, 99, 42");
scanner.useDelimiter("\\s*,\\s*");
while (scanner.hasNextInt()) System.out.println(scanner.nextInt());
}
}
输出:
12
42
78
99
42
7.2 用正则表达式扫描
除了能够扫描基本类型之外,你还可以使用自定义的正则表达式进行扫描,这在扫描复杂数据时非常有用。
import java.util.Scanner;
import java.util.regex.MatchResult;
public class ThreatAnalyzer {
static String threatData = "58.27.82.161@08/10/2015\n" +
"204.45.234.40@08/11/2015\n" +
"58.27.82.161@08/11/2015\n" +
"58.27.82.161@08/12/2015\n" +
"58.27.82.161@08/12/2015\n" +
"[Next log section with different data format]";
public static void main(String[] args) {
Scanner scanner = new Scanner(threatData);
String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@" + "(\\d{2}/\\d{2}/\\d{4})";
while (scanner.hasNext(pattern)) {
scanner.next(pattern);
MatchResult match = scanner.match();
String ip = match.group(1); // 表达式一共2个括号,共3组,总的为下标 0
String date = match.group(2);
System.out.format("Threat on %s from %s%n", date, ip);
}
}
}
输出:
Threat on 08/10/2015 from 58.27.82.161
Threat on 08/11/2015 from 204.45.234.40
Threat on 08/11/2015 from 58.27.82.161
Threat on 08/12/2015 from 58.27.82.161
Threat on 08/12/2015 from 58.27.82.161
8. StringTokenizer 类
在 Java 引入正则表达式(J2SE1.4)和 Scanner 类(Java SE5)之前,分割字符串的唯一方法是使用 StringTokenizer 来分词。
import java.util.Arrays;
import java.util.Scanner;
import java.util.StringTokenizer;
public class ReplacingStringTokenizer {
public static void main(String[] args) {
String input = "But I'm not dead yet! I feel happy!";
StringTokenizer stoke = new StringTokenizer(input);
while (stoke.hasMoreTokens()) {
System.out.print(stoke.nextToken() + " ");
}
System.out.println();
System.out.println(Arrays.toString(input.split(" ")));
Scanner scanner = new Scanner(input);
while (scanner.hasNext()) {
System.out.print(scanner.next() + " ");
}
}
}
输出:
But I'm not dead yet! I feel happy!
[But, I'm, not, dead, yet!, I, feel, happy!]
But I'm not dead yet! I feel happy!
—PS: StringTokenizer 已经可以废弃不用了
9. 本章小结
过去,Java 对于字符串操作的技术相当不完善。不过随着近几个版本的升级,我们可以看到,Java 已经从其他语言中吸取了许多成熟的经验。到目前为止,它对字符串操作的支持已经很完善了。
自我学习总结:
- 正则表达式是一种文本处理工具,可以解决各种字符串的问题:匹配、选择、编辑以及验证。
- 推荐一个大佬的文章:常用正则表达式合集,这一篇就够了!!(含完整案例,建议收藏)
- Pattern 和 Matcher 类,正则表达式对象。
- Pattern.compile(regex).matcher() 返回 Matcher 对象,它有很多功能可用。
- Matcher 主要方法有:find() group() start() end() split() replaceXX() reset()
- 扫描输入相关类 Scanner 类,可以配合正则表达式
- 关键词 正则表达式、 Pattern 、 Match 、 Scanner
(图网,侵删)