0%

java正则表达式讲解

正则表达式基本简介

正则表达式(Regular Expression)是一种文本模式,包含数字,字母和以及特殊字符(元字符)等。从语法上来看,它虽然繁琐难以理解,但是在描述和处理文本方面它显得格外强大。很多编程语言都支持正则,包括但不限于C++、C#、Golang、java、python、js等等,而不同的语言其正则的语法亦略微有些差异。

正则表达式的的底层引擎实现有两种:DFA 自动机(Deterministic Final Automata 确定型有穷自动机)和 NFA 自动机(Non deterministic Finite Automaton 不确定型有穷自动机)。DFA自动机的时间复杂度是线性的,更加稳定,但是功能有限。而NFA的时间复杂度比较不稳定,有时候很好,有时候不怎么好,好不好取决于你写的正则表达式(可参考一个由正则表达式引发的血案)。

下面将简单介绍一下java(8)- NFA 正则表达式的使用。

Java正则介绍

Java正则方面主要包含三个类:Pattern、Matcher、PatternSyntaxException,在java.util.regex包下。

Pattern类

在Pattern注释中是这样描述这个类的:A compiled representation of a regular expression(正则表达式的编译表示)。java中,一个正则表达式(字符串)必须编译成该类的实例,才能去被Matcher使用,一个pattern实例是可以被多个matcher进行使用的。

compile

Pattern类不提供公共的构造函数,其只对外暴露了两个静态重载方法来编译构建pattern实例:

1
2
3
4
5
6
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
public static Pattern compile(String regex, int flags) {
return new Pattern(regex, flags);
}

由代码可见,Pattern的私有构造函数会有一个flags的参数,第一个函数默认给的是0,第二个是让使用者自己控制,那么flags这个参数有什么用呢?这个参数可以被赋予哪儿些值呢?

flags是匹配标志,一个位掩码,其可取值如下:

表达式 embedded flag 描述
Pattern.UNIX_LINES 0x01 (?d) 此模式下只有’\n’换行符被认可,且同’.’,’^’,’$’一样进行匹配
Pattern.CASE_INSENSITIVE 0x02 (?i) 默认情况下,大小写不敏感只针对US-ASCII字符,此模式在搭配UNICODE_CASE时可使得Unicode字符对大小写不敏感
Pattern.COMMENTS 0x04 (?x) 在此模式下,空格会被忽略,且以‘#’开头的行注释会被忽略
Pattern.MULTILINE 0x08 (?m) 此模式下,’^’和’$’分别匹配一行的开始和结束,此外,’^’仍然匹配字符串的开始,’$’也匹配字符串的结束。默认情况下,这两个表达式仅仅匹配字符串的开始和结束
Pattern.LITERAL 0x10 此模式下输入的字符串被认为是文字字符,其中的元字符和转移字符将失去其特殊意义。除了标志CASE_INSENSITIVE和UNICODE_CASE与此标志一起使用时保留其影响,其他标志会变得多余
Pattern.DOTALL 0x20 (?s) 此模式下’.’符号可以匹配任意字符,包含行结束符(默认情况不包含)
Pattern.UNICODE_CASE 0x40 (?u) 使用此模式,当同时使用CASE_INSENSITIVE模式时,匹配是对大小写不敏感
Pattern.CANON_EQ 0x80 启动规范等价,此模式下,如果且仅当它们的完整规范分解匹配时,将认为两个字符匹配
Pattern.UNICODE_CHARACTER_CLASS 0x100 (?U) 启用Unicode版本的预定义字符类和POSIX字符类

下面给出flags的一种使用情况,针对三种情况匹配正则忽略大小写问题:

  1. flags = 0(默认), 预期结果不不忽略;
  2. flags = Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE,预期结果忽略大小写;
  3. 正则加入(?i)且flags=0(默认),预期结果忽略大小写;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class RegexDemo {

public static void main(String[] args) {
System.out.println("====case one====");
String regex = "Hello";
String text = "Hello, word! hello!";

Pattern pattern1 = Pattern.compile(regex);
Matcher matcher1 = pattern1.matcher(text);
while (matcher1.find()) {
System.out.println("match1: " + matcher1.group());
}
System.out.println("====case two====");
Pattern pattern2 = Pattern.compile(regex, Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE);
Matcher matcher2 = pattern2.matcher(text);
while (matcher2.find()) {
System.out.println("match2: " + matcher2.group());
}
System.out.println("====case three====");
String regex3 = "(?i)Hello";
Pattern pattern3 = Pattern.compile(regex3);
Matcher matcher3 = pattern3.matcher(text);
while (matcher3.find()) {
System.out.println("match3: " + matcher3.group());
}
}
}

输出结果如下,可以看到全部达到预期结果:

1
2
3
4
5
6
7
8
====case one====
match1: Hello
====case two====
match2: Hello
match2: hello
====case three====
match3: Hello
match3: hello

matches

1
public static boolean matches(String regex, CharSequence input) ;

matches是Pattern类提供的静态公共方法,用于判断正则与输入的文本是否匹配.

quote

1
public static String quote(String s);

quote方法是Pattern提供的静态公共方法,其说明为“Metacharacters or escape sequences in the input sequence will be given no special meaning”,意义是输入序列中的元字符或转义序列将没有特殊含义,也就是说用此方法得到的一个字符串,其内部的 ‘.’ , ‘*’ 等特殊符号和’\n’等转义符号将变成普通的文本。

私有方法

下面是Pattern类下的私有方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 创建Matcher对象
public Matcher matcher(CharSequence input)

// 获取当前正则的flags标志
public int flags();

// 返回正则表达式
public String pattern();

// 根据正则匹配的结果来分割输出的字符串数组
// example:regex = "(?i)A|a"; text = "小a你好,我是大A。"
// 则结果为 {"小", "你好,我是大","。"}
// limit是控制匹配的次数,若limit = n,则最多匹配n-1次,limit=0表示全匹配。
// example:limit = 2,能匹配1次
// 则结果为 {"小","你好,我是大A。"}
public String[] split(CharSequence input, int limit) ;

// 同上,默认limit=0
public String[] split(CharSequence input);

// 根据正则匹配结果分割输出流式的数组(全匹配),从而可进行流式操作
public Stream<String> splitAsStream(final CharSequence input);

// 创建可用于匹配字符串的谓词
public Predicate<String> asPredicate();

这里来说明一下split方法,split方法时根据正则匹配的结果来分割输出的字符串数组。参数limit是控制匹配的次数,若limit = n,则最多匹配n-1次,limit=0表示全匹配。 下面用2中情况来说明:

  1. limit=0(默认),预期结果为 {“小”, “你好,我是大”,”。”}
  2. limit=2,预期结果为 {“小”,”你好,我是大A。”}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class RegexDemo {

public static void main(String[] args) {
String regex = "(?i)A|a";
String text = "小a你好,我是大A。";
Pattern pattern = Pattern.compile(regex);
String[] result1 = pattern.split(text);
System.out.println("====limit 0====");
Stream.of(result1).forEach(System.out::println);
System.out.println("====limit 2====");
String[] result2 = pattern.split(text, 2);
Stream.of(result2).forEach(System.out::println);
}

输出结果如下:

1
2
3
4
5
6
7
====limit 0====

你好,我是大

====limit 2====

你好,我是大A。

Matcher类

Matcher类是对输入字符串进行解释和匹配操作的引擎。其没有公共构造方法。是通过Pattern 对象的matcher方法来创建Matcher对象的。

quoteReplacement

quoteReplacement是Matcher类提供的仅有的一个静态公共方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static String quoteReplacement(String s) {
if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1))
return s;
StringBuilder sb = new StringBuilder();
for (int i=0; i<s.length(); i++) {
char c = s.charAt(i);
if (c == '\\' || c == '$') {
sb.append('\\');
}
sb.append(c);
}
return sb.toString();
}

其作用是处理字符串文本中的‘$’和‘\’两个字符,将其从原本的特殊意义字符转义成普通字符。

校验是否匹配

Matcher类的校验API如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 尝试查找与该模式匹配的输入序列的下一个子序列。
public boolean find();

// 重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。
public boolean find(int start);

// 匹配全文本
// example:
// regex = "regex"; text = "regex"; result = true
// regex = "regex"; text = "regex, matches"; result = false
public boolean matches();

// 从文本的最开始位置进行匹配,判断是否与该模式匹配。
// example:
// regex = "regex"; text = "matches, regex"; result = false
// regex = "regex"; text = "regex, matches"; result = true
public boolean lookingAt()

下面通过代码来说明一下matchers和lookingAt方法的使用,首先是matches方法:

  1. regex = “regex”; text = “regex” 预期结果true
  2. regex = “regex”; text = “regex, matches”; 预期结果false
1
2
3
4
5
6
7
8
9
10
11
12
public class RegexDemo {

public static void main(String[] args) {
String regex = "regex";
String text1 = "regex";
String text2 = "regex, matches";
System.out.println("====case one====");
System.out.println(Pattern.matches(regex, text1));
System.out.println("====case two====");
System.out.println(Pattern.matches(regex, text2));
}
}

输出结果如下:

1
2
3
4
====case one====
true
====case two====
false

其次是lookingAt方法:

  1. regex = “regex”; text = “matches, regex”; 预期结 false
  2. regex = “regex”; text = “regex, matches”; 预期结 true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RegexDemo {

public static void main(String[] args) {
String regex = "regex";
String text1 = "matches, regex";
String text2 = "regex, matches";
Pattern pattern = Pattern.compile(regex);
System.out.println("====case one====");
Matcher matcher1 = pattern.matcher(text1);
System.out.println(matcher1.lookingAt());
System.out.println("====case two====");
Matcher matcher2 = pattern.matcher(text2);
System.out.println(matcher2.lookingAt());
}
}

输出结果如下:

1
2
3
4
====case one====
false
====case two====
true

定位匹配配置

Matcher类的定位查找方面的API如主要是group, start, end与region等,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 返回指定的符合匹配条件的子序列(正则的组概念), 0代表这个那个正则表达式,group>0表示正则的子表达式
// example: regex = "(.)\\1{2,}"; text = "Hello, aaaaa!";
// 匹配结果 group(0) = "aaaaa" ; group(1) = "a";
public String group(int group)
// 返回匹配到的子串,同 matcher.group(0)
public String group()
// 根据组名得到匹配子串
// example: regex = "(?<groupName>.)\\1{2,}"; text = "Hello, aaaaa!";
// 匹配结果 group(0) = "aaaaa" ; group(groupName) = "a";
public String group(String name)
// 返回捕获组的个数
public int groupCount();

// 返回匹配到的字串的开始索引位置
public int start();
// 返回上一个匹配操作期间给定组ID捕获的子序列的起始索引。
public int start(int group);
// 返回上一个匹配操作期间给定组名捕获的子序列的起始索引。
public int start(String name)

// 最后一个字符匹配后的偏移量
public int end();
// 返回上一个匹配操作期间给定组ID捕获的子序列的最后一个字符匹配的偏移量
public int end(int group);
// 返回上一个匹配操作期间给定组名捕获的子序列的最后一个字符匹配的偏移量
public int end(String name);

// 设置匹配器的匹配域(文本索引区间)
// example:regex = "VIP|(?<wifi>wifi)|(?<vip>vip)|wifi"; text = "我用的wifi是vip的。";
// region(0,8);
// 结果: 能匹配到wifi, 无法匹配到vip
public Matcher region(int start, int end);
// 返回匹配域的开始位置
public int regionStart();
// 返回匹配域的结束位置
public int regionEnd();

下面通过使用代码来说明一下group和region的使用。首先是group,下面的代码将展示group几个重载方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RegexDemo {

public static void main(String[] args) {
String regex1 = "(.)\\1{2,}";
String regex2 = "(?<groupName>.)\\1{2,}";
String text = "Hello, aaaaa!";
Pattern pattern1 = Pattern.compile(regex1);
Pattern pattern2 = Pattern.compile(regex2);
Matcher matcher1 = pattern1.matcher(text);
Matcher matcher2 = pattern2.matcher(text);
if (matcher1.find()) {
System.out.println("方法group()输出:" + matcher1.group());
}
// 重置匹配器
matcher1.reset();
if (matcher1.find()) {
System.out.println("方法group(1)输出:" + matcher1.group(1));
}
if (matcher2.find()) {
System.out.println("方法group(name)输出:" + matcher2.group("groupName"));
}
}
}

输出结果如下:

1
2
3
方法group()输出:aaaaa
方法group(1)输出:a
方法group(name)输出:a

接下来是region,通过下面的代码可以了解region的使用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RegexDemo {

public static void main(String[] args) {
String regex = "VIP|(?<wifi>wifi)|(?<vip>vip)|wifi";
String text = "我用的wifi是vip的。";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
matcher = matcher.region(0, 8);
System.out.println("方法regionStart()输出:" + matcher.regionStart());
System.out.println("方法regionEnd()输出:" + matcher.regionEnd());
while (matcher.find()) {
System.out.println("方法region(0,8)匹配:" + matcher.group());
}
matcher = matcher.region(8, 12);
System.out.println("方法regionStart()输出:" + matcher.regionStart());
System.out.println("方法regionEnd()输出:" + matcher.regionEnd());
while (matcher.find()) {
System.out.println("方法region(8,12)匹配:" + matcher.group());
}
}
}

输出结果如下:

1
2
3
4
5
6
方法regionStart()输出:0
方法regionEnd()输出:8
方法region(0,8)匹配:wifi
方法regionStart()输出:8
方法regionEnd()输出:12
方法region(8,12)匹配:vip

替换匹配文本

Matcher类文本替换相关的方法如下:

1
2
3
4
5
6
7
8
9
10
11
// 替换第一个匹配到的第一个子串
public String replaceFirst(String replacement)

// 替换匹配到的所有子串
public String replaceAll(String replacement);

// 实现非终端添加和替换步骤
public Matcher appendReplacement(StringBuffer sb, String replacement);

// 实现终端添加和替换步骤
public StringBuffer appendTail(StringBuffer sb);

这里着重看一下appendReplacement和appendTail的使用方法,下面的代码是展示用正则来剔除英文串的情况,同时若存在白名单中的单词不剔除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class RegexDemo {

private static final Pattern PATTERN_WHITE_NAME = Pattern.compile("(?i)VIP|wifi");
private static final Pattern PATTERN_ENGLISH = Pattern.compile("[a-zA-Z]+");

public static void main(String[] args) {
String text = "我用的aaaAwifigood是viphelooooo的。";
Matcher matcher = PATTERN_ENGLISH.matcher(text);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String str = matcher.group();
matcher.appendReplacement(sb, processWhiteName(str));
}
matcher.appendTail(sb);
System.out.println(sb.toString());
}

private static String processWhiteName(String str) {
StringBuilder stringBuilder = new StringBuilder();
Matcher matcherWhiteName = PATTERN_WHITE_NAME.matcher(str);
while (matcherWhiteName.find()) {
stringBuilder.append(matcherWhiteName.group());
}
return stringBuilder.toString();
}
}

输出结果如下:

1
我用的wifi是vip的。

其他API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 重置匹配器
public Matcher reset();
// 根据新输入的文本重值匹配器
public Matcher reset(CharSequence input);

// 设置匹配器的区域边界的锚定
public Matcher useAnchoringBounds(boolean b)
// 查询此匹配器的区域边界的锚定
// 默认情况下为true
public boolean hasAnchoringBounds();

// 设置匹配器的区域边界的透明度
public Matcher useTransparentBounds(boolean b)
// 查询此匹配器的区域边界的透明度
// 默认情况下为true
public boolean hasTransparentBounds()

// 在此匹配器执行的最后一次匹配操作, 如果输入的结尾被搜索引擎命中,则返回true,
public boolean hitEnd();

// 返回匹配结果
public MatchResult toMatchResult();

// 返回此匹配器的模式
public Pattern pattern();
// 根据新的模式设置匹配器
public Matcher usePattern(Pattern newPattern);

// 如果更多输入可以将正匹配更改为负匹配,则返回true
public boolean requireEnd();

PatternSyntaxException类

PatternSyntaxException顾名思义,是用来表示在正则在编译的时候是否有语法错误。提供了如下API:

1
2
3
4
5
6
7
8
// 获取错误的描述
public String getDescription();
// 获取错误的索引
public int getIndex() ;
// 获取错误的正则表达式模式。
public String getPattern() ;
//返回多行字符串,包含语法错误及其索引的描述、错误的正则表达式模式和模式中错误索引的可视化指示
public String getMessage()

通过一个代码示例来看一下API的使用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RegexDemo {

public static void main(String[] args) {
String regex = "((?i)[a-zA-Z]+";
try {
Pattern pattern = Pattern.compile(regex);
} catch (PatternSyntaxException e) {
System.out.println("getIndex输出: " + e.getIndex());
System.out.println("getDescription输出: " + e.getDescription());
System.out.println("getMessage输出: " + e.getMessage());
System.out.println("getPattern输出: " + e.getPattern());
}
}
}

输出结果:

1
2
3
4
5
6
getIndex输出: 14
getDescription输出: Unclosed group
getMessage输出: Unclosed group near index 14
((?i)[a-zA-Z]+
^
getPattern输出: ((?i)[a-zA-Z]+