admin管理员组

文章数量:1579083

目录

正则表达式(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 = Patternpile("[a-zA-Z]+"); //找英文
        //Pattern pattern = Patternpile("[1-9]+"); //找数字
        Pattern pattern = Patternpile("([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 = Patternpile(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)); 越界
        }
    }

小结
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或多个



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 = Patternpile(regStr);
        // Pattern.CASE_INSENSITIVE 表示不区分字母大小写
        //Pattern pattern = Patternpile(regStr,Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()){
            System.out.println("找到:"+ matcher.group(0));
        }
    }

选择匹配符 

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

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

限定符


注意:当 ? 紧跟其他限定符匹配模式时,匹配模式为“懒惰模式” 详见(关于优化)

 关于贪婪匹配

        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 = Patternpile(regStr);
        Matcher matcher = compile.matcher(content);
        while (matcher.find()){
            System.out.println("找到:"+matcher.group(0));
        }
    }

定位符

    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 = Patternpile(regStr3);
        Matcher matcher = compile.matcher(content);
        while (matcher.find()){
            System.out.println("找到:"+matcher.group(0));
        }
    }

非命名分组、命名分组

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

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

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

        Pattern compile = Patternpile(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 重载方法 可通过组数、组名获取匹配字符串 

 

特别分组(了解)

    public static void main(String[] args) {

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

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

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

两个常用类 :Pattern、Matcher

Pattern 类

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

Matcher 类

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

分组、捕获、反向引用

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

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

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

        //2.去掉重复的字
        //思路
        //(1) 使用(.)\\1+  匹配重复的字
        pattern = Patternpile("(.)\\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 = Patternpile("(.)\\1").matcher(content).replaceAll("$1");

    }

String 类中使用正则表达式

 StringreplaceAll(String regex, String replacement)
          使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
 StringreplaceFirst(String regex, String replacement)
          使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
booleanmatches(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 或者 tsinghua
    //4、写出对应的正则表达式,验证输入的字符串是否为满足规则
    public static void main(String[] args) {
        String mail = "810467545@qq";
        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:8080/abc/index.htm
    //1: 要求得到协议是什么  http
    //2: 域名是什么?       www.shohu
    //3: 端口是什么?       8080
    //4: 文件名是什么?     index.htm
    @Test
    public void test1(){
        String content = "http://www.sohu:8080/abc/xxx/yyy/index.htm";
        //思路: 分组 分为4组 依次判断  关键: 找到定位的符号 :/  :  /
        String regStr = "^([a-zA-Z]+)://([a-zA-Z.]+):(\\d+)[\\w-/]*/([\\w.]+)$";

        Pattern pattern = Patternpile(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 = "<input high=\"20\" weight=\"70\">test</input>";
        String reg = "(<input.*?>)(.*?)(</input>)";
        Pattern p = Patternpile(reg);
        Matcher m = p.matcher(text);
        while (m.find()){
            System.out.println(m.group(0));//整个匹配到的内容
            System.out.println(m.group(1));//<input.*?>
            System.out.println(m.group(2));//(.*?)
            System.out.println(m.group(3));//(</input>)
        }
    }
//=====运行结果=====
//<input high="20" weight="70">test</input>
//<input high="20" weight="70">
//test
//</input>

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

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

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

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

本文标签: 详解效率正则表达式RegExp