目录
正则表达式(regular expression)
元字符
字符匹配符、预定义字符类
选择匹配符
限定符
定位符
非命名分组、命名分组
特别分组(了解)
两个常用类 :Pattern、Matcher
分组、捕获、反向引用
String 类中使用正则表达式
练习题
关于优化
如何避免NFA自动机回溯问题?
1.贪婪模式(Greedy)
2.懒惰模式(Reluctant)“?”
3.独占模式(Possessive)
五.正则表达式的优化
注意:正则表达式中空格不能随便用!!
public static void main(String[] args) {
//假定,编写了爬虫,从百度页面得到了如下文本
String content = "1995年,互联网的蓬勃发展给了Oak机会。业界为了使死板、单调的静态网页能够" +
"“灵活”起来,急需一种软件技术来开发一种程序,这种程序可以通过网络传播并且能够跨平台运行。" +
"于是,世界各大IT企业为此纷纷投入了大量的人力、物力和财力。这个时候,Sun公司想起了那个...." ;
//1.创建一个Pattern 模式对象,可以理解成正则表达式对象
//Pattern pattern = Pattern.compile("[a-zA-Z]+"); //找英文
//Pattern pattern = Pattern.compile("[1-9]+"); //找数字
Pattern pattern = Pattern.compile("([1-9]+)|([a-zA-Z]+)"); //找英文和数字
//2.创建一个匹配器对象
// Matcher按照 pattern 去匹配,传入 String字符串
Matcher matcher = pattern.matcher(content);
//3.循环匹配
while (matcher.find()){
System.out.println("找到:"+matcher.group(0));
}
}
public static void main(String[] args) {
String content = "1998年12月8日,第二代Java平台的企业版J2EE发布。1999年6月," +
"Sun公司发布了第二代Java平台(简称为Java2)的3个版本:J2ME(Java2 Mi" +
"cro Edition,Java2平台的微型版),应用于移动、无线及有限资源的环境;J" +
"2SE(Java 2 Standard Edition,Java 2平台的标准版),应用于桌面环境" +
";J2EE(Java 2Enterprise Edition,Java 2平台的企业版),应用于基...";
//目标:匹配所有的四个数字
//说明
//1. \d 表示任意的数字
String regStr = "(\\d\\d)(\\d\\d)"; //分为2组 正则表达式中()表示分组
//2. 创建一个模式对象
Pattern pattern = Pattern.compile(regStr);
//3. 创建匹配器
//说明: 创建匹配器matcher,按照 正则表达式的规则 去匹配 content 字符串
Matcher matcher = pattern.matcher(content);
//4.开始匹配
/**
* matcher.find() 完成的任务 (考虑分组)
* 1. 根据指定的规则,定位满足规则的子字符串(比如(19)(98)),
* 2. 找到后,将子字符串的开始索引记录在 matcher对象的 int[] groups;中,
* 2.1 groups[0] = 0 ,把该子字符串结束的 index 记录到 groups[1] = 4;
* 2.2 记录第一组()匹配到的字符串 groups[2] = 0 groups[3] = 2
* 2.3 记录第二组()匹配到的字符串 groups[4] = 2 groups[5] = 4
* 3. 同时记录 oldLast 的值为字符串结束的 index,
* 4.下次执行find时,就从 oldLast 开始匹配
*
* matcher.group(0) 分析
* 1. 根据 groups[0] 和 groups[1] 的记录的位置,从content开始截取子字符串返回
* [0,4)
*
* 如果再次执行 find方法,仍然按上面的分析执行
* */
while (matcher.find()){
System.out.println("找到: "+matcher.group(0));
System.out.println("第一组()匹配到的值="+matcher.group(1));
System.out.println("第二组()匹配到的值="+matcher.group(2));
//System.out.println("第三组()匹配到的值="+matcher.group(3)); 越界
}
}
小结
1. 如果正则表达式使用() 分组
2. matcher.find() 方法会记录每组起始与结尾的 index
3. matcher.group(0) 表示匹配到的整体的子字符串
4. matcher.group(1) 表示匹配到的第一组()
5. matcher.group(2) 表示匹配到的第二组()
6. ... 但是不能不能越界(大于分组数) IndexOutOfBoundsException
geoup源码:
public String group(int group) {
if (first < 0)
throw new IllegalStateException("No match found");
if (group < 0 || group > groupCount())
throw new IndexOutOfBoundsException("No group " + group);
if ((groups[group*2] == -1) || (groups[group*2+1] == -1))
return null;
return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();
}
+表示1个或多个,?表示0个或1个,*表示0或多个
POSIX 字符类(仅 US-ASCII)
\p{Lower}
小写字母字符:[a-z]
\p{Upper}
大写字母字符:[A-Z]
\p{ASCII}
所有 ASCII:[\x00-\x7F]
\p{Alpha}
字母字符:[\p{Lower}\p{Upper}]
\p{Digit}
十进制数字:[0-9]
\p{Alnum}
字母数字字符:[\p{Alpha}\p{Digit}]
\p{Punct}
标点符号:!"#$%&'()*+,-./:;<=>?@[]^_`{|}~
\p{Blank}
空格或制表符:[ \t]
public static void main(String[] args) {
String content = "a11c8_abcABC @";
String regStr1 = "[a-z]"; //匹配a-z之间的任意字符
String regStr2 = "[A-Z]"; //匹配A-Z之间的任意字符
String regStr3 = "abc"; //匹配 abc 字符串,[默认匹配大小写]
String regStr4 = "(?i)abc"; //匹配 abc 字符串,[不区分大小写]
String regStr5 = "[0-9]"; //匹配 0-9 之间任意一个字符
String regStr6 = "[!0-9]"; //匹配 不在 0-9 之间任意一个字符
String regStr7 = "\\D"; //匹配 不在 0-9 之间任意一个字符
String regStr8 = "\\w"; //匹配 大小写字母、数字、下划线
String regStr9 = "\\W"; //匹配 等价于[^a-zA-Z0-9]
String regStr0 = "\\s"; //匹配 空白字符(空格,制表符)
String regStrA = "\\S"; //匹配 非空白字符
String regStrB = "."; //匹配 除\n之外所有字符,如果要匹配. 需要用\\.
Pattern pattern = Pattern.compile(regStr);
// Pattern.CASE_INSENSITIVE 表示不区分字母大小写
//Pattern pattern = Pattern.compile(regStr,Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(content);
while (matcher.find()){
System.out.println("找到:"+ matcher.group(0));
}
}
String content = "茴香豆的四种写法:回 囘 囬 廻";
String regStr = "回|囘|囬|廻"; //| 选择匹配
//String regStr = "[回囘囬廻]"; 相同效果??
Pattern compile = Pattern.compile(regStr);
Matcher matcher = compile.matcher(content);
while (matcher.find()){
System.out.println("找到:"+matcher.group(0));
}
注意:当 ? 紧跟其他限定符匹配模式时,匹配模式为“懒惰模式” 详见(关于优化)
String content = "111113ka1saaaaaaHello";
String regStr1 = "a{3}"; //等于"aaa";
String regStr2 = "1{4}"; //等于"1111";
String regStr3 = "\\d{2}"; //匹配2位的任意数字;
细节:java匹配默认贪婪匹配,尽可能匹配多的
String regStr4 = "a{3,4}"; //匹配aaa或aaaa;
//关闭贪婪模式 开启懒惰模式
String regStr5 = "a{3,4}?";
String regStr6 = "\\d{2,5}"; //匹配2-5位数字组合
//1+
String regStr7 = "1+"; //匹配一个1或者多个1
//关于?的使用,遵守贪婪匹配
String regStr8 = "a1?"; //匹配 a 或者 a1
Pattern compile = Pattern.compile(regStr);
Matcher matcher = compile.matcher(content);
while (matcher.find()){
System.out.println("找到:"+matcher.group(0));
}
}
public static void main(String[] args) {
String content = "123abc hanshunping sphan nnhan";
//至少1数字开头,后接任意小写字母
String regStr1 = "^[0-9]+[a-z]*";
//至少1数字开头,至少一个小写字母结尾
String regStr2 = "^[0-9]+[a-z]+$";
//匹配边界的han 边界:匹配到字符串的最后、空格的子字符串最后
String regStr3 = "han\\b";
//匹配边界的han 开头的 , 与\\b相反
String regStr4 = "han\\B";
Pattern compile = Pattern.compile(regStr3);
Matcher matcher = compile.matcher(content);
while (matcher.find()){
System.out.println("找到:"+matcher.group(0));
}
}
public static void main(String[] args) {
String content = "hanshunping s7789 nn1189han";
//非命名分组 通过matcher.group(?)获取
String regStr = "(\\d\\d)(\\d\\d)";
//分组命名 可通过matcher.group(组名)获取
String regStr1 = "(?\\d\\d)(?\\d\\d)";
Pattern compile = Pattern.compile(regStr1);
Matcher matcher = compile.matcher(content);
while (matcher.find()){
System.out.println("找到:"+matcher.group(0));
System.out.println("第1个分组内容:"+matcher.group(1));
System.out.println("第2个分组内容:"+matcher.group(2));
//命名分组
System.out.println("第1个分组内容:"+matcher.group("g1"));
System.out.println("第2个分组内容:"+matcher.group("g2"));
}
}
group 重载方法 可通过组数、组名获取匹配字符串
public static void main(String[] args) {
String content1 = "hello韩顺平教育 jack韩顺平老师 韩顺平同学hello";
String regStr1 = "韩顺平教育|韩顺平老师|韩顺平同学";
//非捕获分组 等价于上面的写法
String regStr2 = "韩顺平(?:教育|老师|同学)";
Pattern compile = Pattern.compile(regStr2);
Matcher matcher = compile.matcher(content1);
while (matcher.find()){
System.out.println("找到:"+matcher.group(0));
//非捕获,所以会报错
//System.out.println("找到:"+matcher.group(1));
}
}
public void test(){
String content1 = "hello韩顺平教育 jack韩顺平老师 韩顺平同学hello";
//只查找韩顺平教育、韩顺平老师
String regStr3 = "韩顺平(?=教育|老师|同学)";
//不查找韩顺平教育、韩顺平老师
String regStr4 = "韩顺平(?!教育|老师)";
Pattern compile = Pattern.compile(regStr3);
Matcher matcher = compile.matcher(content1);
while (matcher.find()){
System.out.println("找到:"+matcher.group(0));
//非捕获,所以会报错
//System.out.println("找到:"+matcher.group(1));
}
}
Pattern 类
Matcher |
matcher(CharSequence input) 创建匹配给定输入与此模式的匹配器。 |
static boolean |
matches(String regex, CharSequence input) 整体匹配 编译给定正则表达式并尝试将给定输入与其匹配。 |
String[] |
split(CharSequence input) 围绕此模式的匹配拆分给定输入序列。 |
String[] |
split(CharSequence input, int limit) 围绕此模式的匹配拆分给定输入序列。 |
Matcher 类
boolean |
find() 尝试查找与该模式匹配的输入序列的下一个子序列。 |
boolean |
find(int start) 重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。 |
String |
group() 返回由以前匹配操作所匹配的输入子序列。 |
String |
group(int group) 返回在以前匹配操作期间由给定组捕获的输入子序列。 |
String |
replaceAll(String replacement) 替换模式与给定替换字符串相匹配的输入序列的每个子序列。 |
int |
start() 返回以前匹配的初始索引。 |
int |
start(int group) 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引。 |
int |
end() 返回最后匹配字符之后的偏移量。 |
int |
end(int group) 返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。 |
int |
groupCount() 返回此匹配器模式中的捕获组数。 |
public static void main(String[] args) {
String content = "我....我要....学学学学....编程java!";
//1. 去掉所有的.
Pattern pattern = Pattern.compile("\\.");
Matcher matcher = pattern.matcher(content);
content = matcher.replaceAll("");
System.out.println("1.content: " + content);
//2.去掉重复的字
//思路
//(1) 使用(.)\\1+ 匹配重复的字
pattern = Pattern.compile("(.)\\1+");
matcher = pattern.matcher(content);
while (matcher.find()){
System.out.println(matcher.group(0));
}
//(2) 使用反向引用 替换匹配到的内容 "我我" 替换成 "我" 内部反向引用\\租号 外部引用$组号
content = matcher.replaceAll("$1");
System.out.println("2.content: " + content);
//简化代码 "我我" 替换成 "我" "学学学学" 替换成 "学"
String $1 = Pattern.compile("(.)\\1").matcher(content).replaceAll("$1");
}
String |
replaceAll(String regex, String replacement) 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 |
String |
replaceFirst(String regex, String replacement) 使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 |
boolean |
matches(String regex) (整体匹配) 底层仍然使用Pattern 和 Matcher 告知此字符串是否匹配给定的正则表达式。 |
String[] |
split(String regex) 根据给定正则表达式的匹配拆分此字符串。 |
String[] |
split(String regex, int limit) 根据匹配给定的正则表达式来拆分此字符串。 |
String content = "2000年5月,JDK1.3、JDK1.4和J2SE1.3相继发布" +
",几周后其获得了Apple公司Mac OS X的工业标准的支持。" +
"2001年9月24日,J2EE1.3发布。2002年2月26日,J2SE1.4" +
"发布。自此Java的计算能力有了大幅提升";
//1.使用正则表达式 将JDK1.3 和 JDK1.4 替换成 JDK
content = content.replaceAll("JDK1.3|JDK1.4","JDK");
System.out.println(content);
//2.要求 验证一个 手机号,要求必须是以 138,139 开头的
content = "13933330123";
if (content.matches("1(38|39)\\d{8}")) {
System.out.println("验证成功");
}else {
System.out.println("验证失败");
}
//3.要求按照 # 或者 - 或者 ~ 或者 数字 来分割
content = "hello#abc-jack12smith~北京";
String[] split = content.split("[#~-] | \\d+");
System.out.println(Arrays.toString(split));
//验证电子邮件 只能有一个 @
//1、只能有一个@
//2、@前面可以是a-z A-Z 0-9_字符
//3、@后面是域名,并且域名只能是英文字母,比如sohu.com 或者 tsinghua.org.cn
//4、写出对应的正则表达式,验证输入的字符串是否为满足规则
public static void main(String[] args) {
String mail = "[email protected]";
String regStr = "[\\w]+@([a-zA-Z]+\\.)+(com|cn)";
//String 的 matches 是整体匹配,所以可以不带^与$
if(mail.matches(regStr)){
System.out.println("匹配成功");
}
}
//要求验证是不是整数或者小数
//提示: 考虑正数和负数 小数 不合理整数
//比如 123 - 345 34.89 -87.9 -0.01 0.45 000.12 等
@Test
public void test (){
//解题思路:由简入繁
String content = "-1.231"; //00.89 如何规范:[1-9开头]
//注意:正则表达式中空格不能随便用!!!
String regStr = "[-]?([1-9]\\d*|0)(\\.\\d+)?";
if(content.matches(regStr)){
System.out.println("匹配成功");
}
}
//对一个url进行解析
//http://www.sohu.com:8080/abc/index.htm
//1: 要求得到协议是什么 http
//2: 域名是什么? www.shohu.com
//3: 端口是什么? 8080
//4: 文件名是什么? index.htm
@Test
public void test1(){
String content = "http://www.sohu.com:8080/abc/xxx/yyy/index.htm";
//思路: 分组 分为4组 依次判断 关键: 找到定位的符号 :/ : /
String regStr = "^([a-zA-Z]+)://([a-zA-Z.]+):(\\d+)[\\w-/]*/([\\w.]+)$";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
if(matcher.matches()){ //整体匹配,如果成功可以通过group(x)获取对应分组内容
System.out.println("整体匹配=" + matcher.group(0));
System.out.println("协议:" + matcher.group(1));
System.out.println("域名:" + matcher.group(2));
System.out.println("端口:" + matcher.group(3));
System.out.println("文件:" + matcher.group(4));
}else {
System.out.println("匹配失败");
}
}
既然回溯会给系统带来性能开销,那我们如何应对呢?如果你有仔细看上面那个案例的话,你会发现 NFA 自动机的贪婪特性就是导火索,这和正则表达式的匹配模式息息相关。
顾名思义,就是在数量匹配中,如果单独使用 +、?、*或(min,max)等量词,正则表达式会匹配尽可能多的内容。
例如,上面那个例子:
text = "abbc"
regex = "ab{1,3}c"
就是在贪婪模式下,NFA自动机读取了最大的匹配范围,即匹配 3 个 b 字符。匹配发生了一次失败,就引起了一次回溯。如果匹配结果是“abbbc”,就会匹配成功。
text = "abbbc"
regex = "ab{1,3}c"
在该模式下,正则表达式会尽可能少地重复匹配字符,如果匹配成功,它会继续匹配剩余的字符串。
例如,上面的例子的字符后面加一个“?”,就可以开启懒惰模式。
text = "abc"
regex = "ab{1,3}?c"
匹配结果是“abc”,该模式下 NFA 自动机首先选择最小的匹配范围,即匹配 1 个 b 字符,因此就避免了回溯问题。
同贪婪模式一样,独占模式一样会最大限度地匹配更多内容;不同的是,在独占模式下,匹配失败就会结束匹配,不会发生回溯问题。
还是上面的例子,在字符后面加一个“+”,就可以开启独占模式。
text = "abbc"
regex = "ab{1,3}+c"
结果是不匹配,结束匹配,不会发生回溯问题。
所以综上所述,避免回溯的方法就是:使用懒惰模式或独占模式。
前面讲述了“Split() 方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题。”,比如使用了 split 方法提取域名,并检查请求参数是否符合规定。
split 在匹配分组时遇到特殊字符产生了大量回溯,解决办法就是在正则表达式后加一个需要匹配的字符和“+”解决了回溯问题:
\\?(([A-Za-z0-9-~_=%]++\\&{0,1})+)
1.少用贪婪模式:多用贪婪模式会引起回溯问题,可以使用独占模式来避免回溯。
2.减少分支选择:分支选择类型 “(X|Y|Z)” 的正则表达式会降低性能,在开发的时候要尽量减少使用。如果一定要用,可以通过以下几种方式来优化:
1)考虑选择的顺序,将比较常用的选择项放在前面,使他们可以较快地被匹配;
2)可以尝试提取共用模式,例如,将 “(abcd|abef)” 替换为 “ab(cd|ef)” ,后者匹配速度较快,因为 NFA 自动机会尝试匹配 ab,如果没有找到,就不会再尝试任何选项;
3)如果是简单的分支选择类型,可以用三次 index 代替 “(X|Y|Z)” ,如果测试话,你就会发现三次 index 的效率要比 “(X|Y|Z)” 高一些。推荐:Java面试练题宝典
3.减少捕获嵌套 :
捕获组是指把正则表达式中,子表达式匹配的内容保存到以数字编号或显式命名的数组中,方便后面引用。一般一个()就是一个捕获组,捕获组可以进行嵌套。
非捕获组则是指参与匹配却不进行分组编号的捕获组,其表达式一般由(?:exp)组成。
在正则表达式中,每个捕获组都有一个编号,编号 0 代表整个匹配到的内容。可以看看下面的例子:
public static void main(String[] args) {
String text = "test";
String reg = "()(.*?)()";
Pattern p = Pattern.compile(reg);
Matcher m = p.matcher(text);
while (m.find()){
System.out.println(m.group(0));//整个匹配到的内容
System.out.println(m.group(1));//
System.out.println(m.group(2));//(.*?)
System.out.println(m.group(3));//()
}
}
//=====运行结果=====
//test
//
//test
//
如果你并不需要获取某一个分组内的文本,那么就使用非捕获组,例如,使用 “(?:x)” 代替 “(X)” ,例如下面的例子:
public static void main(String[] args) {
String text = "test";
String reg = "(?:)(.*?)(?:)";
Pattern p = Pattern.compile(reg);
Matcher m = p.matcher(text);
while (m.find()) {
System.out.println(m.group(0));//整个匹配到的内容
System.out.println(m.group(1));//(.*?)
}
}
//=====运行结果=====
//test
//test
关于优化:
谈谈正则表达式的性能优化问题_Java笔记虾的博客-CSDN博客
学习教程:【韩顺平讲Java】Java 正则表达式专题 -正则 正则表达式 元字符 限定符 Pattern Matcher 分组 捕获 反向引用等_哔哩哔哩_bilibili