正则表达式
正则表达式是什么?有什么用?
正则表达式(Regular Expression)是一种文本规则,可以用来校验、查找、替换与规则匹配的文本。
正则表达式是一个强大的文本匹配工具,但是它的规则实在很繁琐,而且理解起来也颇为蛋疼,容易让人望而生畏。
学习正则应该从实例去理解规则。
一、正则表达式介绍
JDK中的java.util.regex
包提供了对正则表达式的支持。
java.util.regex
有三个核心类:
- Pattern类:
Pattern
是一个正则表达式的编译表示。 - Matcher类:
Matcher
是对输入字符串进行解释和匹配操作的引擎。 - PatternSyntaxException:
PatternSyntaxException
是一个非强制异常类,它表示一个正则表达式模式中的语法错误。
注:需要格外注意一点,在Java中使用反斜杠"\"时必须写成 "\\"
。所以本文的代码出现形如String regex = "\\$\\{.*?\\}"
其实就是"$\{.*?\}",不要以为是画风不对哦。
1. Pattern类
Pattern
类没有公共构造方法。要创建一个Pattern
对象,你必须首先调用其静态方法compile
,加载正则规则字符串,然后返回一个Pattern对象。
与Pattern
类一样,Matcher
类也没有公共构造方法。你需要调用Pattern
对象的matcher
方法来获得一个Matcher
对象。
示例
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(content);
2. Matcher类
Matcher
类可以说是java.util.regex
核心类中的必杀技!
Matcher
类有三板斧(三类功能):
- 校验
- 查找
- 替换
校验文本是否与正则规则匹配
为了检查文本是否与正则规则匹配,Matcher提供了以下几个返回值为boolean
的方法。
序号 | 方法及说明 |
---|---|
1 | public boolean lookingAt() 尝试将从区域开头开始的输入序列与该模式匹配。 |
2 | public boolean find() 尝试查找与该模式匹配的输入序列的下一个子序列。 |
3 | public boolean find(int start)重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。 |
4 | public boolean matches() 尝试将整个区域与模式匹配。 |
如果你傻傻分不清上面的查找方法有什么区别,那么下面一个例子就可以让你秒懂。
public static void main(String[] args) { checkLookingAt("hello", "helloworld"); checkLookingAt("world", "helloworld"); checkFind("hello", "helloworld"); checkFind("world", "helloworld"); checkMatches("hello", "helloworld"); checkMatches("world", "helloworld"); checkMatches("helloworld", "helloworld"); } private static void checkLookingAt(String regex, String content) { Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); if (m.lookingAt()) { System.out.println(content + "\tlookingAt: " + regex); } else { System.out.println(content + "\tnot lookingAt: " + regex); } } private static void checkFind(String regex, String content) { Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); if (m.find()) { System.out.println(content + "\tfind: " + regex); } else { System.out.println(content + "\tnot find: " + regex); } } private static void checkMatches(String regex, String content) { Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); if (m.matches()) { System.out.println(content + "\tmatches: " + regex); } else { System.out.println(content + "\tnot matches: " + regex); } }
输出
helloworld lookingAt: hello
helloworld not lookingAt: world
helloworld find: hello
helloworld find: world
helloworld not matches: hello
helloworld not matches: world
helloworld matches: helloworld
说明
regex = “world” 表示的正则规则是以world开头的字符串,regex = “hello” 和regex = “helloworld” 也是同理。
lookingAt
方法从头部开始,检查content字符串是否有子字符串于正则规则匹配。find
方法检查content字符串是否有子字符串于正则规则匹配,不管字符串所在位置。matches
方法检查content字符串整体是否与正则规则匹配。
查找匹配正则规则的文本位置
为了查找文本匹配正则规则的位置,Matcher
提供了以下方法:
序号 | 方法及说明 |
---|---|
1 | public int start() 返回以前匹配的初始索引。 |
2 | public int start(int group) 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引 |
3 | public int end()返回最后匹配字符之后的偏移量。 |
4 | public int end(int group)返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。 |
5 | public String group()返回前一个符合匹配条件的子序列。 |
6 | public String group(int group)返回指定的符合匹配条件的子序列。 |
示例:使用start()、end()、group() 查找所有匹配正则条件的子序列
public static void main(String[] args) { final String regex = "world"; final String content = "helloworld helloworld"; Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); System.out.println("content: " + content); int i = 0; while (m.find()) { i++; System.out.println("[" + i + "th] found"); System.out.print("start: " + m.start() + ", "); System.out.print("end: " + m.end() + ", "); System.out.print("group: " + m.group() + "\n"); } }
输出
content: helloworld helloworld [1th] found start: 5, end: 10, group: world [2th] found start: 16, end: 21, group: world
替换匹配正则规则的文本
替换方法是替换输入字符串里文本的方法:
序号 | 方法及说明 |
---|---|
1 | public Matcher appendReplacement(StringBuffer sb, String replacement)实现非终端添加和替换步骤。 |
2 | public StringBuffer appendTail(StringBuffer sb)实现终端添加和替换步骤。 |
3 | public String replaceAll(String replacement) 替换模式与给定替换字符串相匹配的输入序列的每个子序列。 |
4 | public String replaceFirst(String replacement) 替换模式与给定替换字符串匹配的输入序列的第一个子序列。 |
5 | public static String quoteReplacement(String s)返回指定字符串的字面替换字符串。这个方法返回一个字符串,就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作。 |
(1)示例:replaceFirst vs replaceAll
public static void main(String[] args) { String regex = "can"; String replace = "can not"; String content = "I can because I think I can."; Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); System.out.println("content: " + content); System.out.println("replaceFirst: " + m.replaceFirst(replace)); System.out.println("replaceAll: " + m.replaceAll(replace)); }
replaceFirst:替换第一个匹配正则规则的子序列。
replaceAll:替换所有匹配正则规则的子序列。
(2)示例:appendReplacement、appendTail和replaceAll
public static void main(String[] args) { String regex = "can"; String replace = "can not"; String content = "I can because I think I can."; StringBuffer sb = new StringBuffer(); StringBuffer sb2 = new StringBuffer(); System.out.println("content: " + content); Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); while (m.find()) { m.appendReplacement(sb, replace); } System.out.println("appendReplacement: " + sb); m.appendTail(sb); System.out.println("appendTail: " + sb); }
说明
从输出结果可以看出,appendReplacement
和appendTail
方法组合起来用,功能和replaceAll
是一样的。
如果你查看replaceAll
的源码,会发现其内部就是使用appendReplacement
和appendTail
方法组合来实现的。
(3)quoteReplacement和replaceAll,解决特殊字符替换问题
public static void main(String[] args) { String regex = "\\$\\{.*?\\}"; String replace = "${product}"; String content = "product is ${productName}."; Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); String replaceAll = m.replaceAll(replace); System.out.println("content: " + content); System.out.println("replaceAll: " + replaceAll); }
说明
String regex = "\\$\\{.*?\\}";
表示匹配类似${name}
这样的字符串。由于$
、{
、}
都是特殊字符,需要用反义字符\
来修饰才能被当做一个字符串字符来处理。
上面的例子是想将 ${productName}
替换为 ${product}
,然而replaceAll
方法却将传入的字符串中的$
当做特殊字符来处理了。结果产生异常。
如何解决这个问题?
JDK1.5引入了quoteReplacement
方法。它可以用来转换特殊字符。其实源码非常简单,就是判断字符串中如果有\
或$
,就为它加一个转义字符\
我们对上面的代码略作调整:
m.replaceAll(replace)
改为m.replaceAll(Matcher.quoteReplacement(replace))
,新代码如下:
public static void main(String[] args) { String regex = "\\$\\{.*?\\}"; String replace = "${product}"; String content = "product is ${productName}."; Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); String replaceAll = m.replaceAll(Matcher.quoteReplacement(replace)); System.out.println("content: " + content); System.out.println("replaceAll: " + replaceAll); }
说明
字符串中如果有\
或$
,不能被正常解析的问题解决。
二、正则表达式语法规则
正则表达式
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
元字符
元字符(metacharacters)就是正则表达式中具有特殊意义的专用字符。
普通字符
普通字符包括没有显式指定为元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。
1. 元字符
基本元字符
正则表达式的元字符难以记忆,很大程度上是因为有很多为了简化表达而出现的等价字符。
而实际上最基本的元字符,并没有那么多。对于大部分的场景,基本元字符都可以搞定。
多选 - |
匹配一个确定的字符串
checkMatches("abc", "abc");
如果要匹配一个确定的字符串,非常简单,如上所示。
如果你不确定要匹配的字符串,希望有多个选择,怎么办?
答案是:使用元字符|
,它的含义是或。
匹配多个可选的字符串
// 测试正则表达式字符:| Assert.assertTrue(checkMatches("yes|no", "yes")); Assert.assertTrue(checkMatches("yes|no", "no")); Assert.assertFalse(checkMatches("yes|no", "right"));
分组
如果你希望表达式由多个子表达式组成,你可以使用 ()
。
匹配组合字符串
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "ended")); Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "ending")); Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "playing")); Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "played"));
指定单字符有效范围 - []
前面展示了如何匹配字符串,但是很多时候你需要精确的匹配一个字符,这时可以使用[]
。
字符在指定范围
// 测试正则表达式字符:[] Assert.assertTrue(checkMatches("[abc]", "b")); // 字符只能是a、b、c Assert.assertTrue(checkMatches("[a-z]", "m")); // 字符只能是a - z Assert.assertTrue(checkMatches("[A-Z]", "O")); // 字符只能是A - Z Assert.assertTrue(checkMatches("[a-zA-Z]", "K")); // 字符只能是a - z和A - Z Assert.assertTrue(checkMatches("[a-zA-Z]", "k")); Assert.assertTrue(checkMatches("[0-9]", "5")); // 字符只能是0 - 9
指定单字符无效范围 - [^]
字符串不能在指定范围
如果需要匹配一个字符的逆操作,即字符不能在指定范围,可以使用[^]。
// 测试正则表达式字符:[^] Assert.assertFalse(checkMatches("[^abc]", "b")); // 字符不能是a、b、c Assert.assertFalse(checkMatches("[^a-z]", "m")); // 字符不能是a - z Assert.assertFalse(checkMatches("[^A-Z]", "O")); // 字符不能是A - Z Assert.assertFalse(checkMatches("[^a-zA-Z]", "K")); // 字符不能是a - z和A - Z Assert.assertFalse(checkMatches("[^a-zA-Z]", "k")); Assert.assertFalse(checkMatches("[^0-9]", "5")); // 字符不能是0 - 9
限制字符数量 - {}
如果想要控制字符出现的次数,可以使用{}
。
字符 | 描述 |
---|---|
{n} |
n 是一个非负整数。匹配确定的 n 次。 |
{n,} |
n 是一个非负整数。至少匹配 n 次。 |
{n,m} |
m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。 |
限制字符出现次数
// {n}: n 是一个非负整数。匹配确定的 n 次。 checkMatches("ap{1}", "a"); checkMatches("ap{1}", "ap"); checkMatches("ap{1}", "app"); checkMatches("ap{1}", "apppppppppp"); // {n,}: n 是一个非负整数。至少匹配 n 次。 checkMatches("ap{1,}", "a"); checkMatches("ap{1,}", "ap"); checkMatches("ap{1,}", "app"); checkMatches("ap{1,}", "apppppppppp"); // {n,m}: m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。 checkMatches("ap{2,5}", "a"); checkMatches("ap{2,5}", "ap"); checkMatches("ap{2,5}", "app"); checkMatches("ap{2,5}", "apppppppppp");
转义字符 - /
如果想要查找元字符本身,你需要使用转义符,使得正则引擎将其视作一个普通字符,而不是一个元字符去处理。
* 的转义字符:\*
+ 的转义字符:\+
? 的转义字符:\?
^ 的转义字符:\^
$ 的转义字符:\$
. 的转义字符:\.
如果是转义符\
本身,你也需要使用\\
。
指定表达式字符串的开始和结尾 - ^、$
如果希望匹配的字符串必须以特定字符串开头,可以使用^
。
注:请特别留意,这里的^
一定要和 [^]
中的 “^” 区分。
限制字符串头部
Assert.assertTrue(checkMatches("^app[a-z]{0,}", "apple")); // 字符串必须以app开头 Assert.assertFalse(checkMatches("^app[a-z]{0,}", "aplause"));
如果希望匹配的字符串必须以特定字符串开头,可以使用$
。
限制字符串尾部
Assert.assertTrue(checkMatches("[a-z]{0,}ing$", "playing")); // 字符串必须以ing结尾 Assert.assertFalse(checkMatches("[a-z]{0,}ing$", "long"));
2. 等价字符
等价字符,顾名思义,就是对于基本元字符表达的一种简化(等价字符的功能都可以通过基本元字符来实现)。
在没有掌握基本元字符之前,可以先不用理会,因为很容易把人绕晕。
等价字符的好处在于简化了基本元字符的写法。
表示某一类型字符的等价字符
下表中的等价字符都表示某一类型的字符。
字符 | 描述 |
---|---|
. |
匹配除“\n”之外的任何单个字符。 |
\d |
匹配一个数字字符。等价于[0-9]。 |
\D |
匹配一个非数字字符。等价于[^0-9]。 |
\w |
匹配包括下划线的任何单词字符。类似但不等价于“[A-Za-z0-9_]”,这里的单词字符指的是Unicode字符集。 |
\W |
匹配任何非单词字符。 |
\s |
匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。 |
\S |
匹配任何可见字符。等价于[ \f\n\r\t\v]。 |
基本等价字符的用法
// 匹配除“\n”之外的任何单个字符 Assert.assertTrue(checkMatches(".{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")); Assert.assertTrue(checkMatches(".{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\")); Assert.assertFalse(checkMatches(".", "\n")); Assert.assertFalse(checkMatches("[^\n]", "\n")); // 匹配一个数字字符。等价于[0-9] Assert.assertTrue(checkMatches("\\d{1,}", "0123456789")); // 匹配一个非数字字符。等价于[^0-9] Assert.assertFalse(checkMatches("\\D{1,}", "0123456789")); // 匹配包括下划线的任何单词字符。类似但不等价于“[A-Za-z0-9_]”,这里的单词字符指的是Unicode字符集 Assert.assertTrue(checkMatches("\\w{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")); Assert.assertFalse(checkMatches("\\w{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\")); // 匹配任何非单词字符 Assert.assertFalse(checkMatches("\\W{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")); Assert.assertTrue(checkMatches("\\W{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\")); // 匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v] Assert.assertTrue(checkMatches("\\s{1,}", " \f\r\n\t")); // 匹配任何可见字符。等价于[^ \f\n\r\t\v] Assert.assertFalse(checkMatches("\\S{1,}", " \f\r\n\t"));
限制字符数量的等价字符
在基本元字符章节中,已经介绍了限制字符数量的基本元字符 - {}
。
此外,还有 *
、+
、?
这个三个为了简化写法而出现的等价字符,我们来认识一下。
字符 | 描述 |
---|---|
* |
匹配前面的子表达式零次或多次。等价于{0,}。 |
+ |
匹配前面的子表达式一次或多次。等价于{1,}。 |
? |
匹配前面的子表达式零次或一次。等价于 {0,1}。 |
限制字符数量的等价字符
// *: 匹配前面的子表达式零次或多次。* 等价于{0,}。 checkMatches("ap*", "a"); checkMatches("ap*", "ap"); checkMatches("ap*", "app"); checkMatches("ap*", "apppppppppp"); // +: 匹配前面的子表达式一次或多次。+ 等价于 {1,}。 checkMatches("ap+", "a"); checkMatches("ap+", "ap"); checkMatches("ap+", "app"); checkMatches("ap+", "apppppppppp"); // ?: 匹配前面的子表达式零次或一次。? 等价于 {0,1}。 checkMatches("ap?", "a"); checkMatches("ap?", "ap"); checkMatches("ap?", "app"); checkMatches("ap?", "apppppppppp");
3. 元字符优先等级
正则表达式从左到右进行计算,并遵循优先级顺序,这与算术表达式非常类似。
下表从最高到最低说明了各种正则表达式运算符的优先级顺序:
运算符 | 说明 |
---|---|
\ | 转义符 |
(), (?, (?=), [] | 括号和中括号 |
*, +, ?, {n}, {n,}, {n,m} | 限定符 |
^, $, *任何元字符、任何字符* | 定位点和序列 |
| | 替换 |
字符具有高于替换运算符的优先级,使得“m|food”匹配“m”或“food”。若要匹配“mood”或“food”,请使用括号创建子表达式,从而产生“(m|f)ood”。
分组构造
在基本元字符章节,提到了 ()
字符可以用来对表达式分组。实际上分组还有更多复杂的用法。
所谓分组构造,是用来描述正则表达式的子表达式,用于捕获字符串中的子字符串。
4. 捕获与非捕获
下表为分组构造中的捕获和非捕获分类。
表达式 | 描述 | 捕获或非捕获 |
---|---|---|
(exp) |
匹配的子表达式 | 捕获 |
(?<name>exp) |
命名的反向引用 | 捕获 |
(?:exp) |
非捕获组 | 非捕获 |
(?=exp) |
零宽度正预测先行断言 | 非捕获 |
(?!exp) |
零宽度负预测先行断言 | 非捕获 |
(?<=exp) |
零宽度正回顾后发断言 | 非捕获 |
(?<!exp) |
零宽度负回顾后发断言 | 非捕获 |
注:Java正则引擎不支持平衡组。
5. 反向引用
带编号的反向引用
带编号的反向引用使用以下语法:\number
其中number 是正则表达式中捕获组的序号位置。 例如,\4 匹配第四个捕获组的内容。 如果正则表达式模式中未定义number,则将发生分析错误
匹配重复的单词和紧随每个重复的单词的单词(不命名子表达式)
// (\w+)\s\1\W(\w+) 匹配重复的单词和紧随每个重复的单词的单词 Assert.assertTrue(findAll("(\\w+)\\s\\1\\W(\\w+)", "He said that that was the the correct answer.") > 0);
说明
(\w+): 匹配一个或多个单词字符。
\s: 与空白字符匹配。
\1: 匹配第一个组,即(\w+)。
\W: 匹配包括空格和标点符号的一个非单词字符。 这样可以防止正则表达式模式匹配从第一个捕获组的单词开头的单词。
命名的反向引用
命名后向引用通过使用下面的语法进行定义:\k<name >
匹配重复的单词和紧随每个重复的单词的单词(命名子表达式)
// (?<duplicateWord>\w+)\s\k<duplicateWord>\W(?<nextWord>\w+) 匹配重复的单词和紧随每个重复的单词的单词 Assert.assertTrue(findAll("(?<duplicateWord>\\w+)\\s\\k<duplicateWord>\\W(?<nextWord>\\w+)", "He said that that was the the correct answer.") > 0);
说明
(?\w+): 匹配一个或多个单词字符。 命名此捕获组 duplicateWord。
\s: 与空白字符匹配。
\k: 匹配名为 duplicateWord 的捕获的组。
\W: 匹配包括空格和标点符号的一个非单词字符。 这样可以防止正则表达式模式匹配从第一个捕获组的单词开头的单词。
(?\w+): 匹配一个或多个单词字符。 命名此捕获组 nextWord。
6. 非捕获组
(?:exp)
表示当一个限定符应用到一个组,但组捕获的子字符串并非所需时,通常会使用非捕获组构造。
匹配以.结束的语句
// 匹配由句号终止的语句。 Assert.assertTrue(findAll("(?:\\b(?:\\w+)\\W*)+\\.", "This is a short sentence. Never end") > 0);
7. 零宽断言
用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。
表达式 | 描述 |
---|---|
(?=exp) |
匹配exp前面的位置 |
(?<=exp) |
匹配exp后面的位置 |
(?!exp) |
匹配后面跟的不是exp的位置 |
(?<!exp) |
匹配前面不是exp的位置 |
匹配exp前面的位置
(?=exp)
表示输入字符串必须匹配子表达式中的正则表达式模式,尽管匹配的子字符串未包含在匹配结果中。
// \b\w+(?=\sis\b) 表示要捕获is之前的单词 Assert.assertTrue(findAll("\\b\\w+(?=\\sis\\b)", "The dog is a Malamute.") > 0); Assert.assertFalse(findAll("\\b\\w+(?=\\sis\\b)", "The island has beautiful birds.") > 0); Assert.assertFalse(findAll("\\b\\w+(?=\\sis\\b)", "The pitch missed home plate.") > 0); Assert.assertTrue(findAll("\\b\\w+(?=\\sis\\b)", "Sunday is a weekend day.") > 0);
说明
\b: 在单词边界处开始匹配。
\w+: 匹配一个或多个单词字符。
(?=\sis\b): 确定单词字符是否后接空白字符和字符串“is”,其在单词边界处结束。 如果如此,则匹配成功。
匹配exp后面的位置
(?<=exp)
表示子表达式不得在输入字符串当前位置左侧出现,尽管子表达式未包含在匹配结果中。零宽度正回顾后发断言不会回溯。
// (?<=\b20)\d{2}\b 表示要捕获以20开头的数字的后面部分 Assert.assertTrue(findAll("(?<=\\b20)\\d{2}\\b", "2010 1999 1861 2140 2009") > 0);
说明
\d{2}: 匹配两个十进制数字。
{?<=\b20): 如果两个十进制数字的字边界以小数位数“20”开头,则继续匹配。
\b: 在单词边界处结束匹配。
匹配后面跟的不是exp的位置
(?!exp)
表示输入字符串不得匹配子表达式中的正则表达式模式,尽管匹配的子字符串未包含在匹配结果中。
捕获未以“un”开头的单词
// \b(?!un)\w+\b 表示要捕获未以“un”开头的单词 Assert.assertTrue(findAll("\\b(?!un)\\w+\\b", "unite one unethical ethics use untie ultimate") > 0);
说明
\b: 在单词边界处开始匹配。
(?!un): 确定接下来的两个的字符是否为“un”。 如果没有,则可能匹配。
\w+: 匹配一个或多个单词字符。
\b: 在单词边界处结束匹配。
匹配前面不是exp的位置
(?<!exp)
表示子表达式不得在输入字符串当前位置的左侧出现。 但是,任何不匹配子表达式 的子字符串不包含在匹配结果中。
捕获任意工作日
// (?<!(Saturday|Sunday) )\b\w+ \d{1,2}, \d{4}\b 表示要捕获任意工作日(即周一到周五) Assert.assertTrue(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Monday February 1, 2010") > 0); Assert.assertTrue(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Wednesday February 3, 2010") > 0); Assert.assertFalse(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Saturday February 6, 2010") > 0); Assert.assertFalse(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Sunday February 7, 2010") > 0); Assert.assertTrue(findAll("(?<!(Saturday|Sunday) )\\b\\w+ \\d{1,2}, \\d{4}\\b", "Monday, February 8, 2010") > 0);
8. 贪婪与懒惰
当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。
有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。
表达式 | 描述 |
---|---|
*? |
重复任意次,但尽可能少重复 |
+? |
重复1次或更多次,但尽可能少重复 |
?? |
重复0次或1次,但尽可能少重复 |
{n,m}? |
重复n到m次,但尽可能少重复 |
{n,}? |
重复n次以上,但尽可能少重复 |
Java正则中贪婪与懒惰的示例
// 贪婪匹配 Assert.assertTrue(findAll("a\\w*b", "abaabaaabaaaab") > 0); // 懒惰匹配 Assert.assertTrue(findAll("a\\w*?b", "abaabaaabaaaab") > 0); Assert.assertTrue(findAll("a\\w+?b", "abaabaaabaaaab") > 0); Assert.assertTrue(findAll("a\\w??b", "abaabaaabaaaab") > 0); Assert.assertTrue(findAll("a\\w{0,4}?b", "abaabaaabaaaab") > 0); Assert.assertTrue(findAll("a\\w{3,}?b", "abaabaaabaaaab") > 0);
说明
本例中代码展示的是使用不同贪婪或懒惰策略去查找字符串"abaabaaabaaaab" 中匹配以"a"开头,以"b"结尾的所有子字符串。
请从输出结果中,细细体味使用不同的贪婪或懒惰策略,对于匹配子字符串有什么影响。
9. 附录
匹配正则字符串的方法
由于正则表达式中很多元字符本身就是转义字符,在Java字符串的规则中不会被显示出来。
为此,可以使用一个工具类org.apache.commons.lang3.StringEscapeUtils
来做特殊处理,使得转义字符可以打印。这个工具类提供的都是静态方法,从方法命名大致也可以猜出用法,这里不多做说明。
如果你了解maven,可以直接引入依赖commons-lang3。
本文为了展示正则匹配规则用到的方法
private boolean checkMatches(String regex, String content) { Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); boolean flag = m.matches(); if (m.matches()) { System.out.println(StringEscapeUtils.escapeJava(content) + "\tmatches: " + StringEscapeUtils.escapeJava(regex)); } else { System.out.println(StringEscapeUtils.escapeJava(content) + "\tnot matches: " + StringEscapeUtils.escapeJava(regex)); } return flag; } public int findAll(String regex, String content) { Pattern p = Pattern.compile(regex); Matcher m = p.matcher(content); System.out.println("regex = " + regex + ", content: " + content); int count = 0; while (m.find()) { count++; System.out.println("[" + count + "th] " + "start: " + m.start() + ", end: " + m.end() + ", group: " + m.group()); } if (0 == count) { System.out.println("not found"); } return count; }
速查元字符字典
为了方便快查正则的元字符含义,在本节根据元字符的功能集中罗列正则的各种元字符。
限定符
字符 | 描述 |
---|---|
* |
匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于{0,}。 |
+ |
匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。 |
? |
匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 中的"do" 。? 等价于 {0,1}。 |
{n} |
n 是一个非负整数。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。 |
{n,} |
n 是一个非负整数。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。 |
{n,m} |
m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格。 |
定位符
字符 | 描述 |
---|---|
^ |
匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配。 |
$ |
匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配。 |
\b |
匹配一个字边界,即字与空格间的位置。 |
\B |
非字边界匹配。 |
非打印字符
字符 | 描述 |
---|---|
\cx |
匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。 |
\f |
匹配一个换页符。等价于 \x0c 和 \cL。 |
\n |
匹配一个换行符。等价于 \x0a 和 \cJ。 |
\r |
匹配一个回车符。等价于 \x0d 和 \cM。 |
\s |
匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。 |
\S |
匹配任何非空白字符。等价于 [^\f\n\r\t\v]。 |
\t |
匹配一个制表符。等价于 \x09 和 \cI。 |
\v |
匹配一个垂直制表符。等价于 \x0b 和 \cK。 |
分组
表达式 | 描述 |
---|---|
(exp) |
匹配的子表达式。()中的内容就是子表达式。 |
(?<name>exp) |
命名的子表达式(反向引用)。 |
(?:exp) |
非捕获组,表示当一个限定符应用到一个组,但组捕获的子字符串并非所需时,通常会使用非捕获组构造。 |
(?=exp) |
匹配exp前面的位置。 |
(?<=exp) |
匹配exp后面的位置。 |
(?!exp) |
匹配后面跟的不是exp的位置。 |
(?<!exp) |
匹配前面不是exp的位置。 |
特殊字符
字符 | 描述 |
---|---|
\ |
将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, 'n' 匹配字符 'n'。'\n' 匹配换行符。序列 '\' 匹配 "",而 '(' 则匹配 "("。 |
\| |
指明两项之间的一个选择。 |
[] |
匹配方括号范围内的任意一个字符。形式如:[xyz]、[xyz]、[a-z]、[a-z]、[x,y,z] |
三、正则表达式实例
正则是把双刃剑,它可以为你节省大量的代码行。但是由于它不易阅读,维护起来可是头疼的哦(你需要一个字符一个字符的去理解)。
1. 最实用的正则
校验中文
描述:校验字符串中只能有中文字符(不包括中文标点符号)。中文字符的Unicode编码范围是\u4e00 到 \u9fa5。
^[\u4e00-\u9fa5]+$
匹配: 春眠不觉晓
不匹配:春眠不觉晓,
校验身份证号码
描述:身份证为15位或18位。15位是第一代身份证。从1999年10月1日起,全国实行公民身份证号码制度,居民身份证编号由原15位升至18位。
15位身份证
描述:由15位数字组成。排列顺序从左至右依次为:六位数字地区码;六位数字出生日期;三位顺序号,其中15位男为单数,女为双数。
18位身份证
描述:由十七位数字本体码和一位数字校验码组成。排列顺序从左至右依次为:六位数字地区码;八位数字出生日期;三位数字顺序码和一位数字校验码(也可能是X)。
地区码(6位)
(1[1-5]|2[1-3]|3[1-7]|4[1-3]|5[0-4]|6[1-5])\d{4}
出生日期(8位)
注:下面的是18位身份证的有效出生日期,如果是15位身份证,只要将第一个\d{4}改为\d{2}即可。
((\d{4}((0[13578]|1[02])(0[1-9]|[12]\d|3[01])|(0[13456789]|1[012])(0[1-9]|[12]\d|30)|02(0[1-9]|1\d|2[0-8])))|([02468][048]|[13579][26])0229)
15位有效身份证
^((1[1-5]|2[1-3]|3[1-7]|4[1-3]|5[0-4]|6[1-5])\d{4})((\d{2}((0[13578]|1[02])(0[1-9]|[12]\d|3[01])|(0[13456789]|1[012])(0[1-9]|[12]\d|30)|02(0[1-9]|1\d|2[0-8])))|([02468][048]|[13579][26])0229)(\d{3})$
匹配:110001700101031
不匹配:110001701501031
18位有效身份证
^((1[1-5]|2[1-3]|3[1-7]|4[1-3]|5[0-4]|6[1-5])\d{4})((\d{4}((0[13578]|1[02])(0[1-9]|[12]\d|3[01])|(0[13456789]|1[012])(0[1-9]|[12]\d|30)|02(0[1-9]|1\d|2[0-8])))|([02468][048]|[13579][26])0229)(\d{3}(\d|X))$
匹配:110001199001010310 | 11000019900101015X
不匹配:990000199001010310 | 110001199013010310
校验有效用户名、密码
描述:长度为6-18个字符,允许输入字母、数字、下划线,首字符必须为字母。
^[a-zA-Z]\w{5,17}$
匹配:he_llo@worl.d.com | hel.l-o@wor-ld.museum | h1ello@123.com
不匹配:hello@worl_d.com | he&llo@world.co1 | .hello@wor#.co.uk
校验邮箱
描述:不允许使用IP作为域名,如 : hello@154.145.68.12
@
符号前的邮箱用户和.
符号前的域名(domain)必须满足以下条件:
- 字符只能是英文字母、数字、下划线
_
、.
、-
; - 首字符必须为字母或数字;
_
、.
、-
不能连续出现。
域名的根域只能为字母,且至少为两个字符。
^[A-Za-z0-9](([_\.\-]?[a-zA-Z0-9]+)*)@([A-Za-z0-9]+)(([\.\-]?[a-zA-Z0-9]+)*)\.([A-Za-z]{2,})$
匹配:he_llo@worl.d.com | hel.l-o@wor-ld.museum | h1ello@123.com
不匹配:hello@worl_d.com | he&llo@world.co1 | .hello@wor#.co.uk
校验URL
描述:校验URL。支持http、https、ftp、ftps。
^(ht|f)(tp|tps)\://[a-zA-Z0-9\-\.]+\.([a-zA-Z]{2,3})?(/\S*)?$
匹配:http://google.com/help/me | http://www.google.com/help/me/ | https://www.google.com/help.asp | ftp://www.google.com | ftps://google.org
不匹配:http://un/www.google.com/index.asp
校验时间
描述:校验时间。时、分、秒必须是有效数字,如果数值不是两位数,十位需要补零。
^([0-1][0-9]|[2][0-3]):([0-5][0-9])$
匹配:00:00:00 | 23:59:59 | 17:06:30
不匹配:17:6:30 | 24:16:30
校验日期
描述:校验日期。日期满足以下条件:
- 格式yyyy-MM-dd或yyyy-M-d
- 连字符可以没有或是“-”、“/”、“.”之一
- 闰年的二月可以有29日;而平年不可以。
- 一、三、五、七、八、十、十二月为31日。四、六、九、十一月为30日。
^(?:(?!0000)[0-9]{4}([-/.]?)(?:(?:0?[1-9]|1[0-2])\1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])\1(?:29|30)|(?:0?[13578]|1[02])\1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2\2(?:29))$
匹配:2016/1/1 | 2016/01/01 | 20160101 | 2016-01-01 | 2016.01.01 | 2000-02-29
不匹配:2001-02-29 | 2016/12/32 | 2016/6/31 | 2016/13/1 | 2016/0/1
校验中国手机号码
描述:中国手机号码正确格式:11位数字。
移动有16个号段:134、135、136、137、138、139、147、150、151、152、157、158、159、182、187、188。其中147、157、188是3G号段,其他都是2G号段。
联通有7种号段:130、131、132、155、156、185、186。其中186是3G(WCDMA)号段,其余为2G号段。
电信有4个号段:133、153、180、189。其中189是3G号段(CDMA2000),133号段主要用作无线网卡号。
总结:13开头手机号0-9;15开头手机号0-3、5-9;18开头手机号0、2、5-9。
此外,中国在国际上的区号为86,所以手机号开头有+86、86也是合法的。
^((\+)?86\s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))\d{8}$
匹配:+86 18012345678 | 86 18012345678 | 15812345678
不匹配:15412345678 | 12912345678 | 180123456789
校验中国固话号码
描述:固话号码,必须加区号(以0开头)。
3位有效区号:010、020~029,固话位数为8位。
4位有效区号:03xx开头到09xx,固话位数为7。
^(010|02[0-9])(\s|-)\d{8}|(0[3-9]\d{2})(\s|-)\d{7}$
匹配:010-12345678 | 010 12345678 | 0512-1234567 | 0512 1234567
不匹配:1234567 | 12345678
校验IPv4地址
描述:IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。
^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$
匹配:0.0.0.0 | 255.255.255.255 | 127.0.0.1
不匹配:10.10.10 | 10.10.10.256
校验IPv6地址
描述:IPv6的128位地址通常写成8组,每组为四个十六进制数的形式。
IPv6地址可以表示为以下形式:
- IPv6 地址
- 零压缩 IPv6 地址(section 2.2 of rfc5952)
- 带有本地链接区域索引的 IPv6 地址 (section 11 of rfc4007)
- 嵌入IPv4的 IPv6 地址(section 2 of rfc6052)
- 映射IPv4的 IPv6 地址 (section 2.1 of rfc2765)
- 翻译IPv4的 IPv6 地址 (section 2.1 of rfc2765)
(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))
匹配:1:2:3:4:5:6:7:8 | 1:: | 1::8 | 1::6:7:8 | 1::5:6:7:8 | 1::4:5:6:7:8 | 1::3:4:5:6:7:8 | ::2:3:4:5:6:7:8 | 1:2:3:4:5:6:7:: | 1:2:3:4:5:6::8 | 1:2:3:4:5::8 | 1:2:3:4::8 | 1:2:3::8 | 1:2::8 | 1::8 | ::8 | fe80::7:8%1 | ::255.255.255.255 | 2001:db8:3:4::192.0.2.33 | 64:ff9b::192.0.2.33
不匹配:1.2.3.4.5.6.7.8 | 1::2::3
2. 特定字符
匹配长度为3的字符串:^.{3}$
。
匹配由26个英文字母组成的字符串:^[A-Za-z]+$
。
匹配由26个大写英文字母组成的字符串:^[A-Z]+$
。
匹配由26个小写英文字母组成的字符串:^[a-z]+$
。
匹配由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
。
匹配由数字、26个英文字母或者下划线组成的字符串:^\w+$
。
3. 特定数字
匹配正整数:^[1-9]\d*$
匹配负整数:^-[1-9]\d*$
匹配整数:^(-?[1-9]\d*)|0$
匹配正浮点数:^[1-9]\d*\.\d+|0\.\d+$
匹配负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$
匹配浮点数:^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$
参考
https://www.cnblogs.com/jingmoxukong/p/6026474.html