Java 文本检索神器 “正则表达式”
每博一文案
在我们短促而又漫长的一生中,我们在苦苦地寻找人生的幸福,可幸福往往又与我们失之交臂,
当我们为此而耗尽宝贵的。青春年华,皱纹也悄悄地爬上了眼角的时候,我们或许才能悄悄懂得生活实际上意味
着什么。
—————— 《平凡的世界》
叶赛宁的诗,不惋惜,不呼唤,我也不啼哭。金黄的落叶堆满我心间。我已经再不是青春少年。
—————— 《平凡的世界》
生活中有那么一种人,你蔑视甚至污辱他,他不仅视为正常,还对你挺佩服;你要是在人格上对他平等相待,
他反而倒小看你!这种人的情况,在伟鲁迅的不朽著作中详尽诠释,这里就不再赘述。
—————— 《平凡的世界》
一个平平常常的日子,细蒙蒙的雨丝夹着一星半点的雪花,正纷纷淋淋地向大地飘洒着。时令已快到惊蛰,
雪当然再也不会存留,往往还没等落地,就已经消失得无影无踪了。黄土高原严寒而漫长的冬天看来
就要过去,但那真正温暖的春天还远远地没有到来。
—————— 《路遥》
文章目录
- Java 文本检索神器 "正则表达式"
- 每博一文案
- 1. 正则表达式的概述
- 1.1 正则表达式的威力
- 2. Java中正则表达式的相关类
- 2.1 Pattern 类
- 2.1.1 Pattern 类中的一些标识属性
- 2.2 Matcher 类
- 2.2.1 索引方法
- 2.2.2 检查方法
- 2.2.3 替换方法
- 2.3 PatternSyntaxException异常类
- 2.4 String 类
- 2.4.1 java.lang.String 与 java.util.regex.Pattern等效的几个方法
- 2.4.2 java.lang.String 与 java.util.regex.Matcher 等效的几个方法
- 3. 正则表达式的语法
- 3.1 元字符: (\\\\) 转义字符
- 3.2 元字符:字符匹配符
- 3.3 元字符正则表达式:不区分大小写的二种方式
- 3.4 元字符:选择匹配符号
- 3.5 元字符:限定符
- 3.6 贪婪匹配(Greedy)/ 非贪婪匹配(Lazy)
- 3.6.1 贪婪匹配(Greedy)
- 3.6.2 非贪婪匹配(Lazy)
- 3.7 元字符:定位符
- 4. 正则表达式:分组 / 特殊的分组
- 4.1 编号分组
- 4.2 命名分组
- 4.3 特殊分组 / 不保存子组
- 4.4 分组中的括号嵌套
- 5. 正则表达式的实现的原理分析:
- 5.1 正则表达式中不含分组的原理分析
- 5.2 正则表达式中含分组的原理分析
- 6. 正则表达式:分组,捕获,反向引用
- 7. 正则表达式:案例练习讲解
- 8. 正则表达式:优化建议
- 8.1 尽量准确表示匹配范围
- 8.2 提取出公共部分
- 8.3 出现可能性大的放左边
- 8.4 只在必要时才使用子组
- 8.5 警惕嵌套的子组重复
- 8.6 避免不同分支重复匹配
- 9. 资料:常用的正则表达式格式
- 10. 总结
- 11. 最后:
1. 正则表达式的概述
我相信,作为一名程序员或者准程序员,你肯定是知道正则表达式的。作为计算机领域最伟大的发明之一,正则表达式简单、强大,它可以极大地提高我们工作中的文本处理效率。现在,各大操作系统、编程语言、文本编辑器都已经支持正则表达式,甚至我还和极客时间的编辑开玩笑说,他们也应该好好学学正则这门手艺。
正则,就是正则表达式,英文是 Regular Expression,简称 RE。 顾名思义,正则其实就是一种 描述文本内容组成规律的表示方式。
在编程语言中,正则常常用来简化文本处理的逻辑。在 Linux 命令中,它也可以帮助我们轻松地查找或编辑文件的内容,甚至实现整个文件夹中所有文件的内容替换,比如 grep、egrep、sed、awk、vim 等。另外,在各种文本编辑器中,比如 Atom,Sublime Text 或 VS Code 等,在查找或替换的时候也会使用到它。总之,正则是无处不在的,已经渗透到了日常工作的方方面面。
简单来说,正则是一个非常强大的文本处理工具,它的应用极其广泛。我们可以利用它来校验数据的有效性,比如用户输入的手机号是不是符合规则;也可以从文本中提取想要的内容,比如从网页中抽取数据;还可以用来做文本内容替换,从而得到我们想要的内容。
通过它的功能和分布的广泛你也能看出来,正则是一个非常值得花时间和精力好好学习的基本技能。之前你花几十分钟才能搞定的事情,可能用正则很快就搞定了;之前不能解决的问题,你系统地学习正则后,可能发现也能轻松解决了。
1.1 正则表达式的威力
初始正则表达式,这里我们先来体会一下正则表达式的威力。
例如01: 下面这段文本是,我们通过爬虫获取到的,下面我们通过正则表达式获取到其中文本的所有英文单词
Java平台由Java虚拟机(Java Virtual Machine)和Java 应用编程接口(Application Programming Interface、简称API)构成。Java 应用编程接口为Java应用提供了一个独立于操作系统的标准接口,可分为基本部分和扩展部分。在硬件或操作系统平台上安装一个Java平台之后,Java应用程序就可运行。Java平台已经嵌入了几乎所有的操作系统。这样Java程序可以只编译一次,就可以在各种系统中运行。Java应用编程接口已经从1.1x版发展到1.2版。常用的Java平台基于Java1.8最近版本为Java19。
package blogs.blog12;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest01 {
public static void main(String[] args) {
// 假定,编写了爬虫,从百度页面得到如下文本:
String content = "Java平台由Java虚拟机(Java Virtual Machine)和Java 应用编程接口(Application Programming Interface、简称API)构成。Java 应用编程接口为Java应用提供了一个独立于操作系统的标准接口," +
"可分为基本部分和扩展部分。在硬件或操作系统平台上安装一个Java平台之后," +
"Java应用程序就可运行。Java平台已经嵌入了几乎所有的操作系统。" +
"这样Java程序可以只编译一次,就可以在各种系统中运行。" +
"Java应用编程接口已经从1.1x版发展到1.2版。常用的Java平台基于Java1.8," +
"最近版本为Java19。";
// 提取文章中所有的英文单词:
// 传统方法: 使用遍历方式: 代码量大,效率不高:
// 正则表达式:
// 1. 先创建一个Pattern 对象,模式对象,可以理解成就是一个正则表达式对象
Pattern pattern = Pattern.compile("[a-zA-Z]+");
// 2. 创建一个匹配器对象
// 理解:就是 matcher 匹配器,按照所编写的 pattern(模式/样式) ,到 content 文本中去匹配
// 找到就返回 true,否则就返回false
Matcher matcher = pattern.matcher(content);
// 3. 开始循环匹配
while(matcher.find()) { // 找到返回 true,否则返回false
// 匹配内容,文本,放到 matcher.group() 当中
String group = matcher.group(0);
System.out.println(group);
}
}
}
例如02: 提取到上述文本内容中的所有 数字 。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest01 {
public static void main(String[] args) {
// 假定,编写了爬虫,从百度页面得到如下文本:
String content = "Java平台由Java虚拟机(Java Virtual Machine)和Java 应用编程接口(Application Programming Interface、简称API)构成。Java 应用编程接口为Java应用提供了一个独立于操作系统的标准接口," +
"可分为基本部分和扩展部分。在硬件或操作系统平台上安装一个Java平台之后," +
"Java应用程序就可运行。Java平台已经嵌入了几乎所有的操作系统。" +
"这样Java程序可以只编译一次,就可以在各种系统中运行。" +
"Java应用编程接口已经从1.1x版发展到1.2版。常用的Java平台基于Java1.8," +
"最近版本为Java19。";
// 1.创建一个正则表达式对象
Pattern pattern = Pattern.compile("[\\d]+");
// 2.创建一个匹配器,用于匹配符合正则表达式的字符串
Matcher matcher = pattern.matcher(content);
// 循环遍历获取符合条件的字符串
while(matcher.find()) { // 一点一点的同文本中遍历匹配是否符合该正则表达式,符合返回true,否则返回false
String group = matcher.group(0);
System.out.println(group);
}
}
}
例如03 : 如下是一段从 www.baidu.com
网页中截取的一段源码:通过正则表达式获取到其中的百度热搜信息 。
name="oq" value=""><input type="hidden" name="rsv_pq" value="0xb79379dd001c441c"><input type="hidden" name="rsv_t" value="6ab2PWJNZs4OfZhgLwgMJI9gVsivBq7kMA9T8vJRItmCHeBsBD0QsnashhrZ"><input type="hidden" name="rqlang" value="en"></form><div id="m" class="under-searchbox-tips s_lm_hide "><div id="lm-new"></div></div><div id="s-hotsearch-wrapper" class="s-isindex-wrap s-hotsearch-wrapper hide "><div class="s-hotsearch-title"><a class="hot-title" href="https://top.baidu.com/board?platform=pc&sa=pcindex_entry" target="_blank"><div class="title-text c-font-medium c-color-t" aria-label="百度热搜"><i class="c-icon"></i><i class="c-icon arrow"></i></div></a><a id="hotsearch-refresh-btn" class="hot-refresh c-font-normal c-color-gray2"><i class="c-icon refresh-icon"></i><span class="hot-refresh-text">换一换</span></a></div><ul class="s-hotsearch-content" id="hotsearch-content-wrapper"><li class="hotsearch-item odd" data-index="0"><a class="title-content c-link c-font-medium c-line-clamp1" href="https://www.baidu.com/s?wd=%E4%BA%8C%E5%8D%81%E5%B1%8A%E4%BA%8C%E4%B8%AD%E5%85%A8%E4%BC%9A%E5%BC%80%E5%A7%8B%E4%B8%BE%E8%A1%8C&sa=fyb_n_homepage&rsv_dl=fyb_n_homepage&from=super&cl=3&tn=baidutop10&fr=top1000&rsv_idx=2&hisfilter=1" target="_blank" ><div class="title-content-noindex" style="display: none;"></div><i class="c-icon title-content-top-icon c-color-red c-gap-right-small" style="display: ;"></i><span class="title-content-index c-index-single c-index-single-hot0" style="display: none;">0</span><span class="title-content-title">二十届二中全会开始举行</span></a><span class="title-content-mark ie-vertical c-text c-gap-left-small "></span></li><li class="hotsearch-item even" data-index="3"><a class="title-content c-link c-font-medium c-line-clamp1" href="https://www.baidu.com/s?wd=%E7%AB%99%E4%B8%8A%E6%96%B0%E8%B5%B7%E7%82%B9+%E5%A5%8B%E5%8A%9B%E5%BC%80%E6%96%B0%E5%B1%80&sa=fyb_n_homepage&rsv_dl=fyb_n_homepage&from=super&cl=3&tn=baidutop10&fr=top1000&rsv_idx=2&hisfilter=1" target="_blank" ><div class="title-content-noindex" style="display: none;"></div><i class="c-icon title-content-top-icon c-color-red c-gap-right-small" style="display: none;"></i><span class="title-content-index c-index-single c-index-single-hot3" style="display: ;">3</span><span class="title-content-title">站上新起点 奋力开新局</span></a><span class="title-content-mark ie-vertical c-text c-gap-left-small "></span></li><li class="hotsearch-item odd" data-index="1"><a class="title-content tag-width c-link c-font-medium c-line-clamp1" href="https://www.baidu.com/s?wd=%E7%94%B7%E5%AD%90%E5%9C%A8%E8%87%AA%E5%AE%B6%E9%B1%BC%E5%A1%98%E7%94%B5%E9%B1%BC%E8%A2%AB%E7%BD%9A+%E8%AD%A6%E6%96%B9%E9%81%93%E6%AD%89&sa=fyb_n_homepage&rsv_dl=fyb_n_homepage&from=super&cl=3&tn=baidutop10&fr=top1000&rsv_idx=2&hisfilter=1" target="_blank" ><div class="title-content-noindex" style="display: none;"></div><i class="c-icon title-content-top-icon c-color-red c-gap-right-small" style="display: none;"></i><span class="title-content-index c-index-single c-index-single-hot1" style="display: ;">1</span><span class="title-content-title">男子在自家鱼塘电鱼被罚 警方道歉</span></a><span class="title-content-mark ie-vertical c-text c-gap-left-small c-text-hot">热</span></li><li class="hotsearch-item even" data-index="4"><a class="title-content c-link c-font-medium c-line-clamp1" href="https://www.baidu.com/s?wd=%E4%BA%94%E4%B8%80%E5%81%87%E6%9C%9F%E5%8F%AF%E4%BB%A5%E6%8B%BC%E5%87%BA9%E5%A4%A9%E9%95%BF%E5%81%87&sa=fyb_n_homepage&rsv_dl=fyb_n_homepage&from=super&cl=3&tn=baidutop10&fr=top1000&rsv_idx=2&hisfilter=1" target="_blank" ><div class="title-content-noindex" style="display: none;"></div><i class="c-icon title-content-top-icon c-color-red c-gap-right-small" style="display: none;"></i><span class="title-content-index c-index-single c-index-single-hot4" style="display: ;">4</span><span class="title-content-title">五一假期可以拼出9天长假</span></a><span class="title-content-mark ie-vertical c-text c-gap-left-small "></span></li><li class="hotsearch-item odd" data-index="2"><a class="title-content tag-width c-link c-font-medium c-line-clamp1" href="https://www.baidu.com/s?wd=%E5%A6%BB%E5%AD%90%E6%83%B3%E5%8D%96%E6%88%BF%E6%95%91%E5%84%BF%E9%81%AD%E4%B8%88%E5%A4%AB%E5%8F%8D%E5%AF%B9+%E6%80%92%E6%8F%90%E7%A6%BB%E5%A9%9A&sa=fyb_n_homepage&rsv_dl=fyb_n_homepage&from=super&cl=3&tn=baidutop10&fr=top1000&rsv_idx=2&hisfilter=1" target="_blank" ><div class="title-content-noindex" style="display: none;"></div><i class="c-icon title-content-top-icon c-color-red c-gap-right-small" style="display: none;"></i><span class="title-content-index c-index-single c-index-single-hot2" style="display: ;">2</span><span class="title-content-title">妻子想卖房救儿遭丈夫反对 怒提离婚</span></a><span class="title-content-mark ie-vertical c-text c-gap-left-small c-text-hot">热</span></li><li class="hotsearch-item even" data-index="5"><a class="title-content c-link c-font-medium c-line-clamp1" href="https://www.baidu.com/s?wd=%E8%B0%81%E5%9C%A8%E9%B9%A4%E5%B2%97%E5%B0%8F%E5%9F%8E%E5%96%9D38%E5%85%83%E4%B8%80%E6%9D%AF%E7%9A%84%E5%92%96%E5%95%A1&sa=fyb_n_homepage&rsv_dl=fyb_n_homepage&from=super&cl=3&tn=baidutop10&fr=top1000&rsv_idx=2&hisfilter=1" target="_blank" ><div class="title-content-noindex" style="display: none;"></div><i class="c-icon title-content-top-icon c-color-red c-gap-right-small" style="display: none;"></i><span class="title-content-index c-index-single c-index-single-hot5" style="display: ;">5</span><span class="title-content-title">谁在鹤岗小城喝38元一杯的咖啡</span></a><span class="title-content-mark ie-vertical c-text c-gap-left-small "></span></li></ul></div><textarea id="hotsearch_data" style="display:none;">
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest01 {
public static void main(String[] args) {
String content = "name=\"oq\" value=\"\"><input type=\"hidden\" name=\"rsv_pq\"" +
" value=\"0xb79379dd001c441c\"><input type=\"hidden\" name=\"rsv_t\" valu" +
"e=\"6ab2PWJNZs4OfZhgLwgMJI9gVsivBq7kMA9T8vJRItmCHeBsBD0QsnashhrZ\"><inp" +
"ut type=\"hidden\" name=\"rqlang\" value=\"en\"></form><div id=\"m\" clas" +
"s=\"under-searchbox-tips s_lm_hide \"><div id=\"lm-new\"></div></div><div" +
" id=\"s-hotsearch-wrapper\" class=\"s-isindex-wrap s-hotsearch-wrapper hid" +
"e \"><div class=\"s-hotsearch-title\"><a class=\"hot-title\" href=\"https:/" +
"/top.baidu.com/board?platform=pc&sa=pcindex_entry\" target=\"_blank\"><div " +
"class=\"title-text c-font-medium c-color-t\" aria-label=\"百度热搜\"><i cla" +
"ss=\"c-icon\"></i><i class=\"c-icon arrow\"></i></div></a><" +
"a id=\"hotsearch-refresh-btn\" class=\"hot-refresh c-font-normal c-color-gra" +
"y2\"><i class=\"c-icon refresh-icon\"></i><span class=\"hot-refresh-" +
"text\">换一换</span></a></div><ul class=\"s-hotsearch-content\" id=\"hotsearc" +
"h-content-wrapper\"><li class=\"hotsearch-item odd\" data-index=\"0\"><a cla" +
"ss=\"title-content c-link c-font-medium c-line-clamp1\" href=\"https://www.b" +
"aidu.com/s?wd=%E4%BA%8C%E5%8D%81%E5%B1%8A%E4%BA%8C%E4%B8%AD%E5%85%A8%E4%BC%" +
"9A%E5%BC%80%E5%A7%8B%E4%B8%BE%E8%A1%8C&sa=fyb_n_homepage&rsv_dl=fy" +
"b_n_homepage&from=super&cl=3&tn=baidutop10&fr=top1000&" +
";rsv_idx=2&hisfilter=1\" target=\"_blank\" ><div class=\"title-content" +
"-noindex\" style=\"display: none;\"></div><i class=\"c-icon title-content-t" +
"op-icon c-color-red c-gap-right-small\" style=\"display: ;\"></i><s" +
"pan class=\"title-content-index c-index-single c-index-single-hot0\" style=" +
"\"display: none;\">0</span><span class=\"title-content-title\">二十届二中全会" +
"开始举行</span></a><span class=\"title-content-mark ie-vertical c-text c-g" +
"ap-left-small \"></span></li><li class=\"hotsearch-item even\" data-index=\"" +
"3\"><a class=\"title-content c-link c-font-medium c-line-clamp1\" href=\"h" +
"ttps://www.baidu.com/s?wd=%E7%AB%99%E4%B8%8A%E6%96%B0%E8%B5%B7%E7%82%B9+%E5" +
"%A5%8B%E5%8A%9B%E5%BC%80%E6%96%B0%E5%B1%80&sa=fyb_n_homepage&rsv_dl" +
"=fyb_n_homepage&from=super&cl=3&tn=baidutop10&fr=top1000&am" +
"p;rsv_idx=2&hisfilter=1\" target=\"_blank\" ><div class=\"title-content" +
"-noindex\" style=\"display: none;\"></div><i class=\"c-icon title-content-t" +
"op-icon c-color-red c-gap-right-small\" style=\"display: none;\"></" +
"i><span class=\"title-content-index c-index-single c-index-single-hot3\" st" +
"yle=\"display: ;\">3</span><span class=\"title-content-title\">站上新起点 奋" +
"力开新局</span></a><span class=\"title-content-mark ie-vertical c-text c-gap" +
"-left-small \"></span></li><li class=\"hotsearch-item odd\" data-index=\"1\"" +
"><a class=\"title-content tag-width c-link c-font-medium c-line-clamp1\" hr" +
"ef=\"https://www.baidu.com/s?wd=%E7%94%B7%E5%AD%90%E5%9C%A8%E8%87%AA%E5%AE%" +
"B6%E9%B1%BC%E5%A1%98%E7%94%B5%E9%B1%BC%E8%A2%AB%E7%BD%9A+%E8%AD%A6%E6%96%B9" +
"%E9%81%93%E6%AD%89&sa=fyb_n_homepage&rsv_dl=fyb_n_homepage&from" +
"=super&cl=3&tn=baidutop10&fr=top1000&rsv_idx=2&hisfilte" +
"r=1\" target=\"_blank\" ><div class=\"title-content-noindex\" style=\"displ" +
"ay: none;\"></div><i class=\"c-icon title-content-top-icon c-color-red c-ga" +
"p-right-small\" style=\"display: none;\"></i><span class=\"title-c" +
"ontent-index c-index-single c-index-single-hot1\" style=\"display: ;\">1<" +
"/span><span class=\"title-content-title\">男子在自家鱼塘电鱼被罚 警方道歉</sp" +
"an></a><span class=\"title-content-mark ie-vertical c-text c-gap-left-smal" +
"l c-text-hot\">热</span></li><li class=\"hotsearch-item even\" data-index=" +
"\"4\"><a class=\"title-content c-link c-font-medium c-line-clamp1\" href=" +
"\"https://www.baidu.com/s?wd=%E4%BA%94%E4%B8%80%E5%81%87%E6%9C%9F%E5%8F%AF" +
"%E4%BB%A5%E6%8B%BC%E5%87%BA9%E5%A4%A9%E9%95%BF%E5%81%87&sa=fyb_n_homep" +
"age&rsv_dl=fyb_n_homepage&from=super&cl=3&tn=baidutop10&am" +
"p;fr=top1000&rsv_idx=2&hisfilter=1\" target=\"_blank\" ><div class" +
"=\"title-content-noindex\" style=\"display: none;\"></div><i class=\"c-ico" +
"n title-content-top-icon c-color-red c-gap-right-small\" style=\"display: " +
"none;\"></i><span class=\"title-content-index c-index-single c-ind" +
"ex-single-hot4\" style=\"display: ;\">4</span><span class=\"title-content-" +
"title\">五一假期可以拼出9天长假</span></a><span class=\"title-content-mark " +
"ie-vertical c-text c-gap-left-small \"></span></li><li class=\"hotsearch-i" +
"tem odd\" data-index=\"2\"><a class=\"title-content tag-width c-link c-font" +
"-medium c-line-clamp1\" href=\"https://www.baidu.com/s?wd=%E5%A6%BB%E5%AD%90" +
"%E6%83%B3%E5%8D%96%E6%88%BF%E6%95%91%E5%84%BF%E9%81%AD%E4%B8%88%E5%A4%AB%E5%" +
"8F%8D%E5%AF%B9+%E6%80%92%E6%8F%90%E7%A6%BB%E5%A9%9A&sa=fyb_n_homepage&am" +
"p;rsv_dl=fyb_n_homepage&from=super&cl=3&tn=baidutop10&fr=to" +
"p1000&rsv_idx=2&hisfilter=1\" target=\"_blank\" ><div class=\"title-c" +
"ontent-noindex\" style=\"display: none;\"></div><i class=\"c-icon title-content-" +
"top-icon c-color-red c-gap-right-small\" style=\"display: none;\"></i><spa" +
"n class=\"title-content-index c-index-single c-index-single-hot2\" style=\"displ" +
"ay: ;\">2</span><span class=\"title-content-title\">妻子想卖房救儿遭丈夫反对 怒提离" +
"婚</span></a><span class=\"title-content-mark ie-vertical c-text c-gap-left-small" +
" c-text-hot\">热</span></li><li class=\"hotsearch-item even\" data-index=\"5\"><a" +
" class=\"title-content c-link c-font-medium c-line-clamp1\" href=\"https://www.ba" +
"idu.com/s?wd=%E8%B0%81%E5%9C%A8%E9%B9%A4%E5%B2%97%E5%B0%8F%E5%9F%8E%E5%96%9D38%E5%" +
"85%83%E4%B8%80%E6%9D%AF%E7%9A%84%E5%92%96%E5%95%A1&sa=fyb_n_homepage&rsv_dl=f" +
"yb_n_homepage&from=super&cl=3&tn=baidutop10&fr=top1000&rsv_idx=2" +
"&hisfilter=1\" target=\"_blank\" ><div class=\"title-content-noindex\" style=\"di" +
"splay: none;\"></div><i class=\"c-icon title-content-top-icon c-color-red c-gap-right" +
"-small\" style=\"display: none;\"></i><span class=\"title-content-index c-ind" +
"ex-single c-index-single-hot5\" style=\"display: ;\">5</span><span class=\"title-cont" +
"ent-title\">谁在鹤岗小城喝38元一杯的咖啡</span></a><span class=\"title-content-mark ie-v" +
"ertical c-text c-gap-left-small \"></span></li></ul></div><textarea id=\"hotsearch_da" +
"ta\" style=\"display:none;\">";
// 找规律:编程正则表达式
// <span class="title-content-title">二十届二中全会开始举行</span></a>
// <span class="title-content-title">站上新起点 奋力开新局</span></a>
// 1. 创建正则表达式对象
String reg = "[\\d]</span><span class=\"title-content-title\">(\\S*\\s?\\S*)</span></a>";
Pattern pattern = Pattern.compile(reg);
// 2. 创建匹配器对象,用于匹配判断
Matcher matcher = pattern.matcher(content);
// 循环匹配,匹配返回true,否则返回false
while(matcher.find()) {
String group = matcher.group(1); // 分组
System.out.println(group);
}
}
}
2. Java中正则表达式的相关类
java.util.regex
包主要包括三类: Pattern
、Matcher
和 PatternSyntaxException
。
-
Pattern 对象是正则表达式的编译标识。没有提供公共的构造。 要创建一个模式,您必须首先调用其中一个
public static compile
方法,然后返回一个Pattern
对象。 这些方法接受正则表达式作为第一个参数。 -
Matcher 对象是解析器和针对输入字符串执行匹配操作的发动机。像 Patter 类一样,也没有定义公共的构造函数, Matcher 通过调用对象上的 matcher 方法来获取 Pattern 对象。
-
PatternSyntaxException 对象是一个未经检查的异常,其指示在正则表达式模式中的语法错误。
2.1 Pattern 类
该类没有定义构造器,所以不可以通过 new 的方式创建对象,而是通过调用其中的 Pattern.comile(String regex)
创建该正则表达式的对象。
public static Pattern compile(String regex); // regex 为正则表达式的格式;
// 返回该正则表达式的 Pattern 对象。
public Matcher matcher(CharSequence input); // CharSequence input 就是一个需要匹配的字符串。
// 返回一个 Matcher 匹配器对象。
2.1.1 Pattern 类中的一些标识属性
在 Pattern 类定义的替代 compile,它接受一组影响匹配的方式的标志方法。flags 参数是一个位掩码,可以包含以下任何公共静态字段:
-
Pattern.CANON_EQ
启用规范等价。指定此标志后,当且仅当其完整规范分解匹配时,两个字符才可视为匹配。 例如,当指定此标志时,表达式 “a\u030A” 将与字符串 “\u00E5” 匹配。默认情况下,匹配不考虑采用规范等价。指定此标志可能会造成性能损失。
-
Pattern.CASE_INSENSITIVE
启用不区分大小写的匹配。默认情况下,不区分大小写的匹配假定仅匹配 US-ASCII 字符集中的字符。 可以通过指定 UNICODE_CASE 标志连同此标志来启用 Unicode 感知的、不区分大小写的匹配。 通过嵌入式标志表达式 (?i) 也可以启用不区分大小写的匹配。 指定此标志可能会造成轻微的性能损失。
-
Pattern.COMMENTS
模式中允许空白和注释。 此模式将忽略空白和在结束行之前以 # 开头的嵌入式注释。 通过嵌入式标志表达式 (?x) 也可以启用注释模式。
-
Pattern.DOTALL
启用点阵模式。在 dotall 模式下,表达式
.
匹配任何字符,包括行终止符。默认情况下, 此表达式与行终止符不匹配。Dotall 模式也可以通过嵌入式标志表达式启用(?s)。(s 是“单行”模式的助记符,这在 Perl 中也被使用)。 -
Pattern.LITERAL
启用模式的文字解析。当指定此标志时,指定模式的输入字符串将被视为文字字符序列。输入序列中的元字符或转义序列将没有特殊意义。当与此标志一起使用时,标志 CASE_INSENSITIVE 和 UNICODE_CASE 保留对匹配的影响。其他旗帜变得多余。没有嵌入的标志字符用于启用文字解析。
-
Pattern.MULTILINE
启用多行模式。在多行模式中,表达式^和$匹配恰好在之前或之前分别是行终止符或输入序列的结尾。 默认情况下,这些表达式仅在整个输入序列的开头和结尾匹配。也可以通过嵌入式标志表达式启用多模式模式(?m)。
-
Pattern.UNICODE_CASE
启用 Unicode 感知的大小写折叠。当指定此标志时,不区分大小写的匹配(由 CASE_INSENSITIVE 标志启用)以与 Unicode 标准一致的方式完成。 默认情况下,不区分大小写的匹配假定仅匹配 US-ASCII 字符集中的字符。Unicode 感知案例折叠也可以通过嵌入式标志表达式启用(?u)。 指定此标志可能会造成性能损失。
-
Pattern.UNIX_LINES
启用 UNIX 线路模式。在这种模式下,只有’\n’ 行结束在行为的认可.,^ 和 $。 UNIX 线路模式也可以通过嵌入式标志表达式启用(?d)。
如下是一些 Pattern 类中常用的方法:
public static boolean matches(String regex,CharSequence input); //判断该字符串中是否有符合该正则表达式的子字符串;有返回 true,没有返回 false。
//CharSequence 是一个接口,其中String 类实现了该接口。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest02 {
/**
* public static boolean matches(String regex,CharSequence input) 方法
*/
public static void main(String[] args) {
String content = "jfdasij123";
boolean matches = Pattern.matches("\\w*123", content);
System.out.println(matches);
boolean matches2 = Pattern.matches("\\d", "1");
System.out.println(matches2);
}
}
public String[] split(CharSequence input); // 根据给定的正则表达式分割,将分割后的字符串存储到String数组中
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest02 {
/**
*public String[] split(CharSequence input) 分割字符串
*/
public static void main(String[] args) {
String content = "one:two:three:four:five";
// 1.创建正则表达式对象
Pattern pattern = Pattern.compile(":"); // 以 : 分割
String[] split = pattern.split(content);
for (String regStr : split) {
System.out.println(regStr);
}
}
}
split 方法是一种很好的工具,用于收集位于匹配模式两侧的文本。如下示例,该 split 方法可以从字符串“one2️⃣three4️⃣five” 中提取单词 “one two three four five”。
-
public static String quote(String s)
返回指定 String 的字面值模式 String,此方法产生一个 String,可以将其用于创建与字符串 s 匹配的 Pattern, 就好像它是字面值模式一样。输入序列中的元字符和转义序列不具有任何特殊意义。
尝试了下,没有搞懂是啥,返回的全是 “
\Qxxx\E
” 的串 -
public String toString()
返回 String 此模式的表示。这是编译此模式的正则表达式。
2.2 Matcher 类
该类同样也是没有公开的构造器调用的,所以同样也是不能 new 对象的。需要通过 调用pattern.matcher()
方法返回一个 Mathcer 对象。
public Matcher matcher(CharSequence input); // CharSequence input 就是一个需要匹配的字符串。
// 返回一个 Matcher 匹配器对象。
如下是 Matcher 类一些常用的方法
2.2.1 索引方法
索引方法提供了有用的索引值,它们精确地显示了输入字符串中匹配的位置:
public int start()
:返回上一个匹配的起始索引。public int start(int group)
:返回上次匹配操作期间给定组捕获的子序列的起始索引。public int end()
:返回最后一个字符匹配后的偏移量。public int end(int group)
:返回在上一次匹配操作期间由给定组捕获的子序列的最后一个字符之后的偏移量。
start 和 end 方法的使用:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest02 {
/**
* matcher 中的 start / end 的使用
*/
public static void main(String[] args) {
String content = "hello hello fdafd hello";
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile("\\bhello\\b");
// 2. 创建匹配器对象
Matcher matcher = pattern.matcher(content);
// 循环匹配
while(matcher.find()) {
System.out.print("start: " + matcher.start()); // 返回所匹配的子字符串的起始下标位置
System.out.print("\t"+"end: " + matcher.end()); // 返回所匹配的子字符串的结束下标位置 + 1
System.out.println();
}
}
}
2.2.2 检查方法
public boolean lookingAt()
:尝试将输入序列从区域开头开始与模式相匹配。public boolean find()
:尝试找到匹配模式的输入序列的下一个子序列。public boolean find(int start)
:重置此匹配器,然后尝试从指定的索引开始找到与模式匹配的输入序列的下一个子序列。public boolean matches()
:尝试将整个区域与模式进行匹配。
使用 matches 和 lookingAt 方法
matches 和 lookingAt 方法都尝试将输入序列与模式进行匹配。然而,差异在于,matches 要求整个输入序列匹配, lookingAt 而不需要。两种方法始终从输入字符串的开头开始。
- lookingAt : 只要所匹配对象的字符串中含有满足该正则表达式中的规则的子字符串,则返回 true,若一个都没有满足该正则表达式的子串,才返回 false。
- matches : 只有所匹配对象的字符串整体都满足该正则表达式中的规则,才返回 true,否则返回 false。
package blogs.blog12;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest02 {
/**
* 使用 matches 和 lookingAt 方法
*/
public static void main(String[] args) {
String content = "hiiiiiiiiiii";
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile("hi");
// 2. 创建匹配器对象
Matcher matcher = pattern.matcher(content);
System.out.println("lookingAt():" + matcher.lookingAt()); // true 只要其中匹配的字符串存在满足该正则表达式的规则返回true,
System.out.println("matches():" + matcher.matches()); // false 必须要该匹配的字符串整体满足正则表达式的规则才返回 true
}
}
2.2.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)
:返回 String 指定的文字替换 String。该方法产生一个在类的方法中 String 作为文字替换 s 的 appendReplacement 方法 Matcher。所产生的字符串 s 将作为字面序列处理。斜杠('\'
)和美元符号(‘$’)将没有特殊意义。
使用 replaceFirst(String) and replaceAll(String)
- replaceFirst(String) : 替换满足给定的正则表达式的第一个子字符串的内容。
- replaceAll(String) : 替换满足给定的正则表达式的所有子字符串的内容。
- 注意: 这两者都不会修改其原本的字符串内容,而是返回一个替换成功后的字符串。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest02 {
/**
* 使用 replaceFirst(String) and replaceAll(String)
*/
public static void main(String[] args) {
String content = "hello world hello world";
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile("hello");
// 2. 创建匹配器对象
Matcher matcher = pattern.matcher(content);
String replaceAll = matcher.replaceAll("world"); // 替换全部
System.out.println("replaceAll: " + replaceAll);
String replaceFirst = matcher.replaceFirst("world"); // 替换第一个
System.out.println("replaceFirst: " + replaceFirst);
}
}
再看:
在第一个版本中,所有出现的代码 hello 都被替换 world。但为什么要停在这里?而不是替换一个简单的文字 hello, 您可以替换匹配任何正则表达式的文本。该方法的 API 指出,“给定正则表达式 a*b
,输入aabfooaabfooabfoob
和替换字符串 -
, 在该表达式的匹配器上调用此方法将产生字符串 -foo-foo-foo-
。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest02 {
/**
* 使用 replaceFirst(String) and replaceAll(String)
*/
public static void main(String[] args) {
String content = "aabfooaabfooabfoob";
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile("a*b");
// 2. 创建匹配器对象
Matcher matcher = pattern.matcher(content);
String replaceAll = matcher.replaceAll("-"); // 替换全部
System.out.println("replaceAll: " + replaceAll);
String replaceFirst = matcher.replaceFirst("-"); // 替换第一个
System.out.println("replaceFirst: " + replaceFirst);
}
}
a*b
表示 ab 或则 b 都符合条件,但是是贪婪量词,会出现长度零匹配的结果。所以就出现了上面的输出效果
Matcher 类还提供 appendReplacement 和 appendTail 文本替换方法。使用这两种方法来实现与之相同效果的 replaceAll。
2.3 PatternSyntaxException异常类
PatternSyntaxException 是未经检查的异常,指示正则表达式模式中的语法错误。PatternSyntaxException 类提供了以下方法来帮助你确定是什么出了问题:
public String getDescription()
:检索错误的描述。public int getIndex()
:检索错误索引。public String getPattern()
:检索错误的正则表达式模式。public String getMessage()
:返回一个多行字符串,其中包含语法错误及其索引的描述,错误的正则表达式模式以及模式中错误索引的可视指示。
2.4 String 类
2.4.1 java.lang.String 与 java.util.regex.Pattern等效的几个方法
java.lang.String
通过几种模仿行为的方法也存在正则表达式支持 java.util.regex.Pattern
中。为方便起见,他们的 API 的关键摘录如下。
-
public boolean matches(String regex)
:告诉这个字符串是否匹配给定的正则表达式。这种形式的这种方法的调用产生与表达式完全相同的结果。
str.matches(regex) 和 Pattern.matches(regex, str)
-
public String[] split(String regex, int limit)
:将此字符串拆分为给定正则表达式的匹配项。这种形式的方法的调用产生与表达式相同的结果
str.split(regex, n) 和 Pattern.compile(regex).split(str, n)
-
public String[] split(String regex)
:将此字符串拆分为给定则表达式的匹配项。此方法的工作方式与使用给定表达式和极限参数为零的双参数拆分方法相同。尾随的空字符串不包含在结果数组中。
还有一个替换方法,替换CharSequence另一个:
-
public String replace(CharSequence target,CharSequence replacement)
:将与字面目标序列匹配的字符串的每个子字符串替换为指定的字面替换序列。替换从字符串开始到结束,例如,在字符串“aaa”中用“b”替换“aa”将导致“ba”而不是“ab”。
2.4.2 java.lang.String 与 java.util.regex.Matcher 等效的几个方法
为了方便起见,String 该类也模仿了几种 Matcher 方法:
public String replaceFirst(String regex, String replacement)
:用给定的替换替换与给定正则表达式匹配的此字符串的第一个子字符串。这种形式的这种方法的调用产生与表达式完全相同的结果str.replaceFirst(regex, repl)
和Pattern.compile(regex).matcher(str).replaceFirst(repl)
public String replaceAll(String regex, String replacement)
:用给定的替换替换与给定正则表达式匹配的此字符串的每个子字符串。这种形式的这种方法的调用产生与表达式完全相同的结果str.replaceAll(regex, repl)
和Pattern.compile(regex).matcher(str).replaceAll(repl)
3. 正则表达式的语法
3.1 元字符: (\\) 转义字符
首先我们说一下什么是 转义字符(Escape Character)。它在维基百科中是这么解释的:
在计算机科学与远程通信中,当转义字符放在字符序列中,它将对它后续的几个字符进行替代并解释。通常,判定某字符是否为转义字符由上下文确定。转义字符即标志着转义序列开始的那个字符。
这么说可能有点不好理解,我再来给你通俗地解释一下。转义序列通常有两种功能。第一种功能是编码无法用字母表直接表示的特殊数据。第二种功能是用于表示无法直接键盘录入的字符(如回车符)。
在Java中对于一些特殊的字符的匹配,判断是 需要通过转义得到其真正的含义的。
注意:在Java当中两个 \\
才表示 一个 \
需要用到转义符号的字符有大致有以下一些: {*,+,(),$,/,\,?,[],^,{},}
举例: “匹配 (
” 左括号,匹配 “$
” 美元符号。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest03 {
public static void main(String[] args) {
String content = "abc$(abc123c)";
// 匹配 (创建正则表达式对象
Pattern pattern = Pattern.compile("\\("); // (需要使用转义,不然编译都无法通过 \\ 表示\
// 匹配器
Matcher matcher = pattern.matcher(content);
// 循环匹配
while(matcher.find()) {
String group = matcher.group(0); // 整体没有分组的情况
System.out.println(group);
}
}
}
举例: 匹配"."
单个 点符号,如果不使用转义的话,单个点具有表示任意字符的意义。
特殊的当 "."
是在 [.]
方括号中表示的就是 .
本身了,不需要转义。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest03 {
public static void main(String[] args) {
String content = "abc.$(abc123c).";
// 匹配 (创建正则表达式对象
Pattern pattern = Pattern.compile("."); // 单个未转义的"."表示任意字符
// 匹配器
Matcher matcher = pattern.matcher(content);
// 循环匹配
while(matcher.find()) {
String group = matcher.group(0); // 整体没有分组的情况
System.out.println(group);
}
}
}
使用 “\\.
” 转义后: 注意:这里其实一个 \
就是表示转义的意思了,但是Java中想要表示一个 \
就需要用两个 \\
表示。
3.2 元字符:字符匹配符
注意: 如下的转义字符实际上是 单个 "\
"的,但是因为在 Java中想要表示单个 斜杆“\” 就需要两个 "\\"
斜杆表示一个斜杆。
符号 | 作用 | 示例 | 解释 |
---|---|---|---|
[ ] | 可接受匹配的字符列表 | [abcd] | 可以匹配a,b,c,d 中的任意 1 个字符 |
[^ ] | ^表示取反的意思,不接受的字符列表 | [^abc] | 除了a,b,c 之外的任何 1 个字符,包括数字和特殊符号 |
- | 连字符:表示之间的意思 | A-Z | 表示任意 大写的 A到 Z 之间的字符。说白了就是任意大写字母 |
. | 单个“点”:表示匹配除了 “\n” 以外的任何单个字符 | a…b | 以 a 开头,b 结尾,中间包含2 个任意字符(除了“\n”) 长度为 4 的字符串。例如:aaab,aefb,a35b,a#*b |
\\d | 匹配单个数字字符,相当于 [0-9] | \\d{3}(\\d)? | 包含3个或4个数字的字符串(其中的 ? 表示 0 个或1个)。例如:123,9876。 |
\\D | 匹配 单 个非数字字符,相当于[^0-9] | \\D(\\d)* | 以 单 个非数字字符开头,后接任意个数字字符串(其中的 * 表示 0 个或 n 个)。例如:a,A123 |
\\w | 小写 w 匹配单 个数字,大小写字母字符以及一个 “_” 下划线相当于[0-9a-zA-Z_ ] | \\d{3}\\w{4} | 以 3 个数字字符开头后接4个数字/大小写字母/下划线的 长度为 7 的字符串。例如:234abcd,12345Pe,123___1 |
\\W | 大写的 W 匹配 单 个非数字,大小写字母字符,相当于 [^0-9a-zA-Z_ ] | \\W+\\d{2} | 以至少1个非数字非字母字符开头的,2个数字字符结尾的字符串(其中的 + 表示 1 个或 n 个)。例如:@29,#?@10。 |
\\s | 小写的 s 表示:匹配任何空白字符(空格,制表符等) | ||
\\S | 大写的 S 表示:匹配任何非空白字符和 \\s 刚好相反 | ||
\\n | 匹配出 \n |
举例: 匹配三个连续的数字字符:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 字符匹配符的使用
*/
public class RegularTest04 {
public static void main(String[] args) {
String content = "adc 999fajoi111 fajoidfj000";
// 编写正则表达式
String regStr = "\\d\\d\\d"; // \\d 表示[0-9]的数字字符
// 或者
String regStr2 = "\\d{3}"; // 表示三个连续的数字字符
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile(regStr);
// 2. 创建匹配器对象
Matcher matcher = pattern.matcher(content);
// 循环匹配
while(matcher.find()) {
String group = matcher.group(0);
System.out.println(group);
}
}
}
3.3 元字符正则表达式:不区分大小写的二种方式
在Java中正则表示式的匹配机制是:默认是区别字母的大小写的 。
首先,我们来看一下不区分大小写模式。它有什么用呢?学一个知识的时候,我一般喜欢先从它的应用出发,这样有时候更能激发我学习的兴趣,也更容易看到学习成果。
下面我来举个例子说明一下。在进行文本匹配时,我们要关心单词本身的意义。比如要查找单词 cat,我们并不需要关心单词是 CAT、Cat,还是 cat。根据之前我们学到的知识,你可能会把正则写成这样:[Cc][Aa][Tt]
,这样写虽然可以达到目的,但不够直观,如果单词比较长,写起来容易出错,阅读起来也比较困难。
方式一:
在正则表达式中存在一个模式修饰符 :放在整个正则前面时,就表示整个正则表达式都是不区分大小写的。模式修饰符是通过 (? 模式标识) 的方式来表示的。常用的有如下三种处理方式。
(?i)abc
: 表示abc都不区分大小写。a(?i)bc
: 表示 bc 不区分大小写,但是 a 是区分大小写的。ab(?i)c
:表示只有 c 是不区分大小写的。a((?i)b)c
: 表示只有 b 是不区分大小写的。
注意: (?i)
模式修饰符,修饰的是后面的字符内容,不是前面的,被修饰的字符匹配时,是忽略大小写的。可以通过括号进行单个的修饰隔离。
举例:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 字符匹配符的使用
*/
public class RegularTest04 {
public static void main(String[] args) {
String content = "abc ABC aBC aBc Abc";
// 1. 创建对应的正则表达式对象
Pattern pattern1 = Pattern.compile("(?i)abc"); // abc 都忽略大小写
Pattern pattern2 = Pattern.compile("a(?i)bc"); // bc 忽略大小写
Pattern pattern3 = Pattern.compile("a((?i)b)c"); // 只有 b 是忽略大小写的
// 2. 创建对应的 匹配器对象
Matcher matcher1 = pattern1.matcher(content);
// 3. 循环匹配
System.out.print("abc 都忽略大小写: ");
while (matcher1.find()) {
System.out.print(" " + matcher1.group());
}
Matcher matcher2 = pattern2.matcher(content);
System.out.print("\nbc 忽略大小写: ");
while (matcher2.find()) {
System.out.print(" " + matcher2.group());
}
Matcher matcher3 = pattern3.matcher(content);
System.out.print("\n只有 b 是忽略大小写: ");
while (matcher3.find()) {
System.out.print(" " + matcher3.group());
}
System.out.println();
}
}
方式二:
通过 在创建 Pattern 正则表达式对象时,设置 忽略大小写。核心代码如下:
public static Pattern compile(String regex,int flags;// 将给定的正则表达式编译到具有给定标志的模式中。
Pattern.CASE_INSENSITIVE
启用不区分大小写的匹配。默认情况下,不区分大小写的匹配假定仅匹配 US-ASCII 字符集中的字符。 可以通过指定 UNICODE_CASE 标志连同此标志来启用 Unicode 感知的、不区分大小写的匹配。 通过嵌入式标志表达式 (?i) 也可以启用不区分大小写的匹配。 指定此标志可能会造成轻微的性能损失。
举例:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 字符匹配符的使用
*/
public class RegularTest04 {
public static void main(String[] args) {
String content = "abc Abc aBc ABC abC";
// 1. 创建正则表达式对象,并设置忽略大小写
Pattern pattern = Pattern.compile("abc",Pattern.CASE_INSENSITIVE);
// 2. 创建匹配器
Matcher matcher = pattern.matcher(content);
// 3.循环匹配
while(matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
}
3.4 元字符:选择匹配符号
在匹配某个字符串的时候是选择性的,即:既可以匹配这个,又可以匹配那个,这时你需要用到选择匹配符号 "|"
。
符号 | 作用 | 示例 | 解释 |
---|---|---|---|
| | 匹配 “|” 前后的表达式 | ab|cd | 匹配 ab 或者 cd |
举例:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 选择匹配符 |
*/
public class RegularTest05 {
public static void main(String[] args) {
String content = "你 您 拟 ";
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile("你|您");
// 2. 创建匹配器
Matcher matcher = pattern.matcher(content);
// 3.循环匹配
while(matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
}
3.5 元字符:限定符
用于指定前面的字符和组合项连续出现多少次。
符号 | 作用 | 示例 | 解释 |
---|---|---|---|
* | 指定字符重复 0 次 或 n 次 : 0 到多 | (abc)* | 仅包含任意个abc的字符串。例如:abc,abcabcabc |
+ | 指定字符重复 1 次或 n次: 至少一次 | m+(abc)* | 以至少m个字符开头(1或多个m字母),后接任意个 abc的字符串。例如:m,mabc,mabcabc |
? | 指定字符重复 0 次或 1 次:最多1次 | m+abc? | 以至少m个字符开头(1或多个m字母),后接ab或abc的字符串。例如:mab,mabc,mmmab。(注意 ? 修饰邻近的字符) |
{n} | 只能输入n个字符 | [abcd]{3} | 由abcd中字母组成的任意长度为3的字符串。例如:abc,dbc,abc |
{n,} | 指定至少 n 个匹配 | [abcd]{3,} | 由 abcd中字母组成的任意长度不小于3 的字符串,最多没有设置。例如:aab,dbc,aaabdc |
{n,m} | 指定至少 n 个但不多于 m 个匹配 | [abcd]{3,5} | 由abcd中字母组成的任意长度不小于3,不大于5 的字符串。例如:abc,abcd,aaaaa,bcdab |
举例:
package blogs.blog12;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 限定符
*/
public class RegularTest06 {
public static void main(String[] args) {
String content = "a111111";
//String regStr = "1+"; // 匹配一个或多个
//String regStr = "\\d+"; // 匹配一个数字或者多个数字
//String regStr = "1*"; // 匹配任意多个
String regStr = "a1?"; // 匹配a 或者 a1 , 0 个或 1 个
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile(regStr);
// 2. 创建匹配器
Matcher matcher = pattern.matcher(content);
// 3.循环匹配
while(matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
}
3.6 贪婪匹配(Greedy)/ 非贪婪匹配(Lazy)
3.6.1 贪婪匹配(Greedy)
首先,我们来看一下贪婪匹配。在正则中,表示次数的量词 默认是贪婪的,在贪婪模式下,会尝试尽可能最大长度去匹配。
首先,我们来看一下在字符串 aaabb 中使用正则 a*
的匹配过程。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 限定符
*/
public class RegularTest06 {
public static void main(String[] args) {
String content = "aaabb ";
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile("a*");
// 2. 创建匹配器
Matcher matcher = pattern.matcher(content);
// 3.循环匹配
while(matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
}
字符串 | aaabb |
---|---|
下标 | 012345 |
匹配 | 开始 | 结束 | 说明 | 匹配内容 |
---|---|---|---|---|
第 1 次 | 0 | 3 | 到第一个字母 b 发现不满足,输出 aaa | aaa |
第 2 次 | 3 | 3 | 匹配剩下的 bb,发现匹配不上,输出空字符串 | 空字符串 |
第 3 次 | 4 | 4 | 匹配剩下的 b,发现匹配不上,输出空字符串 | 空字符串 |
第 4 次 | 5 | 5 | 匹配剩下的空字符串,输出空字符串 | 空字符串 |
a*
在匹配开头的 a 时,会尝试尽量匹配更多的 a,直到第一个字母 b 不满足要求为止,匹配上三个 a,后面每次匹配时都得到了空字符串。
相信看到这里你也发现了,贪婪模式的特点就是尽可能进行最大长度匹配。所以要不要使用贪婪模式是根据需求场景来定的。如果我们想尽可能最短匹配呢?那就要用到非贪婪匹配模式了。
3.6.2 非贪婪匹配(Lazy)
那么如何将贪婪模式变成非贪婪模式呢?我们 可以在量词后面加上英文的问号 (?
),正则就变成了 a*?
。此时的匹配结果
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 限定符
*/
public class RegularTest06 {
public static void main(String[] args) {
String content = "aaabb ";
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile("a+?");
// 2. 创建匹配器
Matcher matcher = pattern.matcher(content);
// 3.循环匹配
while(matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
这一次我们可以看到,这次匹配到的结果都是单个的 a,就连每个 a 左边的空字符串也匹配上了。
到这里你可能就明白了,非贪婪模式会尽可能短地去匹配,我把这两者之间的区别写到了下面这张图中。
3.7 元字符:定位符
定位符,规定要匹配的字符串出现的位置,比如在字符串的开始,还是结束的位置,这个也是相当有用的。
符号 | 作用 | 示例 | 解释 |
---|---|---|---|
^ | 指定起始字符 | 1+[a-z]* | 以至少1个数字开头,后接任意个小写字母的字符串。例如:123,6aa,555edf |
$ | 指定结束字符 | 2\\-[a-z]+$ | 以至少1个数字开头,中间连接字符”-“,并以至少1个小写字母结尾的字符串。例如:1-a |
\\b | 匹配目标字符串的边界。 | han\\b | (这里的边界可以是字符之间的空格/结尾)。例如:sphan nnhan可以匹配到两者中的 han |
\\B | 匹配目标字符串的非边界 | han\\B | 和 \\b 的含义相反。例如:sphan nnhan。只能匹配到 nnhan中的 han |
举例:
package blogs.blog12;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest07 {
public static void main(String[] args) {
String content = "123-abc"; // a123abc 123abc 123abc12,
//String regStr = "^[0-9]+[a-z]*"; // 以至少1个数字开头,后接任意个小写字母的字符串
//String regStr = "^[0-9]+[a-z]+$"; // 以至少1个数字开头,必须以至少一个小写字母界结尾
String regStr = "^[0-9]+\\-[a-z]+$";
// 创建正则对象
Pattern pattern = Pattern.compile(regStr);
// 创建匹配器
Matcher matcher = pattern.matcher(content);
// 循环
while(matcher.find()) {
String group = matcher.group(0);
System.out.println(group);
}
}
}
举例:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest07 {
public static void main(String[] args) {
String content = "hanshunping sphan nnhaan";
//String regStr = "han\\b"; // 表示匹配边界 han ,边界:空格的子字符串的后面
String regStr = "han\\B"; // 仅仅只是字符串的结尾,不含空格
// 创建正则对象
Pattern pattern = Pattern.compile(regStr);
// 创建匹配对象
Matcher matcher = pattern.matcher(content); // 该参数是个接口,String 实现了该接口
// 循环
while(matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
}
4. 正则表达式:分组 / 特殊的分组
常用的分组构造形式:
在正则表达式中使用()
圆括号,括起来的就表示分组了 。
()
非命名捕获/也称为编号分组,捕获匹配的子字符串,编号为 0 的表示第一个分组,第一个捕获是由整个正则表达式模式匹配的文本,其它捕获结果则根据左括号的顺序从 1 开始自动编号。(? \<name> pattern)
: 命令捕获/也称为命名分组,将匹配的子字符串捕获到一个组名称或编号名称中,用于 name 的字符串不能包含任何标点符号,并且不能以数字开头,可以使用单引号代替尖括号。例如 (‘?’ name)。
4.1 编号分组
括号在正则中可以 用于分组,被括号括起来的部分 「子表达式」会被保存成一个 子组。
那分组和编号的规则是怎样的呢?其实很简单,用一句话来说就是,第几个括号就是第几个分组。这么说可能不好理解,我们来举一个例子看一下。
这里有个时间格式 2020-05-10 20:23:05。假设我们想要使用正则提取出里面的日期和时间。
我们可以写出如图所示的正则,将日期和时间都括号括起来。这个正则中一共有两个分组,日期是第 1 个,时间是第 2 个。
注意: 不要分组越界了。就是说不要去访问没有分组的 **group()**的内容。比如说:你只分了两组,分别是 1,2 但是你却访问了一个不存在的分组 3,会报异常:java.lang.IndexOutOfBoundsException:
补充:
public String group(int group); // Matcher 类中的对象方法,获取到分组/捕获到的分组中的内容。
- group(0)/() :表示获取到符合正则表达式中整体的子字符串内容。如果不传参数默认调用的还是 group(0)的,源码中可以看出来如下:
- group(1) :表示获取到从整体符合正则表达式的子字符串的基础上,进行第一个分组中的内容。
- group(2) :表示获取到从整体符合正则表达式的子字符串的基础上,进行第二个分组中的内容。
- group(3) … 第3组,第4组,第5组都是以此类推的。
- 注意不要越界了:不然报:
java.lang.IndexOutOfBoundsException
异常。
举例:
package blogs.blog12;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 分组
*/
public class RegularTest08 {
public static void main(String[] args) {
String content = "2020-05-10 20:23:05";
String regStr = "(\\d{4}-\\d{2}-\\d{2}).(\\d{2}:\\d{2}:\\d{2})";
// 1.创建正则表达式对象
Pattern pattern = Pattern.compile(regStr);
// 2. 创建匹配器对象
Matcher matcher = pattern.matcher(content);
// 3.循环匹配获取
while (matcher.find()) {
String group = matcher.group(0); // 0 / 不传参数,默认获取是整体正则表达式匹配的内容
System.out.println("整体:日期和时间:" + group);
String group1 = matcher.group(1); // 1 获取到表示第一个分组内容
System.out.println("第一个分组(第一个圆括号的内容)日期:" + group1);
String group2 = matcher.group(2); // 2获取到表示第二个分组的内容
System.out.println("第二个分组(第二个圆括号的内容)时间:" + group2);
}
}
}
注意不存在: group(3) 第三组分组的内容,获取的话报异常
4.2 命名分组
前面我们讲了分组编号,但由于编号得数在第几个位置,后续如果发现正则有问题,改动了括号的个数,还 可能导致编号发生变化,因此一些编程语言提供了 命名分组(named grouping),这样和数字相比更容易辨识,不容易出错。命名分组的格式为 (?P<分组名>正则)
。使用了命名分组,你即可以使用你命名的名称 / 自动编号的序号获取到对应分组中的内容。
命名的注意事项:
不能包含任何标点符号,并且不能以数字开头,可以使用单引号代替尖括号。例如 (‘?’ name)。
比如在获取四个连续的数字中,再分组获取两个连续的数字: 数值为 8899
8899
"(?<g1>\\d\\d)(?<g2>\\d\\d)";
补充:
public String group(String name); // Matcher 类中的对象方法,通过设置的分组名获取到对应匹配的分组内容。
举例:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 分组
*/
public class RegularTest08 {
public static void main(String[] args) {
String content = "9988fasjiofas";
//String regStr = "(\\d\\d)(\\d\\d)"; // 匹配 4 个数字的字符串
// 命名分组: 即可以给分组取名:
String regStr = "(?<num1>\\d\\d)(?<num2>\\d\\d)"; // 注意命名不可以数字开头
// 创建正则对象
Pattern pattern = Pattern.compile(regStr); // String实现了该参数的接口
// 创建匹配器
Matcher matcher = pattern.matcher(content);
// 循环匹配
while (matcher.find()) {
String group = matcher.group(0); // 整体分组
System.out.println(group);
//String group1 = matcher.group(1); // 第一分组
String group1 = matcher.group("num1"); // 第一分组
System.out.println(group1);
//String group2 = matcher.group(2); // 第二分组
String group2 = matcher.group("num2"); // 第二分组 可以用命名也可以用编号
System.out.println(group2);
}
}
}
需要注意的是,刚刚提到的方式命名分组和前面一样,给这个分组分配一个编号,不过你可以使用名称,不用编号,实际上命名分组的编号已经分配好了。不过命名分组并不是所有语言都支持的,在使用时,你需要查阅所用语言正则说明文档,如果支持,那你才可以使用。
4.3 特殊分组 / 不保存子组
在括号里面的会保存成子组,但有些情况下,你可能只想用括号将某些部分看成一个整体,后续不用再用它,类似这种情况,在实际使用时,是没必要保存子组的。这时我们可以在括号里面使用 ?:
不保存子组。
如果正则中出现了括号,那么我们就认为,这个子表达式在后续可能会再次被引用,所以 不保存子组可以提高正则的性能。除此之外呢,这么做还有一些好处,由于子组变少了,正则性能会更好,在 子组计数时也更不容易出错。
那到底啥是不保存子组呢?我们可以理解成,括号只用于归组,把某个部分当成「单个元素」,不分配编号,后面不会再进行这部分的引用。
具体的说明如下:
表格中的 pattern 表示匹配的内容/正则表达式。注意: 该格式中的 ( ) 圆括号是不表示分组的,因为该分组的内容是不会存储起来的,更无法捕获到的。
格式 | 说明 |
---|---|
(?:pattern) | 匹配 pattern ,但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储不供以后使用,这对于用 ”or“ 字符(|) 组合模式部件的情况很有用。例如:industr(?:y|ies) 是比 ‘industry|industries’ 更经济的表达式 |
(?=pattern) | 它是一个非捕获匹配,例如:Windows(?=95|98|NT|2000) 匹配 Windows 2000 中的 ”Windows" ,但不匹配 “Windows 3.1" 中的 Windows |
(?!pattern) | 该表达式匹配不处于匹配 pattern 的字符串的起始点的搜索字符串。它是一个非捕获匹配。例如:Windows(?!95|98|NT|200) 匹配 ”Windows 3.1" 中的 ”Windows ",但不匹配Windows 2000 中的 “Windows” |
举例:(?:pattern) 的使用 从”李华校长 李华院长 李华老师 李华同学 “的字符串中找到 李华校长 李华院长 李华老师 李华同学
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 分组
*/
public class RegularTest08 {
/**
* 1. 找到李华校长 李华院长 李华老师 李华同学 子字符串
*
*/
public static void main(String[] args) {
String content = "李华校长 李华院长 李华老师 李华同学";
String regStr = "李华(?:校长|院长|老师|同学)";
// 创建正则对象
Pattern pattern = Pattern.compile(regStr);
// 创建匹配器对象
Matcher matcher = pattern.matcher(content);
// 循环
while (matcher.find()) {
String group = matcher.group(0);
//String group1 = matcher.group(1); 特殊分组中的括号不表示分组,没有分组就无法捕获到分组内容了
System.out.println(group);
}
}
}
举例:(?=pattern) 的使用 。从“李华校长 李华院长 李华老师 李华同学 “字符串中找到身份为校长院长”名为李华的名字。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 分组
*/
public class RegularTest08 {
/**
* 从“李华校长 李华院长 李华老师 李华同学 “字符串中找到身份为校长院长”名为李华的名字。
*
*/
public static void main(String[] args) {
String content = "李华校长 李华院长 李华老师 李华同学";
String regStr = "李华(?=校长|院长)";
// 创建正则对象
Pattern pattern = Pattern.compile(regStr);
// 创建匹配器对象
Matcher matcher = pattern.matcher(content);
// 循环
while (matcher.find()) {
String group = matcher.group(0);
//String group1 = matcher.group(1); 特殊分组中的括号不表示分组,没有分组就无法捕获到分组内容了
System.out.println(group);
}
}
}
举例:(?!pattern)的使用 。 从“李华校长 李华院长 李华老师 李华同学 “字符串中找到身份不是院长,校长,老师的李华姓名
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 分组
*/
public class RegularTest08 {
/**
* 从“李华校长 李华院长 李华老师 李华同学 “字符串中找到身份不是院长,校长,老师的李华姓名
*
*/
public static void main(String[] args) {
String content = "李华校长 李华院长 李华老师 李华同学";
String regStr = "李华(?!校长|院长|老师)";
// 创建正则对象
Pattern pattern = Pattern.compile(regStr);
// 创建匹配器对象
Matcher matcher = pattern.matcher(content);
// 循环
while (matcher.find()) {
String group = matcher.group(0);
//String group1 = matcher.group(1); 特殊分组中的括号不表示分组,没有分组就无法捕获到分组内容了
System.out.println(group);
}
}
}
4.4 分组中的括号嵌套
前面讲完了子组和编号,但有些情况会比较复杂,比如在括号嵌套的情况里,我们要看某个括号里面的内容是第几个分组怎么办?不要担心,其实方法很简单,我们只需要数左括号(开括号)是第几个,就可以确定是第几个子组。
在阿里云简单日志系统中,我们可以使用正则来匹配一行日志的行首。假设时间格式是 2020-05-10 20:23:05 。
日期分组编号是 1,时间分组编号是 5,年月日对应的分组编号分别是 2,3,4,时分秒的分组编号分别是 6,7,8。
举例: 对 2020-05-10 20:23:05 日期时间进行 8 次分组。分为日期:年月日,时间:时分秒。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 分组
*/
public class RegularTest08 {
/**
*
* 对 2020-05-10 20:23:05 日期时间进行 8 次分组。分为日期:年月日,时间:时分秒。
*/
public static void main(String[] args) {
String content = "2020-05-10 20:23:05";
String regStr = "((\\d{4})-(\\d{2})-(\\d{2})).((\\d{2}):(\\d{2}):(\\d{2}))";
// 1.创建正则表达式对象
Pattern pattern = Pattern.compile(regStr);
// 2. 创建匹配器对象
Matcher matcher = pattern.matcher(content);
// 3.循环匹配遍历
while(matcher.find()) {
System.out.println("日期时间:" + matcher.group(1));
System.out.println("年:" + matcher.group(2));
System.out.println("月:" + matcher.group(3));
System.out.println("日:" + matcher.group(4));
System.out.println("时间:" + matcher.group(5));
System.out.println("时:" + matcher.group(6));
System.out.println("分:" + matcher.group(7));
System.out.println("秒:" + matcher.group(8));
}
}
}
5. 正则表达式的实现的原理分析:
下面我们来分析一下关于 正则表达式在Java中的 Patter 类和 Matcher 类中方法的调用的底层实现的机制,原理。
5.1 正则表达式中不含分组的原理分析
首先我们来分析没有分组中的正则表达式的原理
分析如下代码:
package blogs.blog12;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Java中正则表达式底层的实现原理
*/
public class RegularTest09 {
public static void main(String[] args) {
String content = "1995年5月23日,JAVA语言诞生(雏形)1996年,1997分别推出JDK1.0,JDK1.1版。" +
"1999年,JAVA被分成J2SE,J2EE,J2ME2000年," +
"JDK1.4发布2004年9月30日18:00PM,J2SE1.5发布,成为Java语言发展史上的又一里程碑。" +
"为了表示该版本的重要性,J2SE1.5更名为Java SE 5.0 2005年," +
"JAVA版本正式更名为JAVAEE,JAVASE, JAVAME2006年12月," +
"SUN公司发布JRE6.0 2009年04月20日,Oracle以74亿美元收购Sun。取得java的版权。 " +
"2010年9月,JDK7.0已经发布,增加了简单闭包功能。 " +
"2011年7月,甲骨文公司发布java7的正式版。2014年," +
"甲骨文公司发布了Java8正式版";
// 这里我编写一个获取连续4个数字的子字符串
String regStr = "\\d\\d\\d\\d";
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile(regStr);
// 2. 创建匹配器对象
Matcher matcher = pattern.matcher(content);
// 3.循环遍历匹配
while(matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
}
分析:
(matcher.find() 方法的执行的任务流程:
-
根据我们指定的/编写的正则表达式的规则,定位到满足我们正则表达式中的子字符串(比如这里是:1995)找4个连续的数字字符串
-
找到后,将该子字符串的开始索引 (这里的1995 的起始下标是 0 )下标和 结束索引 (这里的1995 的结束下标是 3 )下标位置,记录到 Matcher 对象中的 int[] groups 数组中。
- groups[0] = 0 ,把该找到的子字符串下标位置存储到 groups[0] 中。再把该子字符串的“结束的索引 + 1” 的值,这里为 4 ,即下次执行 find() 时,就从 4 索引开始匹配了。
- 注意:groups[20] 数组默认容量是 20 ,如果不足会自动扩容,其中的默认值是 -1(字符串的索引下标是从 0 开始的,没有-1 这个下标索引)。如下是我们 Debug 调试的结果
- 同时记录 odLast 的赋值为该子字符串的结束索引下标 + 1 的值,这里是 3 + 1 = 4 ,即下次执行 find() 方法时,就从 4 索引下标开始匹配了。
-
再次执行 find() 方法,从 oldLast 的值 开始匹配(这里是 4 )。覆盖第一个找到符合正则表达式的子字符串索引的在 groups[0]和groups[1] 数组当中的内容,这里因为没有分组所以对应的索引下标位置是仅仅只会存储到 groups[0]和groups[1] 数组下标位置。
- groups[0] = 23 值是:该匹配字符串的开始下标位置
- groups[1] = 27 值是:把该子字符串的“结束的索引” + 1的值。这里是 26 + 1 = 27。
- 同样 其中的 odLast 的赋值为该子字符串的结束索引下标 + 1 的值,这里是 26 + 1 = 27 ,即下次执行 find() 方法时,就从 27 索引下标开始匹配了。
如下是我们 Debug 调试的结果
- … 如果再次执行 find() 方法,仍然是按照上面的方式找寻的。
Matcher 类中的 group()方法的源码分析:
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();
}
从源码上 传入参数 0 走一下
5.2 正则表达式中含分组的原理分析
以如下代码分析:该代码的作用:将4个连续的数字字符,再分组为 2 个连续的数字字符
package blogs.blog12;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Java中正则表达式底层的实现原理
*/
public class RegularTest09 {
public static void main(String[] args) {
String content = "1995年5月23日,JAVA语言诞生(雏形)1996年,1997分别推出JDK1.0,JDK1.1版。" +
"1999年,JAVA被分成J2SE,J2EE,J2ME2000年," +
"JDK1.4发布2004年9月30日18:00PM,J2SE1.5发布,成为Java语言发展史上的又一里程碑。" +
"为了表示该版本的重要性,J2SE1.5更名为Java SE 5.0 2005年," +
"JAVA版本正式更名为JAVAEE,JAVASE, JAVAME2006年12月," +
"SUN公司发布JRE6.0 2009年04月20日,Oracle以74亿美元收购Sun。取得java的版权。 " +
"2010年9月,JDK7.0已经发布,增加了简单闭包功能。 " +
"2011年7月,甲骨文公司发布java7的正式版。2014年," +
"甲骨文公司发布了Java8正式版";
// 这里我编写一个获取连续4个数字的子字符串
String regStr = "(\\d\\d)(\\d\\d)";
// 1. 创建正则表达式对象
Pattern pattern = Pattern.compile(regStr);
// 2. 创建匹配器对象
Matcher matcher = pattern.matcher(content);
// 3.循环遍历匹配
while(matcher.find()) {
String group = matcher.group(0); // 获取整体正则表达式内容
System.out.println(group);
String group1 = matcher.group(1); // 获取在符合匹配整体正则表达式内容的基础上,再的第1个分组(编号是从左往右)
System.out.println(group1);
String group2 = matcher.group(2);
System.out.println(group2); // 获取在符合匹配整体正则表达式内容的基础上,再的第2个分组(编号是从左往右)
}
}
}
分析: 如下是我们 Debug 调试的结果
从结果上看,我们再进行分析:
正则表达式中含有的分组,其 matcher.find() 方法的执行流程和上面没有进行分组的流程是一样的。同样的地方就不多赘述了。
这里我们来说说不同的地方。
分组: 比如(\d\d)(\d\d)
正则表达式中有()表示分组,第1个()表示第1组,第2个表示第2组…(注意:编号分组是从左向右的圆括号开始的)
根据指定的规则,定位满足正则表达式规则的子字符串(比如 (19)(98))
注意是先从整体上匹配正则表达式,也就是没有分组的时候: \d\d\d\d ,只有到整体都匹配了
才会再该整体的基础上,再进行分组
找到后:将子字符串的开始索引记录到 matcher对象的属性 int[] groups数组当中
- groups[0] 记录的是整体匹配正则表达式的子字符串的起始位置;(1995) 0
- groups[1] 记录的是整体匹配正则表达式的子字符串的结束下标索引 +1 (1995) 3+1= 4
- groups[2] 记录的是再整体匹配正则表达式的基础上再分组的第1个分组的子字符串的起始下标位置: (1995)(19) 0
- groups[3] 记录的是再整体匹配正则表达式的基础上再分组的第1个分组的子字符串的结束下标位置 + 1:(1995)(19) 1+1=2
- groups[4] 记录的是再整体匹配正则表达式的基础上再分组的第2个分组的子字符串的起始下标位置: (1995)(95) 2
- groups[3] 记录的是再整体匹配正则表达式的基础上再分组的第2个分组的子字符串的结束下标位置 + 1:(1995)(95) 3+1=4
- 第3组,第4组 … 都还是以此类推 。
6. 正则表达式:分组,捕获,反向引用
分组: 我们可以用圆括号 “()”
组成一个比较复杂的匹配模式,那么一个圆括号的部分我们可以看作是一个子表达式/一个分组。
捕获: 把正则表达式中子表达式/分组匹配的内容,保存到内存中以数字编号/显式命名的组里。方便后面引用,从左向右,以分组的左括号为标志,第一个出现的分组的组号为 1,第二个为 2,依次类推。组0 代表的是整个正则表达式。
反向引用: 圆括号的内容被捕获后,可以在这个括号后被重复使用,从而写出一个比较实用的匹配模式。这个我们称为反向引用。这种引用即可以是在正则表达式内部使用,也可以在正则表示式的外部使用。内部反向引用使用 \\分组号
,外部反向引用使用 $分组号
。
反向引用与捕获组匹配的输入字符串的部分保存在存储器中,以便以后通过反向引用进行调用。**反向引用在正则表达式中指定为反斜杠(\)
, 后跟数字,表示要调用的组的编号。**例如,表达式 (\d\d)
定义了一行匹配一行中的两位数的捕获组,后者可以通过反向引用在表达式中调用该捕获组 \1
。**注意:**因为Java中的 想要表示 \ 反斜杠,是用两个 \\两个反斜杠表示的。
反向引用 的主要作用就是:重复使用分组中的内容,因为分组中的内容是会存储起来的,所以我们可以使用反向引用重复使用。
要匹配任何2位数,后跟完全相同的两位数,您将使用 (\\d)\\1
正则表达式:
package blogs.blog12;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 反向引用:
*
*/
public class RegularTest10 {
public static void main(String[] args) {
String content = "hello jack14 tom11 jack222222 yyy 5225, 1551 xxx";
// 反向引用在正则表达式内部中使用的 \\ 反斜杠的格式。
String regStr = "(\\d)\\1"; // 11,22 两个连续的数字 \\1反引用:表示重复使用第一个分组号中的内容
// 创建正则对象
Pattern pattern = Pattern.compile(regStr);
// 创建比较器对象
Matcher matcher = pattern.matcher(content);
while(matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
}
举例: 要匹配个位与千位相同,十位与百位相同的数: 5225, 1551 (\\d)(\\\d)\\2\\1
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 反向引用:
*
*/
public class RegularTest10 {
public static void main(String[] args) {
String content = "hello jack14 tom11 jack222222 yyy 5225, 1551 xxx";
// 反向引用在正则表达式内部中使用的 \\ 反斜杠的格式。
// 要匹配个位与千位相同,十位与百位相同的数: 5225, 1551 (\\d)(\\d)\\2\\1
String regStr = "(\\d)(\\d)\\2\\1"; // \\1反引用:表示重复使用第一个分组号中的内容
// \\2 反引用:表示重复使用第2个分组号中的内容
// 创建正则对象
Pattern pattern = Pattern.compile(regStr);
// 创建比较器对象
Matcher matcher = pattern.matcher(content);
while(matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
}
举例: 请在字符串中检索商品编号,形式如: 12321-333999111 要求满足前面是一个五位数,然后一个1号,然后是一个九位数,连续的每三位要相同。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 反向引用:
*/
public class RegularTest10 {
/**
* 请在字符串中检索商品编号,形式如: 12321-333999111
* 要求满足前面是一个五位数,然后一个1号,然后是一个九位数,连续的每三位要相同。
*/
public static void main(String[] args) {
String content = " 12321-333999111";
String regStr = "\\d{5}-(\\d)\\1{2}(\\d)\\2{2}(\\d)\\3{2}";
// 创建正则对象
Pattern pattern = Pattern.compile(regStr);
// 创建比较器对象
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
String group = matcher.group();
System.out.println(group);
}
}
}
7. 正则表达式:案例练习讲解
- 正则表达式:经典的结巴程序把类似:“我…我要…学学学学…编程java!”;通过正则表达式 修改成 “我要学编程Java”
package blogs.blog12;
import org.junit.Test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest11 {
/**
* 正则表达式:经典的结巴程序
* 把类似:"我...我要...学学学学...编程java!";
* 通过正则表达式 修改成 "我要学编程Java"
*/
@Test
public void test() {
String content = "我...我要...学学学学...编程java!";
// 1.去掉所有的.
String regStr = "\\.";
// 正则对象
Pattern pattern = Pattern.compile(regStr);
// 匹配器对象
Matcher matcher = pattern.matcher(content);
// 1. 去掉所有的. 使用 matcher.replaceAll("") 替换为 空
content = matcher.replaceAll("");
// 2. 去重复的子,我我要学学学学编程Java
// 思路:
//(1)使用(.)\\1+ 找到重复的
//(2)使用 反向引用$1 来替换匹配到的内容: 因为是在正则表达式外部了,反向引用格式是 $
pattern = Pattern.compile("(.)\\1+");
matcher = pattern.matcher(content);
System.out.println(content);
// 循环
while(matcher.find()) {
String group = matcher.group(0);
System.out.println(group);
}
//(2)使用 反向引用$1 来替换匹配到的内容: 因为是在正则表达式外部了,反向引用格式是 $
content = matcher.replaceAll("$1"); //将 我我替换成 单个我,将学学学学替换成单个学
System.out.println(content);
}
}
方式二:一条语句完成
import org.junit.Test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest11 {
/**
* 方式二
*/
// 方式2 一条语句完成:
@Test
public void test2() {
String content = "我...我要...学学学学...编程java!";
// 1.去除.
content = Pattern.compile("\\.+").matcher(content).replaceAll("");
content = Pattern.compile("(.)\\1+").matcher(content).replaceAll("$1");
System.out.println(content);
}
}
- 替换功能;String 类 public String replaceAll(String regex,String replacement),参数1是正则表达式,参数2是字符串,将如下文字 JDK1.3 JDK1.4 统一替换成 JDK,fajiodfjasiofjioasjJDK1.3afsjoiadsjfoiasjJDK1.4。
import org.junit.Test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest11 {
/**
* 替换功能;
* String 类 public String replaceAll(String regex,String replacement)
* 参数1是正则表达式,参数2是字符串
* 将如下文字 JDK1.3 JDK1.4 统一替换成 JDK
* fajiodfjasiofjioasjJDK1.3afsjoiadsjfoiasjJDK1.4
*/
@Test
public void test3() {
String content = "fajiodfjasiofjioasjJDK1.3afsjoiadsjfoiasjJDK1.4";
// 使用正则表达式,将JDK1.3和JDK1.4替换成JDK
String regStr = content.replaceAll("JDK1\\.3|JDK1\\.4", "JDK");
System.out.println(regStr);
}
}
- 要求: 验证一个手机号,要求必须是以138 139 开头的,使用 String 类 public boolean matchers(String regex) {} 方法。
import org.junit.Test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest11 {
/**
* 要求: 验证一个手机号,要求必须是以138 139 开头的
* 使用 String 类 public boolean matchers(String regex) {} 方法
*/
@Test
public void test4() {
String content = "13812312123";
boolean matches = content.matches("(138|139)\\d{8}");
System.out.println(matches);
}
}
- 使用 String 类 public String[] split(String regex),将 "hello#abc-jack12smith~北京’,要求按照 # 或者 -或者 ~ 或者数字来分割
import org.junit.Test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest11 {
/**
* 使用 String 类 public String[] split(String regex)
* 将 "hello#abc-jack12smith~北京’
* 要求按照 # 或者 -或者 ~ 或者数字来分割
*/
@Test
public void test5() {
String content = "hello#abc-jack12smith~北京";
String[] split = content.split("#|-|~|\\d+");
for (String regStr : split) {
System.out.println(regStr);
}
}
}
- 验证电子邮件格式是否合法规定电子邮件规则为:
- 只能有一个@
- @前面是用户名,可以是a-z,A-Z,0-9 _字符
- @后面是域名,并且域名只能是英文字母,比如: sohu.com 或者 tsinghua.org.cn
- 写出对应的正则表达式,验证输入的字符串是否为满足规则。
import org.junit.Test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest11 {
/**
* 验证电子邮件格式是否合法
* 规定电子邮件规则为:
* 1. 只能有一个@
* 2. @前面是用户名,可以是a-z,A-Z,0-9 _字符
* 3. @后面是域名,并且域名只能是英文字母,比如: sohu.com 或者 tsinghua.org.cn
* 4.写出对应的正则表达式,验证输入的字符串是否为满足规则。
*/
@Test
public void test6() {
String content = "f3213@qq.com";
String regStr = "[\\w-]+@([a-zA-Z]+\\.)+[a-zA-Z]+";
boolean matches = content.matches(regStr);
System.out.println(matches);
}
}
- 要求:验证是不是整数或者小数这个题目要考虑正数和负数: 123 -345 34.89 -87.9 -0.01 0.45
import org.junit.Test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest11 {
/**
* 要求:验证是不是整数或者小数
* 这个题目要考虑正数和负数
* 123 -345 34.89 -87.9 -0.01 0.45
*/
@Test
public void test7() {
//String content = "123 -345 34.89 -87.9 -0.01 0.45";
String content = "0.56";
// 思路先写简单的正则表达式
// 在逐步完善
//String regStr = "^\\d+$"; // 判断数值
//String regStr = "^[-+]?\\d+(\\.\\d+)?$"; // 包含正负和小数
// 处理特殊情况 000123.45
String regStr = "^[-+]?([1-9]\\d*|0)(\\.\\d+)?$"; // 包含正负和小数
boolean matches = content.matches(regStr);
System.out.println(matches);
}
}
- 对 url 进行解析:https://www.sohu.com:8080/abc/index.htm
获取协议 https
获取域名: www.sohu.com
端口: 8080
文件名: index.htm
import org.junit.Test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegularTest11 {
/**
* 对 url 进行解析:
* https://www.sohu.com:8080/abc/index.htm
* 获取协议 https
* 获取域名: www.sohu.com
* 端口: 8080
* 文件名: index.htm
*/
@Test
public void test8() {
String content = "https://www.sohu.com:8080/abc/index.htm";
String regStr = "^([a-zA-Z]+)://([a-zA-z.]+):(\\d+)[\\w-/]*/([\\w.])+$";
//String regStr = "^([a-zA-Z]+)://([a-zA-z.]+):(\\d+)[\\w-/]*/([\\w.%¥#(特殊符号)])+$"; 不够的时候还可以加
// 正则对象
Pattern pattern = Pattern.compile(regStr);
// 匹配器
Matcher matcher = pattern.matcher(content);
if(matcher.matches()) { // 整体匹配,成功获取信息
String group = matcher.group(1);
System.out.println(group);
String group2 = matcher.group(2);
System.out.println(group2);
String group3 = matcher.group(3);
System.out.println(group3);
String group4 = matcher.group(4);
System.out.println(group4);
} else {
System.out.println("匹配失败");
}
}
}
8. 正则表达式:优化建议
8.1 尽量准确表示匹配范围
比如我们要匹配引号里面的内容,除了写成 ".+?"
之外,我们可以写成 "[^"]+"
。使用 [^"] 要比使用点号好很多,虽然使用的是贪婪模式,但它不会出现点号将引号匹配上,再吐出的问题。
上面 的区别如下:
".+"
:如果使用贪婪模式,那么将会导致第一遍,将最后一个引号"
吃掉,再回溯(吐出来)匹配正则里面的最后一个引号".+?"
:使用了非贪婪模式,不会导致发生回溯"[^"]+"
:贪婪模式,引号开头结尾,中间用中括号选用非引号的字符出现 1 到多次,也不会发生回溯
8.2 提取出公共部分
通过上面对 NFA 引擎的学习,相信你应该明白 (abcd|abxy)
这样的表达式,可以优化成 ab(cd|xy)
,因为 NFA 以正则为主导,会导致字符串中的某些部分重复匹配多次,影响效率。
因此我们会知道 th(?:is|at)
要比 this|that
要快一些,但从可读性上看,后者要好一些,这个就需要用的时候去权衡,也可以添加代码注释让代码更容易理解。
类似地,如果是锚点,比如 (^this|^that) is
这样的,锚点部分也应该独立出来,可以写成比如 ^th(is|at) is
的形式,因为锚点部分也是需要尝试去匹配的,匹配次数要尽可能少。
8.3 出现可能性大的放左边
由于正则是从左到右看的,把出现概率大的放左边,域名中 .com
的使用是比 .net
多的,所以我们可以写成. (?:com|net)\b
,而不是 \.(?:net|com)\b
。
8.4 只在必要时才使用子组
在正则中,括号可以用于归组,但如果某部分后续不会再用到,就不需要保存成子组。通常的做法是,在写好正则后,把不需要保存子组的括号中加上 ?:
来表示只用于归组。如果保存成子组,正则引擎必须做一些额外工作来保存匹配到的内容,因为后面可能会用到,这会降低正则的匹配性能。
8.5 警惕嵌套的子组重复
如果一个组里面包含重复,接着这个组整体也可以重复,比如 (.*)*
这个正则,匹配的次数会呈指数级增长,所以尽量不要写这样的正则。
8.6 避免不同分支重复匹配
在多选分支选择中,要避免不同分支出现相同范围的情况
9. 资料:常用的正则表达式格式
一、校验数字的表达式
1 数字:^[0-9]*$
2 n位的数字:^\d{n}$
3 至少n位的数字:^\d{n,}$
4 m-n位的数字:^\d{m,n}$
5 零和非零开头的数字:^(0|[1-9][0-9]*)$
6 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
7 带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?$
8 正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
9 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
10 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
11 非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
12 非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
13 非负整数:^\d+$ 或 ^[1-9]\d*|0$
14 非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
15 非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
16 非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
17 正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
18 负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
19 浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$
二、校验字符的表达式
1 汉字:^[\u4e00-\u9fa5]{0,}$
2 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
3 长度为3-20的所有字符:^.{3,20}$
4 由26个英文字母组成的字符串:^[A-Za-z]+$
5 由26个大写英文字母组成的字符串:^[A-Z]+$
6 由26个小写英文字母组成的字符串:^[a-z]+$
7 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
8 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
9 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
10 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
11 可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+
12 禁止输入含有~的字符:[^~\x22]+
三、特殊需求表达式
1 Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
2 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
3 InternetURL:[a-zA-z]+://[^\s]* 或 ^https://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
4 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
5 电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$
6 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
7 身份证号:
15或18位身份证:^\d{15}|\d{18}$
15位身份证:^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$
18位身份证:^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{4}$
8 短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$
9 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
10 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
11 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
12 日期格式:^\d{4}-\d{1,2}-\d{1,2}
13 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
14 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
15 钱的输入格式:
16 1.有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^[1-9][0-9]*$
17 2.这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$
18 3.一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$
19 4.这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
20 5.必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$
21 6.这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
22 7.这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$
23 8.1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$
24 备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
25 xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
26 中文字符的正则表达式:[\u4e00-\u9fa5]
27 双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
28 空白行的正则表达式:\n\s*\r (可以用来删除空白行)
29 HTML标记的正则表达式:<(\S*?)[^>]*>.*?|<.*? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)
30 首尾空白字符的正则表达式:^\s*|\s*$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
31 腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
32 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
33 IP地址:\d+\.\d+\.\d+\.\d+ (提取IP地址时有用)
10. 总结
- 正则表达式中默认是一个
\
斜杆的,不过在Java中一个斜杆 需要两个\\
才能表示。 - 正则表达式默认是区分大小写的,可以设置不区分大小写的
- 正则表达式中默认是贪婪匹配:默认是匹配最多的可能结果。可以设置为非贪婪匹配
- 正则表达式中的分组/命名分组,默认分组编号是从左边第一个圆括号开始计数的,默认的 group[0] 是正则表达式整体,注意不要越界访问了。
- Java中执行正则表达式的原理流程。
- 反向引用的使用的格式:正则表达式内部
\\分组号
,正则表达式外部使用格式是 :$分组号
。
11. 最后:
👍👍👍 ✏️✏️✏️✏️✏️✏️✏️✏️✏️✏️ 感谢以下大佬提供的参考资料 ✏️✏️✏️✏️✏️✏️✏️✏️✏️✏️ 👍👍👍
【1】:https://zq99299.github.io/note-book/regular/
【2】:https://zq99299.github.io/java-tutorial/essential/regex/
【3】:https://www.runoob.com/java/java-regular-expressions.html
【4】:https://www.w3cschool.cn/regexp/x9hf1pq9.html
限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善 。谢谢大家,江湖再见,后会有期 !!!
0-9 ↩︎
0-9 ↩︎