前言
不久前参加一个写代码的挑战赛,最后一题中考到了正则表达式,当时没解决,所以花了点时间学习了一下正则表达式在java中是如何使用
一、正则表达式是什么
正则表达式(英语:Regular Expression,常简写为regex、regexp或RE),又称正则表示式、正则表示法、规则表达式、常规表示法,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些匹配某个模式的文本。 --维基百科
给定一个正则表达式和另一个字符串,我们可以达到如下的目的:
- 给定的字符串是否符合正则表达式的过滤逻辑(称作“匹配”)
- 可以通过正则表达式,从字符串中获取我们想要的特定部分。
二、Regular Expressions API规范
1.官方定义
Java语言提供了四个类来支持正则表达式
Pattern 类的实例表示以字符串形式指定的正则表达式
Matcher 类的实例用于将字符序列与给定的模式进行匹配
MatchResult 表示匹配操作的结果
PatternSyntaxException表示正则表达式语法错误
2.Pattern
Pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。
这个类所支持的匹配字符参考doc
public static Pattern compile(String regex);
将给定的正则表达式编译成一个Pattern类实例
public static Pattern compile(String regex, int flags);
是上一个方法的重载,多了个flag参数
在Pattern内定义了Flag
flag | 描述 |
---|---|
UNIX_LINES | 启用 Unix 换行模式 |
CASE_INSENSITIVE | 启用不区分大小写的匹配 |
COMMENTS | 允许模式中的空格和注释 |
MULTILINE | 启用多行模式 |
LITERAL | 启用模式的文字解析 |
DOTALL | 启用 dotall 模式 |
UNICODE_CASE | 启用 Unicode 感知大小写折叠 |
CANON_EQ | 启用规范等价 |
UNICODE_CHARACTER_CLASS | 启用 Unicode 版本的预定义字符类和 POSIX 字符类 |
public int flags();
返回创建Pattern对象的flags参数
public Matcher matcher(CharSequence input);
创建一个匹配器,将给定的输入与此匹配
public static boolean matches(String regex, CharSequence input)
编译给定的正则表达式并尝试将给定的输入与其匹配,此方法主要作为一次性使用
public String pattern();
返回创建该对象时传入的正则表达式
public static String quote(String s);
把传入的正则表达式转换为它的字面量
public String[] split(CharSequence input)
将输入的字符序列分割
public String[] split(CharSequence input, int limit)
将输入的字符序列分割,返回的数组最多有limit项
public Stream<String> splitAsStream(final CharSequence input)
与split方法作用相同,返回值为流
3.Matcher
一个引擎,通过解释Pattern执行匹配一个操作字符序列。
3.1索引方法
索引方法提供了有用的索引值,精确表明输入字符串中在哪能找到匹配:
方法 | 描述 |
---|---|
public int start() | 返回以前匹配的初始索引 |
public int start(int group) | 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引 |
public int end() | 返回最后匹配字符之后的偏移量 |
public int end(int group) | 返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量 |
3.2查找方法
查找方法用来检查输入字符串并返回一个布尔值,表示是否找到该模式:
方法 | 描述 |
---|---|
public boolean lookingAt() | 尝试将从区域开头开始的输入序列与该模式匹配 |
public boolean find() | 尝试查找与该模式匹配的输入序列的下一个子序列 |
public boolean find(int start) | 重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列 |
public boolean matches() | 尝试将整个区域与模式匹配 |
3.3 替换方法
替换方法是替换输入字符串里文本的方法
方法 | 描述 |
---|---|
public Matcher appendReplacement(StringBuffer sb, String replacement) | 实现非终端添加和替换步骤 |
public StringBuffer appendTail(StringBuffer sb) | 实现终端添加和替换步骤 |
public String replaceAll(String replacement) | 替换模式与给定替换字符串相匹配的输入序列的每个子序列 |
public String replaceFirst(String replacement) | 替换模式与给定替换字符串匹配的输入序列的第一个子序列 |
public static String quoteReplacement(String s) | 返回指定字符串的字面替换字符串。这个方法返回一个字符串,就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作 |
3.4 其他方法
方法 | 描述 |
---|---|
public String group() | 返回与前一个匹配项匹配的输入子序列 |
public String group(int group) | 返回给定组在上一次匹配操作期间捕获的输入子序列 |
public String group(String name) | 返回在前一个匹配操作期间由给定命名捕获组捕获的输入子序列 |
public int groupCount() | 返回此匹配器模式中的捕获组数 |
public boolean hasAnchoringBounds() | 返回是否使用锚定边界 |
public boolean hasTransparentBounds() | 返回匹配器是否使用透明边界 |
public boolean hitEnd() | 如果在此匹配器执行的最后一次匹配操作中搜索引擎命中了输入的结尾,则返回 true |
public Matcher region(int start, int end) | 设置此匹配器区域的限制。 |
public int regionStart() | 返回此匹配器区域的起始索引 |
public int regionEnd() | 返回此匹配器区域的结束索引 |
public Matcher reset() | 重置此匹配器 |
public Matcher reset(CharSequence input) | 使用新的输入序列重置此匹配器 |
public Matcher useAnchoringBounds(boolean b) | 设置此匹配器的区域边界的锚定 |
public Matcher useTransparentBounds(boolean b) | 设置此匹配器的区域边界的透明度 |
public Matcher usePattern(Pattern newPattern) | 更改此匹配器用于查找匹配项的模式 |
public MatchResult toMatchResult() | 将此匹配器的匹配状态作为 返回MatchResult |
public boolean requireEnd() | 返回更多输入是否可以将正匹配更改为负匹配 |
三、使用示例
没有耐心的基本直接看这个
1. 常用
1.1 判断是否输入字符串是否满足正则表达
Pattern pattern = Pattern.compile("[a-zA-Z]{3}");
Matcher matcher = pattern.matcher("PEK");
System.out.println(matcher.matches()); // true
或者直接
System.out.println(Pattern.matches("[a-zA-Z]{3}","PEK")); // true
查看Pattern::matches方法源码,发现其实也就是帮你做了new对象的活
1.2 从字符串中提取字符串
先给个文本
<h1>Java 正则表达式</h1>
<p>正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。</p>
<font>正则表达式定义了字符串的模式。</font>
<p>正则表达式可以用来搜索、编辑或处理文本。</p>
我们要做的就是提取P标签里的内容
String str = "<h1>Java 正则表达式</h1>\n" +
"<p>正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。</p>\n" +
"<font>正则表达式定义了字符串的模式。</font>\n" +
"<p>正则表达式可以用来搜索、编辑或处理文本。</p>";
Pattern pattern = Pattern.compile("(?<=<p>).*?(?=</p>)");
Matcher matcher = pattern.matcher(str);
while (matcher.find()){
System.out.println(matcher.group());
}
输出
正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。
正则表达式可以用来搜索、编辑或处理文本。
上面的正则如果看不懂去看Pattern类的文档 。 传送门
1.3 替换或者编辑字符串
还是上面的字符串,这次我想把所有的html标签去掉
很多人会写出下面的代码,我也会这样写
String str = "<h1>Java 正则表达式</h1>\n" +
"<p>正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。</p>\n" +
"<font>正则表达式定义了字符串的模式。</font>\n" +
"<p>正则表达式可以用来搜索、编辑或处理文本。</p>";
String s = str.replaceAll("<.*?>", "");
System.out.println(s);
输出
Java 正则表达式
正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。
正则表达式定义了字符串的模式。
正则表达式可以用来搜索、编辑或处理文本。
实际上String.replaceAll最终还是调用了Matcher的replaceAll方法
public String replaceAll(String replacement) {
reset();
boolean result = find();
if (result) {
StringBuffer sb = new StringBuffer();
do {
appendReplacement(sb, replacement);
result = find();
} while (result);
appendTail(sb);
return sb.toString();
}
return text.toString();
}
参考replaceAll的代码,我们可以这样写
String str = "<h1>Java 正则表达式</h1>\n" +
"<p>正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。</p>\n" +
"<font>正则表达式定义了字符串的模式。</font>\n" +
"<p>正则表达式可以用来搜索、编辑或处理文本。</p>";
Pattern pattern = Pattern.compile("<.*?>");
Matcher matcher = pattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()){
matcher.appendReplacement(sb,"");
}
matcher.appendTail(sb); // 千万别漏了
System.out.println(sb);
咋一看,我不是脑瘫吗,一个字符串替换写这么多。。。怎么说呢
这样比较灵活,简单字符串替换肯定选第一种,如果你只需要替换前2对标签或者需要记录每次替换掉了哪个标签,再或者每次替换的内容不同都可以试试第二种,多了解一些肯定是不吃亏的。
2. 不常用
2.1 Pattern.flag
2.1.1 MULTILINE
启用多行模式
在多行模式下,表达式^和$ 分别匹配行终止符或输入序列的结尾之后或之前。 默认情况下,这些表达式仅匹配整个输入序列的开头和结尾
不启用多行模式
Pattern pattern = Pattern.compile("^app$");
Matcher matcher = pattern.matcher("app\napp");
while (matcher.find()) {
System.out.println(matcher.group());
}
无输出
启用多行模式
Pattern pattern = Pattern.compile("^app$",Pattern.MULTILINE);
Matcher matcher = pattern.matcher("app\napp");
while (matcher.find()) {
System.out.println(matcher.group());
}
输出
app
app
2.1.2 UNIX_LINES
启用 Unix 行模式。
默认情况下Pattern把以下下几种看做行分隔符
- 换行(换行)字符( ‘\n’ )
- 一个回车符紧跟一个换行符( “\r\n” )
- 一个独立的回车符( ‘\r’ )
- 下一行字符(’\u0085’ )
- 行分隔符( ‘\u2028’ )
- 段落分隔符 ( '\u2029 )
开启UNIX_LINE模式后仅把’\n’看做行分隔符
不启用 Unix行模式
Pattern pattern = Pattern.compile("^app$",Pattern.MULTILINE);
Matcher matcher = pattern.matcher("app\rapp");
while (matcher.find()) {
System.out.println(matcher.group());
}
输出
app
app
启用 Unix行模式
Pattern pattern = Pattern.compile("^app$",Pattern.MULTILINE|Pattern.UNIX_LINES);
Matcher matcher = pattern.matcher("app\rapp");
while (matcher.find()) {
System.out.println(matcher.group());
}
什么都没有输出
注意:当启用Unix行模式后,点匹配除了\n 外所有字符,包括\r
2.1.3 CASE_INSENSITIVE 及UNICODE_CASE
ascii大小写不敏感,当和UNICODE_CASE一起指定时可以启用 Unicode大小写不敏感。
ascii大小写不敏感的例子就不写了,写个UNICODE_CASE大小写不敏感的例子吧
Pattern pattern = Pattern.compile("\u00de",Pattern.CASE_INSENSITIVE|Pattern.UNICODE_CASE);
Matcher matcher = pattern.matcher("\u00de\u00fe");
while (matcher.find()) {
System.out.println(matcher.group());
}
输出
Þ
þ
þ : 拉丁文小写字母 Thorn
Þ : 拉丁文大写字母 Thorn
2.1.4 DOTALL
启用 dotall 模式。开启后点匹配所有字符,不开启此模式点不匹配行分隔符号
Pattern pattern = Pattern.compile(".");
Matcher matcher = pattern.matcher("\na\r");
while (matcher.find()){
String group = matcher.group();
System.out.println((int) group.charAt(0));
}
输出
97
Pattern pattern = Pattern.compile(".",Pattern.DOTALL);
Matcher matcher = pattern.matcher("\na\r");
while (matcher.find()){
String group = matcher.group();
System.out.println((int) group.charAt(0));
}
输出
10
97
13
2.1.5 COMMENTS
允许在模式中使用空格和注释。
Pattern pattern = Pattern.compile(" c#dasd",Pattern.COMMENTS);
Matcher matcher = pattern.matcher("bcd");
while (matcher.find()){
String group = matcher.group();
System.out.println(group);
}
输出
c
2.1.6 CANON_EQ
不懂这是什么玩意(unicode)
2.1.7 LITERAL
指定此标志后,指定模式的输入字符串将被视为文字字符序列。 输入序列中的元字符或转义序列将没有特殊含义。
当与此标志结合使用时,标志 CASE_INSENSITIVE 和 UNICODE_CASE 保留其对匹配的影响。 其他标志变得多余
String input = "This is the first line\n"
+ "This is the second line\n"
+ "^This is the third line";
String regex = "^This";
Pattern pattern = Pattern.compile(regex,Pattern.LITERAL);
Matcher matcher = pattern.matcher(input);
int count = 0;
while(matcher.find()) {
count++;
System.out.println(matcher.group());
}
System.out.println("Number of matches: "+count);
输出
^This
Number of matches: 1
2.1.8 UNICODE_CHARACTER_CLASS
不太懂
2.2 Pattern.split
split方法的功能是将字符串分割,大部分情况下我们需要分割字符串的时候使用String的split方法就够了,所以不怎么用
Pattern pattern = Pattern.compile(":");
System.out.println(Arrays.toString(pattern.split("a:b:c:d:e:f")));
System.out.println(Arrays.toString(pattern.split("a:b:c:d:e:f",3))); // String类也提供了limit参数
2.3 Matcher.group相关
java正则表达支持捕获组
捕获组分为:
- 普通捕获组(Expression)
- 命名捕获组(?Expression)
2.3.1 普通捕获组
从正则表达式左侧开始,每出现一个左括号"("记做一个分组,分组编号从 1 开始。0 代表整个表达式。
对于时间字符串:2017-04-25,表达式如下
(\\d{4})-((\\d{2})-(\\d{2}))
有 4 个左括号,所以有 4 个分组:
编号 | 捕获组 | 匹配 |
---|---|---|
0 | (\d{4})-((\d{2})-(\d{2})) | 2017-04-25 |
1 | (\d{4}) | 2017 |
2 | ((\d{2})-(\d{2})) | 04-25 |
3 | (\d{2}) | 04 |
4 | (\d{2}) | 25 |
Pattern pattern = Pattern.compile("(\\d{4})-((\\d{2})-(\\d{2}))");
Matcher matcher = pattern.matcher("2021-12-16");
while (matcher.find()){
System.out.println("group 0: "+ matcher.group(0)+"\t\t\t" + "start: "+ matcher.start(0) +"\t end: " + matcher.end(0));
System.out.println("group 1: "+ matcher.group(1)+"\t\t\t" + "start: "+ matcher.start(0) +"\t end: " + matcher.end(0));
System.out.println("group 2: "+ matcher.group(2)+"\t\t\t" + "start: "+ matcher.start(0) +"\t end: " + matcher.end(0));
System.out.println("group 3: "+ matcher.group(3)+"\t\t\t" + "start: "+ matcher.start(0) +"\t end: " + matcher.end(0));
System.out.println("group 4: "+ matcher.group(4)+"\t\t\t" + "start: "+ matcher.start(0) +"\t end: " + matcher.end(0));
}
输出
group 0: 2021-12-16 start: 0 end: 10
group 1: 2021 start: 0 end: 10
group 2: 12-16 start: 0 end: 10
group 3: 12 start: 0 end: 10
group 4: 16 start: 0 end: 10
上面的很多示例代码调用group方法并没有穿group索引,查看源码就可以知道默认是第0组,start和end方法也一样
2.3.2 命名捕获组
每个以左括号开始的捕获组,都紧跟着 ?,而后才是正则表达式。
对于时间字符串:2017-04-25,表达式如下:
(?<year>\\d{4})-(?<md>(?<month>\\d{2})-(?<date>\\d{2}))
编号 | 名称 | 捕获组 | 匹配 |
---|---|---|---|
0 | (\d{4})-((\d{2})-(\d{2})) | 2017-04-25 | |
1 | year | (\d{4}) | 2017 |
2 | md | ((\d{2})-(\d{2})) | 04-25 |
3 | month | (\d{2}) | 04 |
4 | date | (\d{2}) | 25 |
示例
Pattern pattern = Pattern.compile("(?<year>\\d{4})-(?<md>(?<month>\\d{2})-(?<date>\\d{2}))");
Matcher matcher = pattern.matcher("2021-12-16");
while (matcher.find()){
System.out.println("group year: "+ matcher.group("year")+"\t\t\t" + "start: "+ matcher.start("year") +"\t end" + matcher.end("year"));
System.out.println("group md: "+ matcher.group("md")+"\t\t\t" + "start: "+ matcher.start("md") +"\t end:" + matcher.end("md"));
System.out.println("group month: "+ matcher.group("month")+"\t\t\t" + "start: "+ matcher.start("month") +"\t end" + matcher.end("month"));
System.out.println("group date: "+ matcher.group("date")+"\t\t\t" + "start: "+ matcher.start("date") +"\t end" + matcher.end("date"));
}
输出
group year: 2021 start: 0 end4
group md: 12-16 start: 5 end:10
group month: 12 start: 5 end7
group date: 16 start: 8 end10
组相关的知识来自菜鸟教程,传送门
2.4 Matcher.region相关
region方法设置匹配的区域
示例
Pattern pattern = Pattern.compile("app");
Matcher matcher = pattern.matcher("app&apple");
while (matcher.find()){
System.out.println(matcher.group());
}
matcher.reset(); // 重置此匹配器
System.out.println("-----------------");
matcher.region(3,8);
while (matcher.find()){
System.out.println(matcher.group());
}
输出
app
app
-----------------
app
设匹配区域后就只能匹配到一个app了,设置区域会带来另一个问题,\b等需要根据边界来匹配的怎么办
懒得写了 看这个吧
总结
本文并没有仔细介绍正则怎么写,老实说,我也记不全正则的语法,我向来都是要用的时候再打开文档,这种东西没必要特地去记,留个印象就行。一步步写完了这个博客,现在我也感说熟悉java的正则表达式API了,源码就以后有空再读一读。(通常说这句话以后就没有再看过了。。。)