Java 正则表达式【匹配与分组基本原理】

简介: Java 正则表达式【匹配与分组基本原理】

简介

       我们一般使用正则表达式是用来处理字符串的,不管是实际的开发中还是我们的算法竞赛中,使用正则表达式绝对可以大大提升我们的效率。


      正则表达式(regular expression)其实就是对字符串进行模式匹配的技术。


快速入门

我们这里演示一个案例,使用正则表达式匹配下面字符串中的所有英文单词:

import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexDemo01 {
    public static void main(String[] args) {
        String content = "1995年,互联网的蓬勃发展给了Oak机会。业界为了使死板、单调的" +
                "静态网页能够“灵活”起来,急需一种软件技术来开发一种程序,这种程序可以通过" +
                "网络传播并且能够跨平台运行。于是,世界各大IT企业为此纷纷投入了大量的人力" +
                "、物力和财力。这个时候,Sun公司想起了那个被搁置起来很久的Oak,并且重新审" +
                "视了那个用软件编写的试验平台,由于它是按照嵌入式系统硬件平台体系结构进行编" +
                "写的,所以非常小,特别适用于网络上的传输系统,而Oak也是一种精简的语言,程" +
                "序非常小,适合在网络上传输。Sun公司首先推出了可以嵌入网页并且可以随同网页" +
                "在网络上传输的Applet(Applet是一种将小程序嵌入到网页中进行执行的技术)" +
                ",并将Oak更名为Java。5月23日,Sun公司在Sun world会议上正式发布Java和" +
                "HotJava浏览器。IBM、Apple、DEC、Adobe、HP、Oracle、Netscape和微软" +
                "等各大公司都纷纷停止了自己的相关开发项目,竞相购买了Java使用许可证,并为自" +
                "己的产品开发了相应的Java平台。";
        // 提取文章中所有英文单词
        Pattern pattern = Pattern.compile("[a-zA-Z]+");
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()){
            // 匹配到的内容都会放到 matcher.group(0)里面
            System.out.println(matcher.group(0));
        }
    }
}

运行结果:

Oak
IT
Sun
Oak
Oak
Sun
Applet
Applet
Oak
Java
Sun
Sun
world
Java
HotJava
IBM
Apple
DEC
Adobe
HP
Oracle
Netscape
Java
Java

同理,如果我们想要获取字符串中所有的数字获取字符串中所有字符串或者数字,只需要这样修改:

        Pattern pattern = Pattern.compile("[0-9]+");
        Pattern pattern = Pattern.compile("([0-9]+)|([a-zA-Z]+)");

     要知道,我们自己实现的话会很复杂,我自己尝试过,比如实现提取所有英文字符,我们需要这样来设计:

  • 把字符串转为 char 数组
  • 遍历数组,判断字符的 ASCII码是否在 [a,z] , [A,Z]范围内
  • 判断英文字符前的字符是否为非英文字符,如果是在字符前添加空格(防止结果连成一片)
  • 判断英文字符后的字符是否为非英文字符,如果是在字符后添加空格(防止结果连成一片)

       可以看到,我们自己编写程序去实现确实是十分复杂,所以为什么不学习正则表达式呢,取英文字符是比较简单的案例,如果遇到验证邮箱、手机号、身份证号、ip地址、提取字符串等需要各种字符串处理算法的时候,手写算法是十分烧脑的,最好的办法就是找到规律使用正则表达式,提高开发效率!              


除此之外,我们学习的网络爬虫在做数据处理的时候,比如各种新闻标题、产品标题、商品评论,这些文本通常都是在超链接或者一些特殊标签内部,这时候我们直接使用正则表达式就可以很轻松地实现标签内文本的提取,这样,我们只需要专心爬虫的代码,而不需要过于担心爬到数据后的数据处理问题了。

底层实现

       我们这里主要讨论一下 Java正则表达式中,matcher.find()matcher.group(int group) 的底层是怎么实现的。

案例

找出四个数字连在一起的子串

1998年12月8日,第二代Java平台的企业版J2EE发布。1999年6月,Sun公司发布了第二代Java平台(简称为
Java2)的3个版本:J2ME(Java2 Micro Edition,Java2平台的微型版),应用于移动、无线及有限资源的
环境;J2SE(Java 2 Standard Edition,Java 2平台的标准版),应用于桌面环境;J2EE(Java   
 2Enterprise Edition,Java 2平台的企业版),应用3443于基于Java的应用服务器。Java 2平台的发布
,是Java发展过程中最重要的一个里程碑,标志着Java的应用开始普及9889

代码:

// 1. 找出四个数字连在一起的子串
        // \\d 代表数字
        String regex = "\\d\\d\\d\\d";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()){
            System.out.println(matcher.group());   // 无参时默认就是 group(0)
        }


输出结果:

1998
1999
3443
9889

我们开始分析代码以及通过debug分析源码:

matcher.find() & matcher.group() 原理

matcher.group(0)

  • 首先,根据我们给它的规则去匹配,定位到满足要求的子字符串位置(比如1998)
  • 然后,将子字符串的开始的索引记录到 matcher 对象的属性中去(int[] groups)。
  • groups[0] = 0 , 把该子字符串结束的索引+1的值记录到 groups[1]中去,groups[1] = 4

我们Matcher类的属性 int[] groups 的初始大小为 20 ,初始值均为 -1.

我们Matcher类的属性 int[] groups 的初始大小为 20 ,初始值均为 -1.

  • 同时记录属性oldLast 的值为 该子字符串结束的索引+1的值, 即 4,即下次执行matcher.find() 的时候从 4 开始。
  • 接下来我们分析 matcher.group(0) 的源码:
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();
    }

此时我们调用 mactcher.group(0) 的话,很明显会返回 getSubSequence(groups[0],groups[1]).toString(); 相当于根据 groups[0]=0 和 groups[1]=4 记录的位置来截取字符串 ,注意是左闭右开的 [0,4)。 此时,输出 1998.

继续下一次 matcher.find() 方法,这次定位到了 1999 这个位置:

  • 此时,groups[0] = 31, groups[1] = 35 ,oldLast = 35 。
  • 同样再次调用 matcher.group(0) , 输出 1999

到这里,我们基本了解了 group(0) 的含义,每次调用matcher.group(0) 它都会去查找字符串中的子串首尾下标,这些下标存在 groups数组中 ,而且首下标永远都是 groups[0] ,尾下标永远都是 groups[1] 。


但是如果我们调用的是 group(1)、或者group(n) 又会是怎样的情况呢?

matcher.group(n)

其实,group(n) 涉及到的是一个分组的概念,体现在代码中的匹配语句上就是括号,我们对上面的匹配语句做一个修改:

String regex = "(\\d\\d)(\\d\\d)";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()){
            System.out.println(matcher.group());   // 无参时默认就是 group(0)
            System.out.println(matcher.group(1));
            System.out.println(matcher.group(2));
        }

输出结果:

group(0) = 1998
group(1) = 19
group(2) = 98
group(0) = 1999
group(1) = 19
group(2) = 99
group(0) = 3443
group(1) = 34
group(2) = 43
group(0) = 9889
group(1) = 98
group(2) = 89

此时,我们再从 matcher.find()  开始分析:

  • 首先,根据我们给它的规则去匹配,定位到满足要求的子字符串位置(比如(19)(98)
  • 然后,将子字符串的开始的索引记录到 matcher 对象的属性中去(int[] groups)。


  • groups[0] = 0 , 把该子字符串结束的索引+1的值记录到 groups[1]中去,groups[1] = 4
  • 记录1组()匹配到的子串下标到 groups[2] = 0 , groups[3] = 2。
  • 记录2组()匹配到的子串下标到 groups[4] = 2 , groups[5] = 4。
  • 如果还有更多组,以此类推...

到这里,我们基本了解了分组的实现原理:groups数组负责存储子字符串的下标以及子字符串内每组子串的首尾下标,我们的 getSubSequence(groups[group * 2], groups[group * 2 + 1]) 方法会去根据 matcher.group(int group) 给的参数 group 去查找对应groups数组的首尾下标,从而调用String.substring(start,end) 截取出每组对应的子串。

不得不说,getSubSequence(groups[group * 2], groups[group * 2 + 1]),这个参数的设置确实十分巧妙!

总结

如果正则表达式中有() 即分组

取出匹配的字符串规则如下:

  • group(0) 代表匹配到的子字符串,不分组
  • group(1) 代表匹配到的子字符串第1组
  • group(2) 代表匹配到的子字符串第2组
  • ...
  • 但是参数不能越界,不能超过分组数
目录
打赏
0
0
0
0
37
分享
相关文章
【原理】【Java并发】【synchronized】适合中学者体质的synchronized原理
本文深入解析了Java中`synchronized`关键字的底层原理,从代码块与方法修饰的区别到锁升级机制,内容详尽。通过`monitorenter`和`monitorexit`指令,阐述了`synchronized`实现原子性、有序性和可见性的原理。同时,详细分析了锁升级流程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,结合对象头`MarkWord`的变化,揭示JVM优化锁性能的策略。此外,还探讨了Monitor的内部结构及线程竞争锁的过程,并介绍了锁消除与锁粗化等优化手段。最后,结合实际案例,帮助读者全面理解`synchronized`在并发编程中的作用与细节。
50 8
【原理】【Java并发】【synchronized】适合中学者体质的synchronized原理
|
24天前
|
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是写出高端的CRUD应用。2025年,我正在沉淀自己,博客更新速度也在加快。在这里,我会分享关于Java并发编程的深入理解,尤其是volatile关键字的底层原理。 本文将带你深入了解Java内存模型(JMM),解释volatile如何通过内存屏障和缓存一致性协议确保可见性和有序性,同时探讨其局限性及优化方案。欢迎订阅专栏《在2B工作中寻求并发是否搞错了什么》,一起探索并发编程的奥秘! 关注我,点赞、收藏、评论,跟上更新节奏,让我们共同进步!
92 8
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
JVM实战—1.Java代码的运行原理
本文介绍了Java代码的运行机制、JVM类加载机制、JVM内存区域及其作用、垃圾回收机制,并汇总了一些常见问题。
JVM实战—1.Java代码的运行原理
|
20天前
|
怎么理解Java中的lambda表达式
Lambda表达式是JDK8引入的新语法,用于简化匿名内部类的代码写法。其格式为`(参数列表) -&gt; { 方法体 }`,适用于函数式接口(仅含一个抽象方法的接口)。通过Lambda表达式,代码更简洁灵活,提升Java的表达能力。
|
24天前
|
《从头开始学java,一天一个知识点》之:运算符与表达式:算术、比较和逻辑运算
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列为你打造Java「速效救心丸」,每天1分钟,地铁通勤、午休间隙即可完成学习。直击高频考点和实际开发中的「坑位」,拒绝冗长概念,每篇都有可运行的代码示例。明日预告:《控制流程:if-else条件语句实战》。
32 6
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
【JAVA】生成accessToken原理
在Java中,生成accessToken用于身份验证和授权,确保合法用户访问受保护资源。流程包括:1. 身份验证(如用户名密码、OAuth 2.0);2. 生成唯一且安全的令牌;3. 设置令牌有效期并存储;4. 客户端传递令牌,服务器验证其有效性。常见场景为OAuth 2.0协议,涉及客户端注册、用户授权、获取授权码和换取accessToken。示例代码展示了使用Apache HttpClient库模拟OAuth 2.0获取accessToken的过程。
|
3月前
|
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
85 3
|
3月前
|
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
118 2
Java 中的正则表达式
正则表达式是Java中强大的文本处理工具,支持灵活的匹配、搜索、替换和验证功能。本文介绍了正则表达式的语法基础及其在Java中的应用,包括字符串匹配、替换、分割及实际场景中的邮箱验证和电话号码提取等示例。通过这些技术,可以显著提高文本处理的效率和准确性。
320 8