1. 简介

正则表达式,Regular Expression,一种字符串匹配的模式。

用途:文本的复杂处理,大部分编程语言、数据库、文本编辑器、开发环境都支持正则表达式。

开发中使用

  • 分析所要匹配的数据,写出测试用的典型数据
  • 在工具软件中进行匹配测试
  • 在程序中调用测试的正则表达式

2. 语法

2.1 标准字符集合

  • 能够与”多种字符”匹配的表达式
  • 注意区分大小写,大写是相反的意思
字符 描述
\d 任意一个数字,0-9中的任意一个
\w 任意一个字母或数字或下划线,也就是A-Z、a-z、0-9、_中任意一个
\s 包括空格、制表符、换行符等空白字符的其中一个
. 小数点可以匹配任意一个字符(除了换行符), 如果要匹配包括”\n”在内的所有字符,一般用[\s\S]

如果要匹配真正的’.’、’+’、’-‘、’\' , 就要转义,用\.\+\-\\

2.2 自定义字符集合

  • []方括号匹配方式,能够匹配方括号中任意一个字符
  • 正则表达式的特殊符号,被包含到中括号中,则失去了特殊意义,除了^、-之外
  • 标准字符集合,除小数点外,如果被包含于中括号,自定义字符集合被包含该集合
    • [\d.-+]将匹配:数字、小数点、+、-
例如 描述
[ab5@] 匹配”a”或”b”或”5”或”@”
[^abc] 匹配”a”、”b”、”c”之外的任意一个字符
[f-k] 匹配”f”-“k”之间的任意一个字母
[^A-F0-3] 匹配”A”-“F”、”0”-“3”之外的任意一个字符

2.3 量词

  • 修饰匹配次数的特殊符号
  • 匹配次数中的贪婪模式(匹配字符越多越好,默认)
  • 匹配次数中的非贪婪模式(匹配字符越少越好,在修饰匹配次数的特殊符号后再加上一个”?”号)
量词 描述
{n} 表达式重复n次
{m,n} 表达式至少重复m次,最多重复n次
{m,} 表达式至少重复m次
? 表达式匹配0次或1次,相当于{0,1}
+ 表达式至少出现1次,相当于{1,}
* 表达式不出现或出现任意次,相当于{0,}

例如:

  • [A-Za-z]+可以匹配单词

例如文本:

1
2
3
6、7、8、9、
1、2、3、4、5、6、7、8、9、
ab a1b a12b a123b a1234b a12345b

(\d、){1,3}的匹配结果:

1
2
3
4
5
6、7、8、
9、
1、2、3、
4、5、6、
7、8、9、

(\d、){1,3}?的匹配结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
6、
7、
8、
9、
1、
2、
3、
4、
5、
6、
7、
8、
9、

a\d?b的匹配结果:

1
2
ab
a1b

a\d+b的匹配结果:

1
2
3
4
5
a1b
a12b
a123b
a1234b
a12345b

a\d-b的匹配结果:

1
2
3
4
5
6
ab
a1b
a12b
a123b
a1234b
a12345b

2.4 字符边界

字符 描述
^ 与字符串开始的地方匹配
$ 与字符串结束的地方匹配
\b 匹配一个单词边界

\b匹配这样一个位置:前面的字符和后面的字符不全是\w。

例如文本:

1
2
3
4
5
6
7
Khighness 
Khighness1 Khighness2
3Khighness
4Khighness
Khighness5 Khighness6
7Khighness
8Khighness

^K匹配最初的”K”,s$匹配最后的”s”;

Khighness\b匹配第1、3、4、6、7行的”Khighness”;

\bKhighness匹配第1、2、5行的”Khighness”;

\bKhighness\b匹配第1行的”Khighness”。

2.5 匹配模式

  • IGNORECASE-忽略大小写模式
    • 匹配时忽略大小写
    • 默认情况下,正则表达式是要区分大小写的
  • SINGLELINE-单行模式
    • 整个文本看做一个字符串,只有一个开头,一个结尾
    • 使小数点”.”可以匹配包含换行符(\n)在内的任意字符
  • MULTILINE-多行模式
    • 每行都是一个字符串,都有开头和结尾
    • 在指定了MULTILINE之后,如果需要仅匹配字符串开始和结束为止,可以使用\A\Z

2.7 选择符和分组

表达式 描述
分支结构: | 左右两边表达式之间“或”关系,匹配左边或者右边
捕获组: () (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰; (2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到; (3) 每一对括号会分配一个编号,使用()的捕获根据左括号的顺序从1开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本
非捕获组: (?:Exception) 一些表达式中,不得不使用(),但又不需要保存()中子表达式匹配的内容,这时可以用非捕获组来抵消使用()带来的副作用

反向引用(\num):每一对()会分配一个编号,使用()的捕获模式

例如([a-z]{2})\1可以匹配类似”abab”、”gogo”、”toto”的字符串。

2.8 预搜索(零宽断言)

  • 只进行子表达式的匹配,匹配内容不计入最终的匹配结果,是零宽度
  • 这个位置应该符合某个条件。判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符。是对位置的匹配
  • 正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么久认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是零宽度的。占有字符还是零宽度,是针对匹配的内容屎否保存到最终的匹配结果中而言的。
表达式 描述
(?=exp) 断言自身出现的位置的后面能匹配表达式exp
(?<=exp) 断言自身出现的位置的前面能匹配表达式exp
(?!exp) 断言此位置的后面不能匹配表达式exp
(?<exp) 断言此位置的前面不能匹配表达式exp

例如:

  • [A-Za-z]+(?=ing)可以匹配所有以”ing”结尾的单词(匹配结果不包含”ing”,[A-Za-z]+ing的匹配结果包含”ing”)

  • (?<=in)[A-Za-z]+可以匹配所有以”in”为前缀的单词(匹配结果不包含”in”,in[A-Za-z]+的匹配结果包含”in”)

3. 验证

3.1 电话号码验证

要求

  • 固定电话号码由数字和”-“构成,电话号码为7到8位
  • 如果固定电话号码中包含有区号,那么区号为三位或四位,首位是0,区号用”-“和其他部分隔开
  • 移动电话号码为11位,第一位和第二位为”13”、”14”、15”、”17”、18”、”19”

分析:

  • 固定电话号码:0\d{2,3}-\d{7,9}
  • 移动电话号码: 1[3|4|5|7|8|9]\d{9}
  • 合起来就是:^0\d{2,3}-\d{7,9}|1[3|4|5|7|8|9]\d{9}$

3.3 电子邮箱验证

要求

  • 电子邮箱格式:名称@域名
  • 邮箱名称部分:允许汉字、字母、数字、中划线和下划线
  • 邮箱域名部分:允许字母、数字、英语句号

分析

  • 邮箱名称表达式:
    • 汉字[\u4e00-\u9fa5]
    • 字母[A-Za-z]
    • 数字[0-9]
    • 中划线和下划线[-_]
    • 综上得到名称表达式[\u4e00-\u9fa5A-Za-z0-9-_]+
  • 邮箱域名表达式:
    • 域名的一般规律为[N级域名].[三级域名].[二级域名].[一级域名],格式类似为**.**.**.**
    • 一级域名只包含字母(如com、top、cn等),长度为2-4位
    • **部分可以表示为[A-Za-z0-9-_]+
    • .**部分可以表示为\.[A-Za-z0-9-_]+
    • 零或多个".**"可以表示为 (\.[A-Za-z0-9-_]+)*
    • 一级域名部分.**可以表示为.\.[a-z]{2,4}
    • 综上得到域名表达式[A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)*\.[a-z]{2,4}
  • 邮箱最终表达式:
    • 使用^匹配邮箱最开始的部分,使用$匹配邮箱结束部分以保证邮箱前后不能有其他字符
    • 由”名称@域名”得到最终表达式:[\u4e00-\u9fa5A-Za-z0-9-_]+@[A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)*\.[a-z]{2,4}
    • 简写即为:^[\u4e00-\u9fa5-\w]+@[-\w]+(\.[-\w]+)*\.[a-z]{2,4}$

4. 使用

Java程序中使用正则表达式,相关类位于java.util.regex包下面

  • Pattern:
    • 正则表达四的编译表示形式
    • 建立正则表达式,并启用相应模式:Pattern pattern = Pattern.compile(Regular Expression);
  • Matcher:
    • 通过解释Patterncharacter squence执行匹配操作的引擎
    • 匹配str字符串:Matcher matcher = pattern.matcher(str);
    • 将整个字符串序列与该模式匹配:boolean res1 = matcher.matches();
    • 扫描字符串序列,查找与该模式匹配的下一个子序列:boolean res2 = matcher.find();

4.1 基本操作

代码

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
37
38
39
40
41
42
43
44
45
46
47
48
package top.parak;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author: KHighness
* @date: 2020/10/7 15:23
* @apiNote: 基本操作
*/

public class Demo1 {
public static void main(String[] args) {
// 正则匹配模式
Pattern pattern = Pattern.compile("\\w+");

// 匹配操作引擎
Matcher matcher = pattern.matcher("KHighness||ParaK||FlowerK||18236763");

// 完整匹配
System.out.println("----------完整匹配----------");
boolean res = matcher.matches();
System.out.println("匹配结果:" + res + ",匹配字符串:" + (res ? matcher.group() : "NULL"));

// 每次操作完引擎中的字符串都会到达末尾,需要重新写
matcher = pattern.matcher("KHighness||ParaK||FlowerK||18236763");

// 子串匹配
System.out.println("----------子串匹配----------");
boolean res1 = matcher.find();
System.out.println("匹配结果:" + res1 + ",匹配字符串:" + (res1 ? matcher.group() : "NULL"));
boolean res2 = matcher.find();
System.out.println("匹配结果:" + res2 + ",匹配字符串:" + (res2 ? matcher.group() : "NULL"));
boolean res3 = matcher.find();
System.out.println("匹配结果:" + res3 + ",匹配字符串:" + (res3 ? matcher.group() : "NULL"));
boolean res4 = matcher.find();
System.out.println("匹配结果:" + res4 + ",匹配字符串:" + (res4 ? matcher.group() : "NULL"));
boolean res5 = matcher.find();
System.out.println("匹配结果:" + res5 + ",匹配字符串:" + (res5 ? matcher.group() : "NULL"));

// 循环查找
matcher = pattern.matcher("KHighness||ParaK||FlowerK||18236763");
System.out.println("----------循环匹配----------");
while (matcher.find()) {
System.out.println(matcher.group());
}
}
}

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
----------完整匹配----------
匹配结果:false,匹配字符串:NULL
----------子串匹配----------
匹配结果:true,匹配字符串:KHighness
匹配结果:true,匹配字符串:ParaK
匹配结果:true,匹配字符串:FlowerK
匹配结果:true,匹配字符串:18236763
匹配结果:false,匹配字符串:NULL
----------循环匹配----------
KHighness
ParaK
FlowerK
18236763

4.2 分组操作

代码

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
package top.parak;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author: KHighness
* @date: 2020/10/7 15:46
* @apiNote: 分组操作
*/

public class Demo2 {
public static void main(String[] args) {
// 正则匹配模式
// 分组两组匹配:字母和数字
Pattern pattern = Pattern.compile("([A-Za-z]+)([0-9]+)");
// 匹配操作引擎
Matcher matcher = pattern.matcher("KHighness18||ParaK23||FlowerK67||KAG63||KAG72");

int index = 1;
while (matcher.find()) {
System.out.printf("-----第%d组-----\n", index++);
System.out.println("group[0]: " + matcher.group(0));
System.out.println("group[1]: " + matcher.group(1));
}
}
}

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-----第1组-----
group[0]: KHighness18
group[1]: KHighness
-----第2组-----
group[0]: ParaK23
group[1]: ParaK
-----第3组-----
group[0]: FlowerK67
group[1]: FlowerK
-----第4组-----
group[0]: KAG63
group[1]: KAG
-----第5组-----
group[0]: KAG72
group[1]: KAG

4.3 替换操作

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package top.parak;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author: KHighness
* @date: 2020/10/7 16:09
* @apiNote: 替换操作
*/

public class Demo3 {
public static void main(String[] args) {
// 正则匹配模式
Pattern pattern = Pattern.compile("[0-9]");
// 匹配操作引擎
Matcher matcher = pattern.matcher("KHighness18||ParaK23||FlowerK67||KAG63||KAG72");

// 数字全部替换为##
String newStr = matcher.replaceAll("#");
System.out.println(newStr);
}
}

运行

1
KHighness##||ParaK##||FlowerK##||KAG##||KAG##

4.4 分割操作

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package top.parak;

import java.util.Arrays;

/**
* @author: KHighness
* @date: 2020/10/7 16:12
* @apiNote: 分割操作
*/

public class Demo4 {
public static void main(String[] args) {
String str = "K18H23I67G63H72N18E23S67S6372";
String[] arr = str.split("\\d+"); // 以数字为边界进行切割
System.out.println(Arrays.toString(arr));
}
}

运行

1
[K, H, I, G, H, N, E, S, S]

4.5 爬取腾讯官网所有的超链接

代码

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package top.parak;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author: KHighness
* @date: 2020/10/7 16:24
* @apiNote: 爬取腾讯官网的所有超链接
*/

public class WebSpider {

/**
* 通过URL获取html
* @param urlStr
* @return
*/
public String getURLContent(String urlStr) {
StringBuilder stringBuilder = new StringBuilder();
try {
URL url = new URL(urlStr);
BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), Charset.forName("UTF-8")));
String temp = "";
while ((temp = reader.readLine()) != null) {
stringBuilder.append(temp + "\n");
}
} catch (MalformedURLException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
}
return stringBuilder.toString();
}

/**
* 将匹配结果装进list
* @param destStr
* @param regex
* @return
*/
public List<String> getMatcherSubs(String destStr, String regex) {
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(destStr);
List<String> result = new LinkedList<String>();
while (matcher.find()) {
result.add(matcher.group(1));
}
return result;
}

public static void main(String[] args) {
String url = "https://www.tencent.com/zh-cn";
WebSpider spider = new WebSpider();
// 获取超链接标签a的内容 <a\s\S]+?</a>
// 获取href的内容 href=\"(.+?)\"
spider.getMatcherSubs(spider.getURLContent(url), "href=\\\"([\\w\\s./:]+?)\\\"").stream().forEach(System.out::println);
}
}

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
/css/base.css
/css/index.css
https://weibo.com/tencent
https://twitter.com/TencentGlobal
https://www.linkedin.com/company/tencent/
https://careers.tencent.com/
https://join.qq.com/
https://spd.tencent.com/portal
https://ipr.tencent.com/
http://beian.miit.gov.cn/
http://beian.miit.gov.cn/
http://beian.miit.gov.cn/
/css/rem.css