Java魔法堂:深入正则表达式API

目录                              

一、前言

二、正则表达式的使用诉求

三、java.util.regex包

四、java.lang.String实例

五、最短路径实现诉求

六、Java支持的正则表达式功能语法

七、总结

八、参考

 

一、前言                             

  正则表达式作为文本处理的利器,早已成为各大语言的必要装备,但各语言对其的实现程度(功能语法支持程度)和API设计均有所差异,本篇将目光投向java原生类库中提供的正则表达式API—— java.util.regex包 和 java.lang.String实例方法 ,和所支持的功能语法上。

 

二、 正则表达式的使用诉求                    

  正则表达式一般用于处理如下诉求,本篇后续内容将以这些诉求为基础检验相关的原生API是否提供方便有效(code less,do more)的操作方式。

  1. 匹配字符串:全字符串匹配、部分匹配(也就是包含关系)

  2. 替换字符串

  3. 萃取字符串

  4. 拆分字符串

 

三、 java.util.regex包                        

  从jdk1.5开始正则表达式相关API就集中存放在该包下,且为其他类中表达式相关方法提供基础处理能力。

  1.  java.util.regex.Pattern类 :模式类,用于对正则表达式进行编译。

       类方法:

/*
 * 对正则表达式进行编译,并返回Pattern实例
 * 入参flag作为表达式属性,启用多个表达式属性时,采用管道符(|)连接多个表达式属性。除了通过入参的方式设置表达式属性,还可以使用嵌入式标识来设置表达式属性,格式为:(?表达式属性1表达式属性2)正则表达式,示例——不区分大小写和全局匹配abcd:(?ig)abcd
 */
Pattern compile(String regex);
Pattern compile(String regex, int flag);

// 字符串完整匹配表达式的快捷方式,内部依然是
// Pattern p = Pattern.compile(regex);
// p.matcher(input).matches();
boolean matches(String regex, CharSequence input);

// 返回可以配置入参s的字面量模式。注意格式为\\Q表达式\\E。表达式中的元字符将当作普通字符处理
String quote(String s);

      表达式属性:

// 以\n作为换行符,内嵌为(?d)
Pattern.UNIX_LINES

// US-ASCII编码字符不区分大小写,内嵌为(?i)
Pattern.CASE_INSENSITIVE

// 忽略空格和注释(注释为以#开头直到出现换行符),内嵌为(?x)
Pattern.COMMENTS

// 启动多行模式,^和$匹配换行符或字符串起始位置。默认为单行模式,^和$仅匹配字符串起始位置。内嵌为(?m)
Pattern.MULTILINE

// 字面量模式,将元字符当作普通字符处理,没有内嵌方式,但可以通过"\\Q正则表达式\\E"的方式实现
Pattern.LITERAL

// 元字符.将匹配换行符。默认情况下,元字符.不匹配换行符。内嵌为(?s)
Pattern.DOTALL

// UNICODE编码字符不区分大小写,内嵌为(?u)
Pattern.UNICODE_CASE

// 当且仅当正则分解匹配时才配置成功。
Pattern.CANON_EQ

// 启用Unicode版本的预定义字符类和POSIX字符类,内嵌为(?U)
Pattern.UNICODE_CHARACTER_CLASS

       实例方法:

// 返回正则表达式
String pattern();

// 使用正则表达式匹配的字符串切割入参input
// 入参limit用于设置返回数组长度的最大值,设置为0时则不限制最大值。
String[] split(CharSequence input);
String[] split(CharSequence input, int limit);

// 获取匹配类
Matcher matcher(CharSequence input);

   2. java.util.regex.Matcher类 :匹配类,用于存储模式实例匹配某字符串后所产生的结果。

       静态方法:

// 将入参s中的\和$元字符转换为普通字符,并返回处理后的s字符串。
String quoteReplacement(String s)

      实例方法:

// 获取匹配子字符串的起始索引
int start();
// 获取匹配子字符串的结束索引
int end();

// 从字符串的end+1位置开始搜索下一个匹配的字符串
boolean find();
boolean find(int start);

// 通过分组索引获取分组内容,若入参group超出分组数量则抛异常
String group();
String group(int group);
// 通过分组名称获取分组内容,若没有相应的分组则返回null
String group(String name);

// 重置匹配实例内部的状态属性
Matacher reset();
// 重置匹配实例内部的状态属性,并重置被匹配的字符串
Matacher reset(CharSequence input);
// 重置模式实例,这导致group信息丢失,但注意:start等信息依旧保留不变。
Matcher usePattern(Pattern newPattern);

// 从字符串起始位开始将匹配成功的子字符串均用入参replacement替换掉
String replaceAll(String replacement);
// 从字符串起始位开始将第一个匹配成功的子字符串均用入参replacement替换掉
String replaceFirst(String replacement);
// 将从字符串起始位开始到最后一匹配的子字符串最后一个字符的位置的字符串复制到sb中,并用入参replacement替换sb中匹配的内容
String appendReplace(StringBuffer sb, String replacement);
// 将剩余的子字符串复制到sb中
String appendTail(StringBuffer sb);
// 示例: sb为one dog two dog
Matcher m = p.matcher("one cat two cats in the yard");
StringBuffer sb = new StringBuffer();
while (m.find()) {
  m.appendReplacement(sb, "dog");
}

// 字符串从头到尾匹配表达式
boolean matches();
// 从字符串起始位置开始匹配表达式,但不要字符串从头到尾匹配表达式
boolean lookingAt();

 

四、 java.lang.String实例                         

   实例方法:



String replaceAll(String replacement);
String replaceFirst(String replacement);
String[] split(String regex);
String[] split(String regex, int limit);
boolean matches(String regex)

五、最短路径实现诉求                          

final class RegExp{
    // 全字符串匹配
    public static boolean isMatch(String regex, String input){
        if (null == input) return false;
        return input.matches(regex);
    }

    // 包含子字符串
    public static boolean contains(String regex, String input){
        Pattern r = Pattern.compile(regex);
        return r.matcher(input).find();
    }

    // 实现indexOf
    public static int indexOf(String regex, String input){
        Pattern r = Pattern.compile(regex);
        Matcher m = r.matcher(input);
        int index = -1;
        if(m.find())
            index = m.start();

        return index;
    }

    // 实现lastIndexOf
    public static int lastIndexOf(String regex, String input){
        Pattern r = Pattern.compile(regex);
        Matcher m = r.matcher(input);
        int index = -1;
        while(m.find())
            index = m.start();

        return index;
    }

    // 替换全部匹配字符串
    public static String replaceAll(String regex, String input, String replacement){
        if (null == regex || regex.isEmpty()) return input;
        return input.replaceAll(regex, replacement);
    }

    // 替换第N个匹配字符串
    public static String replaceSome(String regex, String input, String replacement, int n){
        if (null == regex || regex.isEmpty()) return input;
        if (0 == n) return input.replaceFirst(regex, replacement);

        Pattern r = Pattern.compile(regex);
        Matcher m = r.matcher(input);
        int i = 0;
        StringBuffer buffer = new StringBuffer();
        while (i <= n && m.find()){
            if (i == n){
                m.appendReplacement(buffer, replacement);
                m.appendTail(buffer);
            }
            ++i;
        }
        if (0 == buffer.length())
            buffer.append(input);

        return buffer.toString();
    }

    // 萃取字符串
    public static String extract(String regex, String input){
        String ret = "";
        Pattern r = Pattern.compile(regex);
        Matcher m = r.matcher(input);
        if (m.find())
            ret = m.group();

        return ret;
    }

    // 拆分字符串
    public static String[] split(String regex, String input, int limit){
        if (null == input || input.isEmpty()
                || null == regex || regex.isEmpty()) return new String[]{input};
        return input.split(regex, limit);
    }
}

   实际应用时当然不会像上面那么简单了。

 

六、Java支持的正则表达式功能语法                       

  本节内容仅针对正则表达式的高级功能语法进行叙述,而各语言的正则实现也就是这部分有所差异而已。

  1. 分组及反向引用

    [a].  (子表达式) ,自动命名分组(从1开始以数字自动为分组命名),后续表达式中可通过反向引用来获取该分组的内容。例如匹配字符串“so so”的正则表达式可以是 ^(\w{2})\s(\1)$ ,其中 \1 就是反向引用。

    [b]. (?:子表达式) ,非捕获分组,该类型的分组将不纳入匹配对象的group属性中,并且无法通过反向引用在表达式的后续部分获取该分组的内容。通常是配合 | 使用。例如匹配字符串"so easy"和"so hard"的正则表达式可以是 so\s(?:easy|hard) 

    [c].  (?<name>子表达式) ,命名分组,该类型的分组将纳入匹配对象的group属性中,并且可以在group属性值中通过name值来获取该分组的值。

    [d].  (?#注释) ,注释分组,该类型分组的内容将被正则表达式编译器忽略,仅供码农查阅而已。

                 

  2. 零宽先行断言

    零宽先行断言初看之下有点不知所云的感觉, 那么我们拆开来分析一下它的意思吧!

    零宽——意思是匹配的子表达式将不被纳入匹配结果,仅作为匹配条件而已。

    先行——意思是子表达式匹配的是后续字符串的内容。

    并且其细分为两类:

    [a].  子表达式B(?=子表达式A) ,零宽正向先行断言(也称为预搜索匹配)。例如匹配字符串"abcd"中的a和b的正则表达式可以是 \w(?=\w{2}) 

    [b].  子表达式B(?!子表达式A) ,零宽负向先行断言(也称为预搜索不匹配)。例如匹配字符串"abcd"中的c和d的正则表达式可以是 \w(?!\w{2}) 

 

  3. 零宽后行断言

    后行——意思是子表达式匹配的是前面字符串的内容。

     [a]. (?<=子表达式A)子表达式B ,零宽正向后行断言(也称为反向搜索匹配)。例如匹配字符串"abcd"中的c和d的正则表达式可以是 (?<=\w{2})\w 

     [b]. (?<!子表达式A)子表达式B ,零宽负向后行断言(也称为反向搜索不匹配)。例如匹配字符串"abcd"中的a和b的正则表达式可以是 (?<!\w{2})\w 

 

  4. 平衡组

    作用:用于匹配左右两边开始、结束符号数量对等的字符串。

   
示例——萃取"<div>parent<div>child</div></div><
/div>"的子字符串"<div>parent<div>child</div><
/div>"

    失败的正则表达式: <div>.*</div> ,匹配结果为"<div>parent<div>child</div></div></div>"。

    成功的正则表达式: ((?'g'<div>).*?)+(?'-g'</div>)+ ,匹配结果为"<div>parent<div>child</div></div>"。

    在分析上述示例前,我们要认识一下平衡组相关的语法。

      (?'name'子表达式A) ,若成功匹配子表达式A,则往名为name的栈空间压一个元素。

      (?'-name'子表达式A) ,若成功匹配子表达式A,则弹出名为name的栈空间的栈顶元素,弹出元素后若栈空间为空则结束匹配

      (?(name)yes表达式|no表达式) ,若名为name的栈空间非空,则使用yes表达式进行匹配,否则则使用no表达式进行匹配。

      (?(name)yes表达式) ,若名为name的栈空间非空,则使用yes表达式进行匹配。

      (?!) ,由于没有后缀表达式,因此总会导致匹配失败并结束匹配

    下面我们一起来分析 ((?'g'<div>).*?)+(?'-g'</div>)+ 的匹配流程吧!

<div>parent     # 步骤1,((?'g'<div>).*?)匹配成功,然后向g栈压入一个元素
<div>child      # 步骤2,((?'g'<div>).*?)匹配成功,然后向g栈压入一个元素,现在栈含2个元素
</div>          # 步骤3,(?'-g'</div>)匹配成功,然后弹出g栈的栈顶元素,现在栈含1个元素
</div>          # 步骤4,(?'-g'</div>)匹配成功,然后弹出g栈的栈顶元素,现在栈含0个元素
                # 步骤5,由于g栈为空因此结束匹配,返回<div>parent<div>child</div></div>

     从该例子我们可以知道平衡组可以解决一些棘手的文本处理问题。但遗憾的是直到JDK1.7的原生API依旧不支持平衡组的功能语法,其余功能语法均被支持。而.Net的Regex类则支持平衡组,在这方面显然全面一些。当然比js连零宽后行断言都不支持要强不少了。

     2015/10/30追加

       注意:若正则表达式仅含/()/、/(?:)/或/(?=)/,则匹配任何字符串均返回匹配成功,且配结果为空字符串。而JS中 RegExp('') 所生成的是无捕获分组 /(?:)/ 。

               而仅含/(?!)/,则匹配任务字符串均返回匹配失败。

console.log(RegExp('').test("12345")) // 显示true
console.log((?:)/.test("12345")) // 显示true
console.log(/(?=)/.test("12345")) // 显示true
console.log(/()/.test("12345")) // 显示true

console.log(/(?!)/.test("12345")) // 显示false

七、总结                                   

  到这里我们已经对Java对正则表达式的支持程度有一定程度的掌握,虽然不支持平衡组但已经为我们提供强大的文本处理能力了。不过我依旧不满意那个碍眼的转义符 \ ,假如我们要写正则表达式 \w\\\{\} 但实际运用时却要写成 \\w\\\\\\{\\} ,倘若能够像JS的正则表达式字面量一样使用,那就舒畅不少了!

时间: 2024-11-01 15:45:42

Java魔法堂:深入正则表达式API的相关文章

Java魔法堂:类加载器入了个门

一.前言   <Java魔法堂:类加载机制入了个门>中提及整个类加载流程中只有加载阶段作为码农的我们可以入手干预,其余均由JVM处理.本文将记录加载阶段的核心组件--类加载器的相关信息,以便日后查阅.若有纰漏请大家指正,谢谢.   注意:以下内容基于JDK7和HotSpot VM.   二.类加载器种类及其关系 从上图可知Java主要有4种类加载器 1. Bootstrap ClassLoader(引导类加载器):作为JVM的一部分无法在应用程序中直接引用,由C/C++实现(其他JVM可能通过

Java魔法堂:打包知识点之jar

一.前言      通过eclipse导出jar包十分方便快捷,但作为码农岂能满足GUI的便捷呢?所以一起来CLI吧!   二.JAR包   JAR包是基于ZIP文件格式,用于将多个.java文件和各种资源文件,或将多个.class和各种资源打包为一个文件.用于发布,部署,封装库.组件和插件程序,从而被编译器和JVM使用.   三.通过jar命令打包     1. 格式 jar [option]* 文件名        必选选项(并且仅能选用其中一个)        -c ,创建一个jar包  

Java魔法堂:找外援的利器——Runtime.exec详解

一.前言  Java虽然五脏俱全但总有软肋,譬如获取CPU等硬件信息,当然我们可以通过JNI调用C/C++来获取,但对于对C/C++和Windows API不熟的码农是一系列复杂的学习和踩坑过程.那能不能通过简单一些.学习成本低一些的方式呢?答案是肯定的,在功能实现放在首位的情况下,借他山之石 是最简洁有力的做法.而 Runtime.exec方法 就为我们打开这么的一条路了.   二.认识 java.lang.Runtime.exec方法    作用:用于调用外部程序,并重定向外部程序的标准输入

Java魔法堂:自定义和解析注解

一.前言   注解(Annotation)作为元数据的载体,为程序代码本身提供额外的信息,使用过MyBatis等ORM框架的朋友对 @Insert 的注解应该不陌生了,这是MyBatis自定义的注解,显然我们也可以按需求自定义一些注解,然后对其进行解析获取元数据,进而实现通过代码生成代码的操作.   二.自定义注解  只需通过 关键字@interface 即可自定义注解 // 标识注解(就是无属性的注解) public @interface AnnotationWithoutProperty{

Java魔法堂:注解用法详解——@SuppressWarnings

一.前言   编码时我们总会发现如下变量未被使用的警告提示:   上述代码编译通过且可以运行,但每行前面的"感叹号"就严重阻碍了我们判断该行是否设置的断点了.这时我们可以在方法前添加 @SuppressWarnings("unused") 去除这些"感叹号".   二. @SuppressWarings注解    作用:用于抑制编译器产生警告信息.   示例1--抑制单类型的警告: @SuppressWarnings("unchecke

Java魔法堂:打包知识点之META-INF/MAINFEST.MF(转)

一.前言 通过执行形如 jar -cvf src.jar src 命令将多个.class文件打包成JAR包时,你会发现JAR包中除了src目录外还多了个MATE-INF/MAINFEST.MF,那是为什么呢? 其实关于JAR包的描述信息.启动时的配置信息和安全性信息等均保存在META-INF下,因此了解META-INF目录对发布.部署十分重要的哦!下面仅为部分内容的整理,待日后深入实践后再补充完整.官方文档为:http://docs.oracle.com/javase/7/docs/techno

Java魔法堂:枚举类型详解

一.前言   Java的枚举类型相对C#来说具有更灵活可配置性,Java的枚举类型可以携带更多的信息. // C# enum MyColor{ RED = 0, BLUE = 1 } Console.Write(MyColor.RED); // Java enum MyColor{ RED("Hot", 4), BLUE("SAD",8); private String mood; public String getMood{ return mood; } priv

Java魔法堂:URI、URL(含URL Protocol Handler)和URN

一.前言   过去一直搞不清什么是URI什么是URL,现在是时候好好弄清楚它们了!本文作为学习笔记,以便日后查询,若有纰漏请大家指正!   二.从URI说起    1. 概念   URI(Uniform Resource Identifier,统一资源标识符)以字符串来表示某种资源的统一资源标识.   格式为: [scheme:]scheme-specific-part[#fragment]    [scheme:]组件 ,URI的名称空间标识.   scheme-specific-part组件

Java魔法堂:初探MessageFormat.format和ChoiceFormat

一.前言   刚开始从.net的转向java的时候总觉得 String.format 用得不习惯,希望格式模版会这样 {0}, this is {1}'s cat.{1},this is {0}'s dog. 而不是 %1$s,this is %2$s's cat.%2$s,this is %1$s's dog. .后来发现 java.text.MessageFormat.format 可以满足我这个小小的愿望.   二.静态方法 java.text.MessageFormat.format