【JavaSE】之正则表达式

94 阅读15分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第21天,点击查看活动详情

前言

本文将为大家对Java基础正则表达式相关知识进行介绍,首先向大家对正则表达式进行简介,帮助大家理解正则表达式究竟是什么以及正则表达式怎么用。然后对正则表达式中的普通字符,非打印字符,特殊字符,限定符,定位符的使用进行详尽介绍~

👉Java全栈学习路线可参考:  【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~

👉算法刷题路线可参考:  算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~


我们可能有如下的需求:

  • 从一个文章里找到所有的邮箱;
  • 看看输入的手机号是不是符合手机号的规则;
  • 检查输入的是不是身份证号。

对于这种需要,都要求对字符串进行特定【模式或规则】的匹配。本章学习的正则表达式可以帮助我们实现这样的功能。

一、正则表达式简介

1.正则表达式是什么

正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。在众多语言中都可以支持正则表达式,如Perl、PHP、Java、Python、Ruby等。当然在Java中也可以通过处理字符串的方式达到检索,替换文本字符串的目的,但是有了正则表达式写代码更加简洁,通常两三行代码就可以达到目的,当然这也是建立在熟悉正则表达式的基础之上的。

2.正则表达式怎么用

正则表达式的使用分为只使用Pattern类两个类联合使用:Pattern、Matcher两种方式。

只使用Pattern类

boolean Pattern.matches(String regex,CharSequence input)

  • regex:正则表达式
  • input:要匹配的字符串
String str="12dr 32 d2d";
System.out.println(Pattern.matches("\\d+", str)); // 一定要全部是数字才行返回true

String[] Pattern.split(CharSequence input)

  • 分割字符串
Pattern p=Pattern.compile("\\d+"); 
String[] str=p.split("a1b1c1"); 
// 结果:
//     str[0]="a" str[1]="b" str[2]="c"

Pattern、Matcher联合使用
Pattern、Matcher联合使用可以多次匹配和对正则表达式的分组支持

Matcher.matches()

  • 看是否全部为正则表达式的类型,返回布尔值
Pattern compile = Pattern.compile("\\d+");
Matcher matcher = compile.matcher("22bb11");
System.out.println(matcher.matches());// 返回false,因为"22bb11"不全部是数字
// 结果:
//     false

lookingAt()

  • 只看第一个匹配到的是不是正则表达式要的
Pattern compile = Pattern.compile("\\d+");
Matcher matcher = compile.matcher("22bb11");
System.out.println(matcher.lookingAt());// 返回true,因为"22bb11"开头是数字
// 结果:
//     true

find()

  • 对字符串进行匹配,匹配到的字符串可以在任何位置
Pattern compile = Pattern.compile("\\d+");
Matcher matcher = compile.matcher("22bb11");
System.out.println(matcher.find());// 返回true,因为"22bb11"中有数字数字

当使用matches(),lookingAt(),find()执行匹配操作后,就可以利用以下三个方法得到更详细的信息

  • start()返回匹配到的子字符串在字符串中的索引位置
  • end()返回匹配到的子字符串的最后一个字符在字符串中的索引位置
  • group()返回匹配到的子字符串
Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("aaa2223bb"); 
m.find();//匹配2223 // 必写,调用此方法后才能调用后面的三个方法
m.start();//返回3 
m.end();//返回7,返回的是2223后的索引号 
m.group();//返回2223

正则表达式还包括捕获组的操作

"((\\d+)(.*))" // 一个括号中包含两个括号,每个括号中代表一种正则表达式,我们可以利用组的操作去获取不同数据类型的数据。

在 java 中start(),end(),group()均有一个重载方法

  • start(int i),end(int i),group(int i)专用于分组操作,Mathcer类还有一个groupCount()用于返回有多少组

代码示例:

Pattern p=Pattern.compile("([a-z]+)(\\d+)"); 
Matcher m=p.matcher("aaa2223bb"); 
m.find();   //匹配aaa2223 
m.groupCount();   //返回2,因为有2组 
m.start(1);   //返回0 返回第一组匹配到的子字符串在字符串中的索引号 
m.start(2);   //返回3 
m.end(1);   //返回3 返回第一组匹配到的子字符串的最后一个字符在字符串中的索引位置. 
m.end(2);   //返回7 
m.group(1);   //返回aaa,返回第一组匹配到的子字符串 
m.group(2);   //返回2223,返回第二组匹配到的子字符串

二、普通字符

  • 普通字符包括没有显式指定为元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。
字符描述
[ABC]匹配 […] 中的所有字符
[^ABC]匹配除了 […] 中字符的所有字符
[A-Z]匹配所有大写字母
[a-z]匹配所有小写字母
.匹配除换行符(\n、\r)之外的任何单个字符,相等于 [^\n\r]
[\s\S]匹配所有。\s 是匹配所有空白符,包括换行,\S 非空白符,不包括换行。
\w匹配字母、数字、下划线。等价于 [A-Za-z0-9_]

三、非打印字符

  • 非打印字符也可以是正则表达式的组成部分。
字符描述
\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。

四、特殊字符

  • 特殊字符,就是一些有特殊含义的字符,如上面说的 runoob 中的 ,简单的说就是表示任何字符串的意思。如果要查找字符串中的符号,则需要对 进行转义,即在其前加一个 \
  • 许多元字符要求在试图匹配它们时特别对待。若要匹配这些特殊字符,必须首先使字符”转义”,即,将反斜杠字符\ 放在它们前面
字符描述
$匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 也匹配‘\n’或‘˚。要匹配也匹配 ‘\n’ 或 ‘\r’。要匹配 字符本身,请使用 $。
( )标记一个子表达式的开始和结束位置。
*匹配前面的子表达式零次或多次。要匹配 * 字符,前面加上 \ 。
+匹配前面的子表达式一次或多次。要匹配 + 字符,前面加上 \。
.匹配除换行符 \n 之外的任何单字符。要匹配 . ,前面加上 \ 。
[标记一个中括号表达式的开始
?匹配前面的子表达式零次或一次
\将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符
{标记限定符表达式的开始

五、限定符

字符描述
*匹配前面的子表达式零次或多次。例如,zo 能匹配 “z” 以及 “zoo”。 等价于{0,}。
+匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 、 “does” 中的 “does” 、 “doxy” 中的 “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?’。请注意在逗号和两个数之间不能有空格。

例1:/[1-9][0-9]*/ [1-9]设置第一个数字不是 0,[0-9]* 表示任意多个数字

例2:/[0-9]{1,2}/ 匹配 0~99 的两位数 缺点是:只能匹配两位数字,而且可以匹配 0、00、01、10 99 等编号仍只匹配开头两位数字。

例3:/[1-9][0-9]?/ 或 /[1-9][0-9]{0,1}/ 匹配 1~99 的正整数

六、定位符

  • 定位符用来描述字符串或单词的边界,^ 和 $ 分别指字符串的开始与结束,\b 描述单词的前或后边界,\B 表示非单词边界。
字符描述
^匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配。
$匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配。
\b匹配一个单词边界,即字与空格间的位置。
\B非单词边界匹配。

注意:

  • 不能将限定符与定位符一起使用。由于在紧靠换行或者单词边界的前面或后面不能有一个以上位置,因此不允许诸如 ^* 之类的表达式。
  • 若要匹配一行文本开始处的文本,请在正则表达式的开始使用 ^ 字符。不要将 ^ 的这种用法与中括号表达式内的用法混淆。
  • 若要匹配一行文本的结束处的文本,请在正则表达式的结束处使用 $ 字符。

例子:

  • /^Chapter [1-9][0-9]{0,1}$/ :匹配Chapter 1到Chapter 99的字符
  • /\bCha/ :匹配Cha开头的单词
  • /ter\b/ :匹配ter结尾的单词
  • /\Bapt/ :匹配 Chapter 中的字符串 apt,但不匹配 aptitude 中的字符串 apt

七、正则表达式基础语法

1、分组

限定符是作用在与他相邻的最左边的一个字符,那么问题来了,如果我想要ab同时被限定那怎么办呢?

正则表达式中用小括号()来做分组,也就是括号中的内容会作为一个整体。

如匹配字符串中包含0到多个ab开头:^(ab)*

2、转义

正则提供了转义的方式,也就是要把这些元字符、限定符或者关键字转义成普通的字符,做法很简答,就是在要转义的字符前面加个斜杠,也就是\即可。

匹配字符串中包含0到多个(ab)开头:^((ab))*

匹配一个字符*:*

3、条件

回到我们刚才的手机号匹配,我们都知道:国内号码都来自三大运营商,它们都有属于自己的号段。

比如联通有130/131/132/155/156/185/186/145/176等号段,假如让我们匹配一个联通的号码,那按照我们目前所学到的正则,应该无从下手的,因为这里包含了一些并列的条件,也就是“或”,那么在正则中是如何表示“或”的呢?

正则用符号 | 来表示或,也叫做分支条件,当满足正则里的分支条件的任何一种条件时,都会当成是匹配成功。

那么我们就可以用或条件来处理这个问题:

^(130|131|132|155|156|185|186|145|176)\d{8}$

4、区间

正则提供一个元字符中括号 [] 来表示区间条件。

  • 限定0到9 可以写成[0-9]
  • 限定A-Z 写成[A-Z]
  • 限定某些数字 [165]

那上面的正则我们还改成这样:

^((13[0-2])|(15[56])|(18[5-6])|145|176)\d{8}$

5、反义

前面说到元字符的都是要匹配什么什么,当然如果你想反着来,不想匹配某些字符,正则也提供了一些常用的反义元字符:

元字符解释
\W匹配任意不是字母,数字,下划线,汉字的字符
\S匹配任意不是空白符的字符
\D匹配任意非数字的字符
\B匹配不是单词开头或结束的位置
[^x]匹配除了x以外的任意字符
[^aeiou]匹配除了aeiou这几个字母以外的任意字符

6、常见的正则表达式

  • 匹配中文字符的正则表达式:[\u4e00-\u9fa5]

    匹配形式:My name is 小新!

  • 匹配Email地址的正则表达式:^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$ 匹配形式: 51012324@qq.comydlclass@163.comydl-class@126.com

  • 匹配国内电话号码:\d{3}-\d{8}|\d{4}-\d{7} 匹配形式:匹配形式如 0511-4405222 或 021-87888822

  • 匹配腾讯QQ号:[1-9][0-9]{4,} 匹配形式:510180222

  • 匹配身份证:(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$ 匹配形式:142228199108252125

  • 匹配ip地址:\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3} 匹配形式:127.0.0.1

  • 匹配国内的手机号:^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$

    匹配形式:1388888888

八、java正则表达式

小知识:

有些语言中,**** 表示:我就是一个普通的【反斜杠】,请不要给我任何特殊的意义。

在 Java 中,**** 表示:我不是一个普通的【反斜杠】,我必须对紧随其后的字符进行转义,如果想将我视为普通反斜杠,请转义我。

我们可以看以下的输出内容:

System.out.print("\");    // 输出为 \
System.out.print("\\");  // 输出为 \

java中正则表达式的执行流程:

1.png

1、正则表达式实例

java.util.regex 包主要包括以下三个类:

  • Pattern 类:

    正则表达式的编译表示形式。若要使用正则表达式必须将其【编译到此类】的实例中。然后,可以使用生成的模式对象创建 Matcher 对象。

  • Matcher 类:

    Matcher 对象是对输入字符串进行【解释和匹配】操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。

  • PatternSyntaxException:

    PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。

以下实例中使用了正则表达式  .itnanls.  用于查找字符串中是否包了 itnanls子串:

实例

class TestRegex{   
    
    @Test
    public void testRegex{      
        String content = "I am itnanls,I'm from ydlclass.";       
        String pattern = ".*itnanls.*";       

        boolean isMatch = Pattern.matches(pattern, content); 
        System.out.println("字符串中是否包含了 'itnanls' 子字符串? " + isMatch);    
    } 
}

 @Test
public void testRegex(){
    String context = "i am itnanls,i com from ydl.";
    String regex = ".*itlils.*";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(context);
    System.out.println(matcher.matches());
}

实例输出结果为:字符串中是否包含了 'itnanls' 子字符串? true

2、Matcher 类的方法

1、匹配方法

(1)该方法可以精确表明输入字符串中在哪能找到与之匹配的内容:

序号方法及说明
1start()返回匹配的起始索引
2start(int group)返回在匹配操作期间,由给定组所捕获的子序列的起始索引
3end()返回匹配字符末尾索引
4end(int group)返回在匹配操作期间,由给定组所捕获的子序列的末尾索引

下面是一个对单词 "cat" 出现在输入字符串中出现次数进行计数的例子:

@Test
public void testStart() {
    String regex = "cat";
    String content = "cat cat dog dog cat";
    Pattern pattern = Pattern.compile(regex);
    Matcher m = pattern.matcher(content); // 获取 matcher 对象
    int count = 0;

    while (m.find()) {
        count++;
        System.out.println("Match number " + count);
        System.out.println("start(): " + m.start());
        System.out.println("end(): " + m.end());
    }
}

以上实例编译运行结果如下(start返回cat的c的索引,end方法返回t的索引):

Match number 1
start(): 0
end(): 3
Match number 2
start(): 4
end(): 7
Match number 3
start(): 16
end(): 19

2、查找方法

查找方法用来检查输入字符串并返回一个布尔值,表示是否找到该模式:

序号方法说明
1lookingAt()返回目标字符串前面部分与 Pattern 是否匹配
2find()返回目标字符串中是否包含与 Pattern 匹配的子串
3find(int start)从指定索引开始匹配查找。
4matches()尝试将整个区域与模式匹配

matches 和 lookingAt 方法都用来尝试匹配一个输入序列模式。它们的不同是 matches 要求整个序列都匹配,而lookingAt 不要求。

lookingAt 方法虽然不需要整句都匹配,但是需要从第一个字符开始匹配。

实例:

@Test
public void testMatches() {
    String regex = "itnanls";
    String content1 = "itnanls";
    String content2 = "itnanls is very handsome  !";
    String content3 = "My name is itnanls.";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher1 = pattern.matcher(content1);
    Matcher matcher2 = pattern.matcher(content2);
    Matcher matcher3 = pattern.matcher(content3);

    System.out.println("matches1(): " + matcher1.matches());
    System.out.println("lookingAt1(): " + matcher1.lookingAt());
    System.out.println("matches2(): " + matcher2.matches());
    System.out.println("lookingAt2(): " + matcher2.lookingAt());
    System.out.println("matches3(): " + matcher3.matches());
    System.out.println("lookingAt3(): " + matcher3.lookingAt());
}

以上实例编译运行结果如下:

matches(): true
lookingAt(): true
matches(): false
lookingAt(): true
matches(): false
lookingAt(): false

3、替换方法

替换方法是替换输入字符串里文本的方法:

序号方法说明
1public String replaceAll(String replacement)替换模式与给定替换字符串相匹配的输入序列的每个子序列。
2public String replaceFirst(String replacement)替换模式与给定替换字符串匹配的输入序列的第一个子序列。
3public Matcher appendReplacement(StringBuffer sb, String replacement)实现非末尾的添加和替换步骤。
4public StringBuffer appendTail(StringBuffer sb)实现末尾的添加和替换步骤。

下面的例子来解释replaceAll和replaceFirst:

@Test
public void testReplace(){
    String regex = "itnanls";
    String context = "My name is itnanls, itnanls is very handsome. ";
    String replacement = "itlils";
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(context);
    String result1 = m.replaceAll(replacement);
    System.out.println(result1);
    String result2 = m.replaceFirst(replacement);
    System.out.println(result2);
}

以上实例编译运行结果如下:

My name is itlils, itlils is very handsome. 
My name is itlils, itnanls is very handsome. 

下面的例子来解释appendReplacement和appendTail:

@Test
public void testAppend() {
    String REGEX = "a*b";
    String INPUT = "aabfooaabfooabfooabkkk";
    String REPLACE = "-";
    Pattern p = Pattern.compile(REGEX);
    // 获取 matcher 对象
    Matcher m = p.matcher(INPUT);
    StringBuffer sb = new StringBuffer();
    m.find();
    m.appendReplacement(sb, REPLACE);
    System.out.println(sb);
    m.find();
    m.appendReplacement(sb, REPLACE);
    System.out.println(sb);
    m.appendTail(sb);
    System.out.println(sb);
}

以上实例编译运行结果如下:

-
-foo-
-foo-fooabfooabkkk
@Test
public void testAppend() {
    String regex = "a*b";
    String context = "aabfooaabfooabfoobkkk";
    String replacement = "-";
    Pattern pattern = Pattern.compile(regex);
    // 获取 matcher 对象
    Matcher matcher = pattern.matcher(context);
    StringBuffer sb = new StringBuffer();
    while (matcher.find()){
        matcher.appendReplacement(sb, replacement);
    }
    matcher.appendTail(sb);
    System.out.println(sb);
}

4、PatternSyntaxException 类的方法

PatternSyntaxException 是一个异常类,它指示一个正则表达式模式中的语法错误。

PatternSyntaxException 类提供了下面的方法来帮助我们查看发生了什么错误。

序号方法说明
1getDescription()获取错误的描述。
2getIndex()获取错误的索引。
3getPattern()获取错误的正则表达式模式。
4getMessage()返回多行字符串,包含语法错误及其索引的描述、错误的正则表达式模式和模式中错误索引的可视化指示。
String REGEX = "a*b[er";

image-20220805132012104

九、正则表达式进阶语法

1、零宽断言

【断言】就是说正则可以【断定】在指定内容的前面或后面会出现满足指定规则的内容。

【零宽】 断言部分只确定位置不匹配任何内容,只是一种模式。内容宽度为零。

我们来举个栗子:假设我们要用爬虫抓取csdn里的文章阅读量。通过查看源代码可以看到文章阅读量这个内容是这样的结构:

"<span class="read-count">阅读数:641</span>"

其中也就【641】这个是变量,也就是说不同文章不同的值,当我们拿到这个字符串时,需要获得这里边的【641】有很多种办法,但如果正则应该怎么匹配呢?下面先来讲几种类型的断言:

几个概念:

概念功能
预测/先行(模式在前),要求后面的符合匹配
回顾/后发(模式在后),要求前面的符合匹配
符合匹配
不符合匹配

1、正向先行断言

零宽度正预测先行断言

  • 语法:(?=pattern)
  • 作用:匹配pattern表达式的前面内容,不返回本身。

【正向先行断言】可以匹配表达式前面的内容,那意思就是(?=) 就可以匹配到前面的内容了。

如果我们要匹配所有内容那就是:

@Test
public void testAssert1(){
    String regex = ".+(?=</span>)";
    String context = "<span class="read-count">阅读数:641</span>";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(context);
    while (matcher.find()){
        System.out.println(matcher.group());
    }
}

//匹配结果:<span class="read-count">阅读数:641
//可是我们要的只是前面的数字呀,那也简单咯,匹配数字 \d,那可以改成:

@Test
public void testAssert2(){
    String regex = "\d+(?=</span>)";
    String context = "<span class="read-count">阅读数:641</span>";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(context);
    while (matcher.find()){
        System.out.println(matcher.group());
    }
}

//匹配结果:
//641

2、正向后行断言

零宽度正回顾后发断言,断言在前,模式在后

  • 语法:(?<=pattern)
  • 作用:匹配pattern表达式的后面的内容,不返回本身。

有先行就有后行,先行是匹配前面的内容,那后行就是匹配后面的内容啦。

上面的例子,我们也可以用后行断言来处理:

@Test
public void testAssert3(){
    String regex = "(?<=<span class="read-count">阅读数:)\d+";
    String context = "<span class="read-count">阅读数:641</span>";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(context);
    while (matcher.find()){
        System.out.println(matcher.group());
    }
}

3、负向先行断言

零宽度负预测先行断言

  • 语法:(?!pattern)
  • 作用:匹配非pattern表达式的前面内容,不返回本身。

有正向也有负向,负向在这里其实就是非的意思。

举个栗子:比如有一句 “我爱祖国,我是祖国的花朵”。现在要找到不是'的花朵'前面的祖国。

用正则就可以这样写:祖国(?!的花朵)

4、负向后行断言

零宽度负回顾后发断言

  • 语法:(?<!pattern)
  • 作用:匹配非pattern表达式的后面内容,不返回本身。

举个例子:比如有一句 “我爱祖国,我是祖国的花朵”。现在要找到不是'我爱'后面的祖国。

用正则就可以这样写:(?<!我爱)祖国

2、捕获和非捕获

**捕获组:**我们匹配子表达式的内容,并把匹配结果【以数字编号或组名的方式】保存到内存中,之后可以通过序号或名称来使用这些匹配结果。

而根据命名方式的不同,又可以分为两种组:

1、数字编号捕获组:

语法:(exp)

解释:从表达式左侧开始,每出现一个左括号和它对应的右括号之间的内容为一个分组,在分组中,第0组为整个表达式,第一组开始为分组。

  • 比如固定电话的:020-85653333
  • 正则表达式为:(0\d{2})-(\d{8})

按照左括号的顺序,这个表达式有如下分组:

序号编号分组内容
00(0\d{2})-(\d{8})020-85653333
11(0\d{2})020
20(\d{8})85653333

String test = "020-85653333";
String reg="(0\d{2})-(\d{8})";
Pattern pattern = Pattern.compile(reg);
Matcher mc= pattern.matcher(test);
if(mc.find()){
    System.out.println("分组的个数有:"+mc.groupCount());
    for(int i=0;i<=mc.groupCount();i++){
        System.out.println("第"+i+"个分组为:"+mc.group(i));
    }
}
输出结果:

分组的个数有:2
第0个分组为:020-85653333
第1个分组为:020
第2个分组为:85653333

可见,分组个数是2,但是因为第0个为整个表达式本身,因此也一起输出了。

2、命名编号捕获组:

语法:(?exp)

解释:分组的命名由表达式中的name指定,比如区号也可以这样写:

(?<quhao>0\d{2})-(?<haoma>\d{8})

按照左括号的顺序,这个表达式有如下分组:

序号名称分组内容
00(0\d{2})-(\d{8})020-85653333
1quhao(0\d{2})020
2haoma(\d{8})85653333

用代码来验证一下:

String test = "020-85653333";
String reg="(?<quhao>0\d{2})-(?<haoma>\d{8})";
Pattern pattern = Pattern.compile(reg);
Matcher mc= pattern.matcher(test);
if(mc.find()){
    System.out.println("分组的个数有:"+mc.groupCount());
    System.out.println(mc.group("quhao"));
    System.out.println(mc.group("haoma"));
}
输出结果:

分组的个数有:2
分组名称为:quhao,匹配内容为:020
分组名称为:haoma,匹配内容为:85653333

3、非捕获组:

  • 语法:(?:exp)
  • 解释:和捕获组刚好相反,它用来标识那些不需要捕获的分组。

比如上面的正则表达式,程序不需要用到第一个分组,那就可以这样写:(?:0\d{2})-(\d{8})

序号名称分组内容
00(0\d{2})-(\d{8})020-85653333
11(\d{8})85653333
String test = "020-85653333";
String reg="(?:0\d{2})-(\d{8})";
Pattern pattern = Pattern.compile(reg);
Matcher mc= pattern.matcher(test);
if(mc.find()){
    System.out.println("分组的个数有:"+mc.groupCount());
    for(int i=0;i<=mc.groupCount();i++){
        System.out.println("第"+i+"个分组为:"+mc.group(i));
    }
}
输出结果:

分组的个数有:1
第0个分组为:020-85653333
第1个分组为:85653333

3、反向引用

我们知道:捕获会返回一个捕获组,这个分组是保存在内存中,不仅可以在正则表达式外部通过程序进行引用,也可以【在正则表达式内部进行引用】,这种引用方式就是【反向引用】。

根据捕获组的命名规则,反向引用可分为:

  • 普通捕获组反向引用:\k<number>,通常简写为\number
  • 命名捕获组反向引用:\k<name>,或者\k'name'

我们可以举一个例子:

比如要查找一串字母"aabbbbgbddesddfiid"里成对的字母,如果按照我们之前学到的正则,什么区间啊限定啊断言啊可能是办不到的。

现在我们先用程序思维理一下思路:

1)匹配到一个字母

2)匹配第下一个字母,检查是否和上一个字母是否一样

3)如果一样,则匹配成功,否则失败

这里的思路中,在匹配下一个字母时,需要用到上一个字母进行比较,但是目前的知识实在办不到。

这下子捕获就有用处啦,我们可以利用捕获把上一个匹配成功的内容用来作为本次匹配的条件即可。

  1. 首先匹配一个字母:\w。我们需要做成分组才能捕获,因此写成这样:(\w)
  2. 那这个表达式就有一个捕获组:(\w)
  3. 然后我们要用这个捕获组作为条件,那就可以:(\w)\1

这里的\1是什么意思呢?根据反向引用的数字命名规则,就需要 \k<1>或者\1,当然,通常都是是后者。

我们来测试一下:

 @Test
public void testRef(){
    String context = "aabbxxccdddsksdhfhshh";
    String regex = "(\w)\1";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(context);
    while (matcher.find()){
        System.out.println(matcher.group());
    }
}
输出结果:

aa
bb
xx
cc
dd
hh

嗯,这就是我们想要的了。 再举个替换的例子,假如想要把字符串中abc换成a

@Test
public void testReplaceAll(){
    String context = "abc aabc bc xxx mm";
    String regex = "(a*)(b)(c)";
    String res = context.replaceAll(regex, "$1");
    System.out.println(res);
}
输出结果:

a aa  xxx mm

4、贪婪和非贪婪

1、贪婪匹配

**贪婪匹配:**当正则表达式中包含能接受重复的限定符时,该方式会匹配尽可能多的字符,这匹配方式叫做贪婪匹配。

前面我们讲过重复限定符,其实这些限定符就是贪婪量词,比如表达式:\d{3,6}

用来匹配3到6位数字,在这种情况下,它是一种贪婪模式的匹配,也就是假如字符串里有6个数字可以匹配,那它就是全部匹配到。

@Test
public void testGreed(){
    String regex = "\d{3,6}";
    String context ="61762828 176 2991 871";
    System.out.println("文本:" + context);
    System.out.println("贪婪模式:"+ regex);
    Pattern pattern =Pattern.compile(regex);
    Matcher matcher = pattern.matcher(context);
    while(matcher.find()){
        System.out.println("匹配结果:" + matcher.group(0));
    }
}
输出结果:

文本:61762828 176 2991 44 871
贪婪模式:\d{3,6}
匹配结果:617628
匹配结果:176
匹配结果:2991
匹配结果:871

由结果可见:本来字符串中的“61762828”这一段,其实只需要出现3个(617)就已经匹配成功了的,但是他并不满足,而是匹配到了最大能匹配的字符,也就是6个。

多个贪婪量词在一起时,如果字符串能满足他们各自最大程度的匹配时,就互不干扰,但如果不能满足时,会优先满足最大数量的匹配,剩余再分配下一个量词匹配。

 @Test
public void testGreed2(){
    String regex = "\d{1,2}\d{3,5}";
    String context ="61762828 176 2991 871";
    System.out.println("文本:" + context);
    System.out.println("贪婪模式:"+ regex);
    Pattern pattern =Pattern.compile(regex);
    Matcher matcher = pattern.matcher(context);
    while(matcher.find()){
        System.out.println("匹配结果:" + matcher.group(0));
    }
}
输出结果:

文本:61762828 176 2991 871
贪婪模式:\d{1,2}\d{3,5}
匹配结果:6176282
匹配结果:2991

2、懒惰匹配

懒惰匹配:当正则表达式中包含能接受重复的限定符时,会匹配尽可能少的字符,这匹配方式叫做懒惰匹配。

懒惰量词是在贪婪量词后面加个“?”

代码说明
*?重复任意次,但尽可能少重复
+?重复1次或更多次,但尽可能少重复
??重复0次或1次,但尽可能少重复
{n,m}?重复n到m次,但尽可能少重复
{n,}?重复n次以上,但尽可能少重复
@Test
public void testNotGreed(){
    String reg="(\d{1,2}?)(\d{3,4})";
    String test="61762828 176 2991 87321";
    System.out.println("文本:"+test);
    System.out.println("贪婪模式:"+reg);
    Pattern p1 =Pattern.compile(reg);
    Matcher m1 = p1.matcher(test);
    while(m1.find()){
        System.out.println("匹配结果:"+m1.group(0));
    }
}
输出结果:

文本:61762828 176 2991 87321
懒惰匹配:(\d{1,2}?)(\d{3,4})
匹配结果:61762
匹配结果:2991
匹配结果:87321
  • “61762” 是左边的懒惰匹配出6,右边的贪婪匹配出1762。
  • "2991" 是左边的懒惰匹配出2,右边的贪婪匹配出991。
  • "87321" 左边的懒惰匹配出8,右边的贪婪匹配出7321。

后记

本文呢为大家介绍了Java基础正则表达式相关知识,首先向大家对正则表达式进行简介,帮助大家理解正则表达式究竟是什么以及正则表达式怎么用。然后对正则表达式中的普通字符,非打印字符,特殊字符,限定符,定位符的使用进行了详尽的介绍~ 希望本文的分享能够使你有所收获,如果您想继续深入的学习数据结构与算法相关的知识,或想继续深入学习Java相关的知识与技术,可以参考:

👉Java全栈学习路线可参考:  【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~

👉算法刷题路线可参考:  算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~

看完不关注就想跑.gif