Day46.正则表达式(RegExp)详解、RegExp效率与优化

目录

正则表达式(regular expression) 

元字符

字符匹配符、预定义字符类

选择匹配符 

限定符

定位符

非命名分组、命名分组

特别分组(了解)

两个常用类 :Pattern、Matcher

分组、捕获、反向引用

String 类中使用正则表达式

练习题

关于优化

如何避免NFA自动机回溯问题?

1.贪婪模式(Greedy)

2.懒惰模式(Reluctant)“?”

3.独占模式(Possessive)

五.正则表达式的优化


正则表达式(regular expression) 

  • 正则表达式の初体验

注意:正则表达式中空格不能随便用!!

    public static void main(String[] args) {
        //假定,编写了爬虫,从百度页面得到了如下文本
        String content = "1995年,互联网的蓬勃发展给了Oak机会。业界为了使死板、单调的静态网页能够" +
                "“灵活”起来,急需一种软件技术来开发一种程序,这种程序可以通过网络传播并且能够跨平台运行。" +
                "于是,世界各大IT企业为此纷纷投入了大量的人力、物力和财力。这个时候,Sun公司想起了那个...." ;

        //1.创建一个Pattern 模式对象,可以理解成正则表达式对象
        //Pattern pattern = Pattern.compile("[a-zA-Z]+"); //找英文
        //Pattern pattern = Pattern.compile("[1-9]+"); //找数字
        Pattern pattern = Pattern.compile("([1-9]+)|([a-zA-Z]+)"); //找英文和数字

        //2.创建一个匹配器对象
        // Matcher按照 pattern 去匹配,传入 String字符串
        Matcher matcher = pattern.matcher(content);
        //3.循环匹配
        while (matcher.find()){
            System.out.println("找到:"+matcher.group(0));
        }
    }
  • 底层实现
    public static void main(String[] args) {
        String content = "1998年12月8日,第二代Java平台的企业版J2EE发布。1999年6月," +
                "Sun公司发布了第二代Java平台(简称为Java2)的3个版本:J2ME(Java2 Mi" +
                "cro Edition,Java2平台的微型版),应用于移动、无线及有限资源的环境;J" +
                "2SE(Java 2 Standard Edition,Java 2平台的标准版),应用于桌面环境" +
                ";J2EE(Java 2Enterprise Edition,Java 2平台的企业版),应用于基...";
        //目标:匹配所有的四个数字
        //说明
        //1. \d 表示任意的数字
        String regStr = "(\\d\\d)(\\d\\d)";    //分为2组 正则表达式中()表示分组
        //2. 创建一个模式对象
        Pattern pattern = Pattern.compile(regStr);
        //3. 创建匹配器
        //说明: 创建匹配器matcher,按照 正则表达式的规则 去匹配 content 字符串
        Matcher matcher = pattern.matcher(content);

        //4.开始匹配
        /**
         * matcher.find() 完成的任务 (考虑分组)
         * 1. 根据指定的规则,定位满足规则的子字符串(比如(19)(98)),
         * 2. 找到后,将子字符串的开始索引记录在 matcher对象的 int[] groups;中,
         *    2.1 groups[0] = 0 ,把该子字符串结束的 index 记录到 groups[1] = 4;
         *    2.2 记录第一组()匹配到的字符串 groups[2] = 0 groups[3] = 2
         *    2.3 记录第二组()匹配到的字符串 groups[4] = 2 groups[5] = 4
         * 3. 同时记录 oldLast 的值为字符串结束的 index,
         * 4.下次执行find时,就从 oldLast 开始匹配
         *
         *  matcher.group(0) 分析
         * 1. 根据 groups[0] 和 groups[1] 的记录的位置,从content开始截取子字符串返回
         *    [0,4)
         *
         *   如果再次执行 find方法,仍然按上面的分析执行
        * */
        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)); 越界
        }
    }

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第1张图片

小结
1. 如果正则表达式使用() 分组
2. matcher.find() 方法会记录每组起始与结尾的 index
3. matcher.group(0) 表示匹配到的整体的子字符串
4. matcher.group(1) 表示匹配到的第一组()
5. matcher.group(2) 表示匹配到的第二组()
6. ... 但是不能不能越界(大于分组数)  IndexOutOfBoundsException

 geoup源码

    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个或多个,?表示0个或1个,*表示0或多个

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第2张图片
Day46.正则表达式(RegExp)详解、RegExp效率与优化_第3张图片
Day46.正则表达式(RegExp)详解、RegExp效率与优化_第4张图片

POSIX 字符类(仅 US-ASCII)

\p{Lower}小写字母字符:[a-z]

\p{Upper}大写字母字符:[A-Z]

\p{ASCII}所有 ASCII:[\x00-\x7F]

\p{Alpha}字母字符:[\p{Lower}\p{Upper}]

\p{Digit} 十进制数字:[0-9]

\p{Alnum}字母数字字符:[\p{Alpha}\p{Digit}]

\p{Punct} 标点符号:!"#$%&'()*+,-./:;<=>?@[]^_`{|}~

\p{Blank} 空格或制表符:[ \t]

    public static void main(String[] args) {
        String content = "a11c8_abcABC  @";
        String regStr1 = "[a-z]";    //匹配a-z之间的任意字符
        String regStr2 = "[A-Z]";    //匹配A-Z之间的任意字符
        String regStr3 = "abc";      //匹配 abc 字符串,[默认匹配大小写]
        String regStr4 = "(?i)abc";  //匹配 abc 字符串,[不区分大小写]
        String regStr5 = "[0-9]";    //匹配 0-9 之间任意一个字符
        String regStr6 = "[!0-9]";   //匹配 不在 0-9 之间任意一个字符
        String regStr7 = "\\D";      //匹配 不在 0-9 之间任意一个字符
        String regStr8 = "\\w";      //匹配 大小写字母、数字、下划线
        String regStr9 = "\\W";      //匹配 等价于[^a-zA-Z0-9]
        String regStr0 = "\\s";      //匹配 空白字符(空格,制表符)
        String regStrA = "\\S";      //匹配 非空白字符
        String regStrB = ".";          //匹配 除\n之外所有字符,如果要匹配. 需要用\\.

        Pattern pattern = Pattern.compile(regStr);
        // Pattern.CASE_INSENSITIVE 表示不区分字母大小写
        //Pattern pattern = Pattern.compile(regStr,Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()){
            System.out.println("找到:"+ matcher.group(0));
        }
    }

选择匹配符 

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第5张图片

        String content = "茴香豆的四种写法:回 囘 囬 廻";
        String regStr = "回|囘|囬|廻";    //| 选择匹配
        //String regStr = "[回囘囬廻]"; 相同效果??

        Pattern compile = Pattern.compile(regStr);
        Matcher matcher = compile.matcher(content);
        while (matcher.find()){
            System.out.println("找到:"+matcher.group(0));
        }

限定符

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第6张图片
注意:当 ? 紧跟其他限定符匹配模式时,匹配模式为“懒惰模式” 详见(关于优化)

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第7张图片

 关于贪婪匹配Day46.正则表达式(RegExp)详解、RegExp效率与优化_第8张图片

        String content = "111113ka1saaaaaaHello";

        String regStr1 = "a{3}";   //等于"aaa";
        String regStr2 = "1{4}";   //等于"1111";
        String regStr3 = "\\d{2}";   //匹配2位的任意数字;

        细节:java匹配默认贪婪匹配,尽可能匹配多的
        String regStr4 = "a{3,4}";   //匹配aaa或aaaa;
        //关闭贪婪模式 开启懒惰模式
        String regStr5 = "a{3,4}?";

        String regStr6 = "\\d{2,5}";   //匹配2-5位数字组合
        
        //1+
        String regStr7 = "1+";   //匹配一个1或者多个1

        //关于?的使用,遵守贪婪匹配
        String regStr8 = "a1?";  //匹配 a 或者 a1

        Pattern compile = Pattern.compile(regStr);
        Matcher matcher = compile.matcher(content);
        while (matcher.find()){
            System.out.println("找到:"+matcher.group(0));
        }
    }

定位符

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第9张图片

    public static void main(String[] args) {
        String content = "123abc hanshunping sphan nnhan";
        //至少1数字开头,后接任意小写字母
        String regStr1 = "^[0-9]+[a-z]*";
        //至少1数字开头,至少一个小写字母结尾
        String regStr2 = "^[0-9]+[a-z]+$";

        //匹配边界的han  边界:匹配到字符串的最后、空格的子字符串最后
        String regStr3 = "han\\b";
        //匹配边界的han  开头的 , 与\\b相反
        String regStr4 = "han\\B";

        Pattern compile = Pattern.compile(regStr3);
        Matcher matcher = compile.matcher(content);
        while (matcher.find()){
            System.out.println("找到:"+matcher.group(0));
        }
    }

非命名分组、命名分组

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第10张图片

    public static void main(String[] args) {
        String content = "hanshunping s7789 nn1189han";

        //非命名分组  通过matcher.group(?)获取
        String regStr = "(\\d\\d)(\\d\\d)";

        //分组命名  可通过matcher.group(组名)获取
        String regStr1 = "(?\\d\\d)(?\\d\\d)";

        Pattern compile = Pattern.compile(regStr1);
        Matcher matcher = compile.matcher(content);
        while (matcher.find()){
            System.out.println("找到:"+matcher.group(0));
            System.out.println("第1个分组内容:"+matcher.group(1));
            System.out.println("第2个分组内容:"+matcher.group(2));
            //命名分组
            System.out.println("第1个分组内容:"+matcher.group("g1"));
            System.out.println("第2个分组内容:"+matcher.group("g2"));
        }
    }

 group 重载方法 可通过组数、组名获取匹配字符串 

 

特别分组(了解)

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第11张图片

    public static void main(String[] args) {

        String content1 = "hello韩顺平教育 jack韩顺平老师 韩顺平同学hello";

        String regStr1 = "韩顺平教育|韩顺平老师|韩顺平同学";
        //非捕获分组 等价于上面的写法
        String regStr2 = "韩顺平(?:教育|老师|同学)";

        Pattern compile = Pattern.compile(regStr2);
        Matcher matcher = compile.matcher(content1);
        while (matcher.find()){
            System.out.println("找到:"+matcher.group(0));
            //非捕获,所以会报错
            //System.out.println("找到:"+matcher.group(1));
        }
    }
    public void test(){

        String content1 = "hello韩顺平教育 jack韩顺平老师 韩顺平同学hello";

        //只查找韩顺平教育、韩顺平老师
        String regStr3 = "韩顺平(?=教育|老师|同学)";
        //不查找韩顺平教育、韩顺平老师
        String regStr4 = "韩顺平(?!教育|老师)";

        Pattern compile = Pattern.compile(regStr3);
        Matcher matcher = compile.matcher(content1);
        while (matcher.find()){
            System.out.println("找到:"+matcher.group(0));
            //非捕获,所以会报错
            //System.out.println("找到:"+matcher.group(1));
        }
    }

两个常用类 :Pattern、Matcher

Pattern 类

 Matcher matcher(CharSequence input) 
          创建匹配给定输入与此模式的匹配器。
static boolean matches(String regex, CharSequence input) 整体匹配
          编译给定正则表达式并尝试将给定输入与其匹配。
 String[] split(CharSequence input)
          围绕此模式的匹配拆分给定输入序列。
 String[] split(CharSequence input, int limit)
          围绕此模式的匹配拆分给定输入序列。

Matcher 类

 boolean find()
          尝试查找与该模式匹配的输入序列的下一个子序列。
 boolean find(int start)
          重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。
 String group()
          返回由以前匹配操作所匹配的输入子序列。
 String group(int group)
          返回在以前匹配操作期间由给定组捕获的输入子序列。
 String replaceAll(String replacement)
          替换模式与给定替换字符串相匹配的输入序列的每个子序列。
 int start()
          返回以前匹配的初始索引。
 int start(int group)
          返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引。
 int end()
          返回最后匹配字符之后的偏移量。
 int end(int group)
          返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。
 int groupCount()
          返回此匹配器模式中的捕获组数。

分组、捕获、反向引用

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第12张图片

Day46.正则表达式(RegExp)详解、RegExp效率与优化_第13张图片

  • 结巴程序 代码:
    public static void main(String[] args) {
        String content = "我....我要....学学学学....编程java!";

        //1. 去掉所有的.
        Pattern pattern = Pattern.compile("\\.");
        Matcher matcher = pattern.matcher(content);
        content = matcher.replaceAll("");

        System.out.println("1.content: " + content);

        //2.去掉重复的字
        //思路
        //(1) 使用(.)\\1+  匹配重复的字
        pattern = Pattern.compile("(.)\\1+");
        matcher = pattern.matcher(content);
        while (matcher.find()){
            System.out.println(matcher.group(0));
        }
        //(2) 使用反向引用 替换匹配到的内容 "我我" 替换成 "我" 内部反向引用\\租号  外部引用$组号
        content = matcher.replaceAll("$1");
        System.out.println("2.content: " + content);

        //简化代码  "我我" 替换成 "我"  "学学学学" 替换成 "学"
        String $1 = Pattern.compile("(.)\\1").matcher(content).replaceAll("$1");

    }

String 类中使用正则表达式

 String replaceAll(String regex, String replacement)
          使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
 String replaceFirst(String regex, String replacement)
          使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
boolean matches(String regex)   (整体匹配) 底层仍然使用Pattern 和 Matcher
          告知此字符串是否匹配给定的正则表达式。
 String[] split(String regex)
          根据给定正则表达式的匹配拆分此字符串。
 String[] split(String regex, int limit)
          根据匹配给定的正则表达式来拆分此字符串。
        String content = "2000年5月,JDK1.3、JDK1.4和J2SE1.3相继发布" +
                ",几周后其获得了Apple公司Mac OS X的工业标准的支持。" +
                "2001年9月24日,J2EE1.3发布。2002年2月26日,J2SE1.4" +
                "发布。自此Java的计算能力有了大幅提升";

        //1.使用正则表达式 将JDK1.3 和 JDK1.4 替换成 JDK

        content = content.replaceAll("JDK1.3|JDK1.4","JDK");
        System.out.println(content);

        //2.要求 验证一个 手机号,要求必须是以 138,139 开头的
        content = "13933330123";
        if (content.matches("1(38|39)\\d{8}")) {
            System.out.println("验证成功");
        }else {
            System.out.println("验证失败");
        }

        //3.要求按照 # 或者 - 或者 ~ 或者 数字 来分割
        content = "hello#abc-jack12smith~北京";
        String[] split = content.split("[#~-] | \\d+");
        System.out.println(Arrays.toString(split));

练习题

    //验证电子邮件 只能有一个 @
    //1、只能有一个@
    //2、@前面可以是a-z A-Z 0-9_字符
    //3、@后面是域名,并且域名只能是英文字母,比如sohu.com 或者 tsinghua.org.cn
    //4、写出对应的正则表达式,验证输入的字符串是否为满足规则
    public static void main(String[] args) {
        String mail = "[email protected]";
        String regStr = "[\\w]+@([a-zA-Z]+\\.)+(com|cn)";

        //String 的 matches 是整体匹配,所以可以不带^与$
        if(mail.matches(regStr)){
            System.out.println("匹配成功");
        }
    }
    //要求验证是不是整数或者小数
    //提示: 考虑正数和负数 小数 不合理整数
    //比如 123 - 345 34.89 -87.9 -0.01 0.45 000.12 等
    @Test
    public void test (){
        //解题思路:由简入繁
        String content = "-1.231"; //00.89 如何规范:[1-9开头]
        //注意:正则表达式中空格不能随便用!!!
        String regStr = "[-]?([1-9]\\d*|0)(\\.\\d+)?";

        if(content.matches(regStr)){
            System.out.println("匹配成功");
        }
    }
    //对一个url进行解析
    //http://www.sohu.com:8080/abc/index.htm
    //1: 要求得到协议是什么  http
    //2: 域名是什么?       www.shohu.com
    //3: 端口是什么?       8080
    //4: 文件名是什么?     index.htm
    @Test
    public void test1(){
        String content = "http://www.sohu.com:8080/abc/xxx/yyy/index.htm";
        //思路: 分组 分为4组 依次判断  关键: 找到定位的符号 :/  :  /
        String regStr = "^([a-zA-Z]+)://([a-zA-Z.]+):(\\d+)[\\w-/]*/([\\w.]+)$";

        Pattern pattern = Pattern.compile(regStr);

        Matcher matcher = pattern.matcher(content);
        if(matcher.matches()){  //整体匹配,如果成功可以通过group(x)获取对应分组内容
            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));
        }else {
            System.out.println("匹配失败");
        }

    }

关于优化

如何避免NFA自动机回溯问题?

既然回溯会给系统带来性能开销,那我们如何应对呢?如果你有仔细看上面那个案例的话,你会发现 NFA 自动机的贪婪特性就是导火索,这和正则表达式的匹配模式息息相关。

1.贪婪模式(Greedy)

顾名思义,就是在数量匹配中,如果单独使用 +、?、*或(min,max)等量词,正则表达式会匹配尽可能多的内容。

例如,上面那个例子:

text = "abbc"
regex = "ab{1,3}c"

 就是在贪婪模式下,NFA自动机读取了最大的匹配范围,即匹配 3 个 b 字符。匹配发生了一次失败,就引起了一次回溯。如果匹配结果是“abbbc”,就会匹配成功。

text = "abbbc"
regex = "ab{1,3}c"

2.懒惰模式(Reluctant)“?”

在该模式下,正则表达式会尽可能少地重复匹配字符,如果匹配成功,它会继续匹配剩余的字符串。

例如,上面的例子的字符后面加一个“?”,就可以开启懒惰模式。

text = "abc"
regex = "ab{1,3}?c"

匹配结果是“abc”,该模式下 NFA 自动机首先选择最小的匹配范围,即匹配 1 个 b 字符,因此就避免了回溯问题。

3.独占模式(Possessive)

同贪婪模式一样,独占模式一样会最大限度地匹配更多内容;不同的是,在独占模式下,匹配失败就会结束匹配,不会发生回溯问题。

还是上面的例子,在字符后面加一个“+”,就可以开启独占模式。

text = "abbc"
regex = "ab{1,3}+c"

结果是不匹配,结束匹配,不会发生回溯问题。

所以综上所述,避免回溯的方法就是:使用懒惰模式或独占模式。

前面讲述了“Split() 方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题。”,比如使用了 split 方法提取域名,并检查请求参数是否符合规定。

split 在匹配分组时遇到特殊字符产生了大量回溯,解决办法就是在正则表达式后加一个需要匹配的字符和“+”解决了回溯问题:

\\?(([A-Za-z0-9-~_=%]++\\&{0,1})+)

五.正则表达式的优化

1.少用贪婪模式:多用贪婪模式会引起回溯问题,可以使用独占模式来避免回溯。

2.减少分支选择:分支选择类型 “(X|Y|Z)” 的正则表达式会降低性能,在开发的时候要尽量减少使用。如果一定要用,可以通过以下几种方式来优化:

1)考虑选择的顺序,将比较常用的选择项放在前面,使他们可以较快地被匹配;

2)可以尝试提取共用模式,例如,将 “(abcd|abef)” 替换为 “ab(cd|ef)” ,后者匹配速度较快,因为 NFA 自动机会尝试匹配 ab,如果没有找到,就不会再尝试任何选项;

3)如果是简单的分支选择类型,可以用三次 index 代替 “(X|Y|Z)” ,如果测试话,你就会发现三次 index 的效率要比 “(X|Y|Z)” 高一些。推荐:Java面试练题宝典

3.减少捕获嵌套 :

捕获组是指把正则表达式中,子表达式匹配的内容保存到以数字编号或显式命名的数组中,方便后面引用。一般一个()就是一个捕获组,捕获组可以进行嵌套。

非捕获组则是指参与匹配却不进行分组编号的捕获组,其表达式一般由(?:exp)组成。

在正则表达式中,每个捕获组都有一个编号,编号 0 代表整个匹配到的内容。可以看看下面的例子:

public static void main(String[] args) {
        String text = "test";
        String reg = "()(.*?)()";
        Pattern p = Pattern.compile(reg);
        Matcher m = p.matcher(text);
        while (m.find()){
            System.out.println(m.group(0));//整个匹配到的内容
            System.out.println(m.group(1));//
            System.out.println(m.group(2));//(.*?)
            System.out.println(m.group(3));//()
        }
    }
//=====运行结果=====
//test
//
//test
//

如果你并不需要获取某一个分组内的文本,那么就使用非捕获组,例如,使用 “(?:x)” 代替 “(X)” ,例如下面的例子:

public static void main(String[] args) {
        String text = "test";
        String reg = "(?:)(.*?)(?:)";
        Pattern p = Pattern.compile(reg);
        Matcher m = p.matcher(text);
        while (m.find()) {
            System.out.println(m.group(0));//整个匹配到的内容
            System.out.println(m.group(1));//(.*?)
        }
    }
//=====运行结果=====
//test
//test

关于优化:
谈谈正则表达式的性能优化问题_Java笔记虾的博客-CSDN博客

学习教程:【韩顺平讲Java】Java 正则表达式专题 -正则 正则表达式 元字符 限定符 Pattern Matcher 分组 捕获 反向引用等_哔哩哔哩_bilibili

你可能感兴趣的:(Java,正则表达式)