Java正则表达式-大白话手撕源码

发布于:2022-12-13 ⋅ 阅读:(420) ⋅ 点赞:(0)

1.介绍

正则表达式英文全称Regular Expression。一个正则表达式就是用某种模式去匹配字符串的一个公式,默认贪婪匹配。正则表达式不只是java语言独有的,许多编程语言都支持正则表达式对字符串的操作。

2.底层实现

要了解正则表达式的底层实现,那就要了解到两个类:PatternMatcher

2.1 一个简单案例

下面通过一个简单案例演示如何使用正则表达式完成匹配,找出符合要求的子串。该案例实现从一个字符串中找出所有的四位数。

代码:

public class RegExp_1 {
    public static void main(String[] args) {
        //匹配的字符串
        String content = "1999abx-#***2000ahj2001&2002";
        //正则表达式,\\d代表一个任意0-9的数字,四个\\d代表4个连在一起的数字
        String regex = "\\d\\d\\d\\d";
        //获得一个模式
        Pattern pattern = Pattern.compile(regex);
        //匹配
        Matcher matcher = pattern.matcher(content);
        while(matcher.find()){
            //打印匹配到的值
            System.out.println(matcher.group(0));
        }
    }
}

控制台输出:
案例1结果

2.2 案例解析

注意一下解析会搬出jdk源码,不需要去深究其中具体实现,只需要了解该方法有什么用?怎么使用?返回的值是什么?就可以了!

1.通过Pattern.compile(regex)方法传入正则表达式返回一个Pattern对象。下面这个代码块是jdk1.8中Pattern类的compile方法的源码,该方法是一个静态方法,返回一个Pattern对象。

public static Pattern compile(String regex) {
        return new Pattern(regex, 0);
    }

2.match意为匹配,调用Pattern类的matcher方法,传入需要匹配的字符串返回一个Mather对象。下面这个代码块是jdk1.8中Pattern类的matcher方法的源码。

public Matcher matcher(CharSequence input) {
        if (!compiled) {
            synchronized(this) {
                if (!compiled)
                    compile();
            }
        }
        Matcher m = new Matcher(this, input);
        return m;
    }

3.find意为找到,也就是找到了符合正则表达式的子串。通过查看Matcher类下该方法的源码,我们知道返回值是一个boolean类型的值。找到返回true,未找到返回false,因此我们在案例中调用并将其放在while循环中,使匹配直到再也找不到的时候才停止。

public boolean find() {
        int nextSearchIndex = last;
        if (nextSearchIndex == first)
            nextSearchIndex++;

        // If next search starts before region, start it at region
        if (nextSearchIndex < from)
            nextSearchIndex = from;

        // If next search starts beyond region then it fails
        if (nextSearchIndex > to) {
            for (int i = 0; i < groups.length; i++)
                groups[i] = -1;
            return false;
        }
        return search(nextSearchIndex);
    }

4.每找到一个匹配的子串,就打印一下matcher.group(0),而打印出来的东西刚好就是我们所需的结果。在展示的案例中有四个符合的子串,因此打印了4次。可以看出这个group()是重点,要解释清楚还是挺不容易的,在接下来的篇章中会重点解析。

2.3 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();
    }

1.首先group(int)返回的是一个字符串,案例中group(0)返回的是我们所需要的结果,说明这个方法返回的是一个字符串也是匹配成功的子串。这个int型的参数为什么取0先别管。

2.返回的是getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString(),也就是调用了getSubSequence这个方法然后将方法的返回值转成了字符串。所以我们查看getSubSequence的源码:

CharSequence getSubSequence(int beginIndex, int endIndex) {
        return text.subSequence(beginIndex, endIndex);
    }

3.显然getSubSequence是截取了text字符串的部分获得一个子串,那这个text究竟是什么,看看jdk源码怎么说:
在这里插入图片描述
text意为匹配的原始字符串,也就是案例中的content。

4.接下来我们只要搞清楚beginIndex和endIndex是怎么产生的,也就是弄清楚groups[ ]这个数组到底有什么用

为了了解groups[ ]里面有些什么,是怎么变化的,直接断点调试案例中的代码,在while(matcher.find())打断点:
在这里插入图片描述
(上图)可以看到matcher里有groups数组,初始化大小为20,全是-1;

在这里插入图片描述
(上图)往下走一步后,groups[0]=0,groups[l]=4

在这里插入图片描述
(上图)第二轮,groups[0]=12,groups[l]=16

在这里插入图片描述

(上图)第三轮,groups[0]=19,groups[l]=23

在这里插入图片描述

(上图)第四轮,groups[0]=24,groups[l]=28

四轮过后结束了。为什么会有4轮,因为案例中有四个匹配的子串,每一轮匹配到一个子串。通过观察会发现:groups只有下标0和1在变化。下标为0储存的值:匹配成功子串的开始下标,下标为1储存的值:匹配成功子串的终止下标+1!!!

5.所以第一轮的beginIndex=0,endIndex=4,截取了1999,以此类推…

2.4 group()的参数

这个参数与分组有关,先了解一下分组。
在正则表达式中,用()小括号括起来的就是一个分组,
现在修改案例代码如下:

public class RegExp_1 {
    public static void main(String[] args) {
        //匹配的字符串
        String content = "1999abx-#***2000ahj2001&2002";
        //正则表达式,\\d代表一个任意0-9的数字,四个\\d代表4个连在一起的数字
//        String regex = "\\d\\d\\d\\d";
        String regex2 = "(\\d\\d)(\\d\\d)";
        //获得一个模式
        Pattern pattern = Pattern.compile(regex2);
        //匹配
        Matcher matcher = pattern.matcher(content);

        int n = 0;
        while(matcher.find()){
            //打印匹配到的值
            System.out.println("匹配到的第"+(++n)+"个子串:"+matcher.group(0));
            System.out.println("第1个分组:"+matcher.group(1));
            System.out.println("第2个分组:"+matcher.group(2));
        }
    }
}

改进的代码中,正则表达式变为了两个分组,输出的多了group(1)和group(2)。

控制台输出:
在这里插入图片描述

可以很明显知道group(0)是打印整个子串,group(1)是打印子串的第1个分组,group(2)是打印子串的第二个分组。

为了说清楚这个group的参数,那追根到底就是要知道groups数组到底是怎么变化的,那再来debug一波:
在这里插入图片描述
(上图)第一轮,可以看到下标0-5都有了数值。推测一下不难得出0-1对应整个子串,2-3对应第一个分组,4-5对应第二个分组。不信可以再看以下几轮:
在这里插入图片描述
(上图)第二轮

在这里插入图片描述
(上图)第三轮

在这里插入图片描述
(上图)第四轮

四轮过后结束,不难看出groups数组记录的是整个子串以及分组在原字符串的开始下标与结束下标+1的数值。group(1)用到的就是groups数组下标为2-3的数值,group(2)用到的就是groups数组下标为4-5的数值,以此类推。

3.元字符

想要很好地使用正则表达式,必须了解各种元字符的功能,按功能分为以下六大类

3.1 限定符

用于指定其前面的字符和组合项连续出现多少次

符号 含义
* 指定字符重复0次或n次
+ 指定字符重复1次或n次(最少1次)
? 指定字符重复0次或1次(最多1次)
{n} 长度为n
{n,} 指定至少n个字符匹配
{n} 指定至少n个字符,至多m个字符匹配
在其它限定符后加上?变为非贪婪匹配

3.2 选择匹配符

符号 含义
l 匹配 l 之前或者之后的表达式

3.3 分组组合和反向引用符

3.3.1 捕获分组

常用分组构造形式 说明
(pattern) 非命名捕获。捕获匹配的子字符串。编号为零的第一个捕获是由整个正则表达式模式匹配的文本,其它捕获结果则根据左括号的顺序从1开始自动编号。
?<name>(pattern) 命名捕获。将匹配的子字符串捕获到一个组名称或编号名称中。用于name的字符串不能包含任何标点符号,并且不能以数字开头。

3.3.2 非捕获分组(特别分组)

常用分组构造形式 说明
(?:pattern) 匹配pattern但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用"or"字符组合模式部件的情况很有用。例如,industr(?:y l ies)是industry或industrie更经济的表达式。
(?=pattern) 它是一个非捕获匹配。例如,“Windows (?=95[98[NT[2000)’匹配"Windows 2000"中的Windows”,但不匹配"Windows 3.1"中的"windows"。
(?!pattern) 该表达式匹配不处于匹配pattern的字符串的起始点的搜索字符串。它是一个非捕获匹配。例如,“Windows (?I95]98[NT[2000)”匹配"Windows 3.1"中的“Windows”,但不匹配"Windows 2000"中的"Windows。.

3.3.3 反向引用

圆括号的内容被捕获后,可以在这个括号后被使用,从而写出一个比较实用的匹配模式,这个我们称为反向引用,这种引用既可以是在正则表达式内部,也可以是在正则表达式外部,内部反向引用\\分组号,外部反向引用$分组号

3.4 特殊字符

在使用正则表达式去匹配某些特殊符号时,需要用到转移符号(//),否则匹配不到结果甚至报错。
在java的正则表达式中,\代表其它语言中的\ !!!
需要用到转义字符的符号有:. * + ( ) & / \ ? [ ] ^ { }

3.5 字符匹配符

符号 含义
[ ] [abcd12] 匹配中括号中的任意一个字符
[^ ] [^abcd12] 匹配除了中括号中的任意一个字符
- A-Z 匹配任意单个大写字母
. 匹配除换行符(\n)以外的任意字符
\\d 匹配单个数字字符 相当于[0-9]
\\D 匹配单个非数字字符 相当于[^0-9]
\\w 匹配单个数字、大小写字母字符 相当于[0-9a-zA-Z]
\\W 匹配非单个数字、大小写字母字符 相当于[^0-9a-zA-Z]

3.6 定位符

规定要匹配的字符串出现的位置

符号 含义
^ 指定开始字符
$ 指定结束字符
\\b 匹配目标字符串的边界
\\B 匹配目标字符串的非边界

4.正则表达式的三个常用类

java.util.regex包主要包括以下三个类Pattern类、Matcher类和PatternSyntaxException

4.1 Pattern

其中有两个常用方法:
1.静态方法compile(String regex),返回一个Pattern对象。

2.静态方法matches(String regex,CharSequence input)用于整体匹配,返回一个boolean值。

上代码:
在这里插入图片描述
在这里插入图片描述
正则表达式只匹配部分,返回了false。
在这里插入图片描述
在这里插入图片描述
正则表达式在原先的基础上加上了至少匹配一个数字,结果返回true。

4.2 Matcher

Matcher类中方法众多,在这里放一个总览图,就不一一展示了。
在这里插入图片描述

4.3 PatternSyntaxException

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

5 实战演练

以下答案不唯一,仅供参考

T1.验证电子邮件格式是否合法
规定电子邮件规则为
1.只能有一个@
2.@前面是用户名,可以是a-z A-Z0-9_-字符
3.@后面是域名,并且域名只能是英文字母,比如sohu.com或者tsinghua.org.cn
4.写出对应的正则表达式,验证输入的字符串是否为满足规则

String regex1 = "^[a-zA-Z0-9_]+@([a-z]+\\.)+[a-zA-Z]+$";
String regex2 = "^[\\w_]+@([a-z]+\\.)+[a-z]+$";

T2.要求验证是不是整数或者小数
提示:这个题要考虑正数和负数
比如:123 -345 34.89 -87.9 -0.01 0.45等

String regex = "^-?(?:[1-9]\\d*|0)(\\.\\d+)*$";

T3.对一个url进行解析
https://www.sohu.com:8080/abc/index.html
a.要求得到协议是什么?
http
b.域名是什么?
www.sohu.com
c.端口是什么?
8080
d.文件名是什么?
index.html

public static void main(String[] args) {
        String content = "https://www.sohu.com:8080/abc/index.html";
        String regex = "^([a-zA-Z]+)://([a-zA-Z.]+):([1-9]\\d*)[\\w/]*/([\\w]+\\.[a-z]+)$";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(content);
        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));
            System.out.println("文件名:"+matcher.group(4));
        }
    }

结果:
在这里插入图片描述

T4.结巴去重
将字符串"java…是…是是.是…世界界…界上…最好的的.语语言"通过正则表达式转换为"java是世界上最好的语言"。

public static void main(String[] args) {
        String content = "java...是..是是.是.....世界界..界上..最好的的.语语言";
        String regex = "(.)";
        Pattern pattern = Pattern.compile("\\.");
        Matcher matcher = pattern.matcher(content);
        content = matcher.replaceAll("");
        System.out.println("去掉省略号后:"+ content);

        Pattern pattern1 = Pattern.compile("(.)\\1+");
        Matcher matcher1 = pattern1.matcher(content);
        content = matcher1.replaceAll("$1");
        System.out.println(("去重后:"+ content));
    }

结果:
在这里插入图片描述

到这里文章就结束了,欢迎各位批评指正,也别忘了点赞+评论+收藏

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到