Java 学习笔记(二十一)

正则表达式

学习内容来自B站韩顺平老师的Java基础课

正则表达式

可以快速方便的匹配字符串的内容,匹配内容可以通过特有规则的 pattern 指定

  • 正则表达式就是用某种模式(pattern)去匹配字符串的一个公式
  • 虽然看上去古怪,但是学起来并不算复杂
  • 学会后可以大量缩短文本处理工作的耗时

比如从给定的文本中找出所有四个数字连在一起的子串,并且这四个数字中,第一个与第四个相同,第二个与第三个相同,比如 1221,3443

如果使用传统方法,只能通过遍历字符串,并且遍历的同时记录连续数字个数,然后再做相应判断,比较麻烦

而使用正则表达式,就可以通过指定 pattern 快速匹配出想要的内容

或者说验证邮箱、手机号的格式,也可以指定 pattern 快速进行判断

底层实现分析

比如在一串文本中找到所有四个数字连在一起的子串:

public static void main(String[] args) {
        String content = "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" +
                "平台的企业版),应用于基于Java的应用服务器。Java 2平台的发布,是Java发展过程中最重要" +
                "的一个里程碑,标志着Java的应用开始普及。";
        // 匹配所有四个数字
        // 1. \\d 表示一个任意的数字
        String regStr = "\\d\\d\\d\\d";
        // 2. 创建 Pattern 对象
        Pattern pattern = Pattern.compile(regStr);
        // 3. 创建匹配器
        // 说明:创建匹配器 matcher,按照 regStr 指定的规则去匹配 content 字符串
        Matcher matcher = pattern.matcher(content);
        // 4. 开始匹配
        // 找到就返回 true,否则返回 false
        // 匹配到的内容放入 matcher.group(0)
        while (matcher.find()) {
            System.out.println("找到:" + matcher.group(0));
        }
    }

其中matcher.find() 完成的任务有:

  1. 根据指定的规则,定位满足要求的字符串(比如 1998 )
  2. 找到后,将子串索引记录到 matcher 对象的属性 int[] groups 中(比如 1998 )。开始索引记录到 groups[0],即 groups[0] = 0;结束索引 +1 后记录到 groups[1] 中,即 groups[1] = 4
  3. 同时记录 oldLast 的值为 groups[1] 的值,用来作为下次执行 find() 方法的匹配开始位置

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

上述可以概括为返回 [groups[0], groups[1]) 之间的子串,类似 subString 方法

那么为什么是 group(0),这个 0 又代表什么意思?

对上述例子稍作修改,在 pattern 中加上两对小括号,如下:
Java 学习笔记(二十一)
这样做相当于对正则表达式进行分组,有多少对括号就分成多少组,那么现在使用 matcher.find() 方法完成的任务有:

  1. 根据指定的规则,定位满足要求的字符串(比如 1998 )
  2. 找到后,将子串索引记录到 matcher 对象的属性 int[] groups 中
  3. 比如 1998,开始索引记录到 groups[0],即 groups[0] = 0; 结束索引 +1 后记录到 groups[1] 中,即 groups[1] = 4
  4. 考虑分组,对于子串 1998,记录第 1 组() 匹配的字符串 19 groups[2] = 0,groups[3] = 2
  5. 考虑分组,对于子串 1998,记录第 2 组() 匹配的字符串 98 groups[4] = 2,groups[5] = 4
  6. 如果有更多的分组,以此类推
  7. 同时记录 oldLast 的值为 groups[1] 的值,用来作为下次执行 find() 方法的匹配开始位置

即分组后,groups 中 0 和 1 索引记录的仍为匹配到的子串的首尾索引,往后的位置依次记录分组对应的索引

比如:

public static void main(String[] args) {
        String content = "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" +
                "平台的企业版),应用于基于Java的应用服务器。Java 2平台的发布,是Java发展过程中最重要" +
                "的一个里程碑,标志着Java的应用开始普及。";
        // 匹配所有四个数字
        // 1. \\d 表示一个任意的数字
        String regStr = "(\\d\\d)(\\d\\d)";
        // 2. 创建 Pattern 对象
        Pattern pattern = Pattern.compile(regStr);
        // 3. 创建匹配器
        // 说明:创建匹配器 matcher,按照 regStr 指定的规则去匹配 content 字符串
        Matcher matcher = pattern.matcher(content);
        // 4. 开始匹配
        // 找到就返回 true,否则返回 false
        // 匹配到的内容放入 matcher.group(0)
        /**
         * matcher.find() 完成的任务:
         * 1. 根据指定的规则,定位满足要求的字符串(比如 1998 )
         * 2. 找到后,将子串索引记录到 matcher 对象的属性 int[] groups 中
         *      开始索引记录到 groups[0],即 groups[0] = 0
         *      结束索引 +1 后记录到 groups[1] 中,即 groups[1] = 4
         *    (考虑分组) 2.1 groups[0] = 0,groups[1] = 4
         *    (考虑分组) 2.2 对于子串 1998,记录第 1 组() 匹配的字符串 19 groups[2] = 0,groups[3] = 2
         *    (考虑分组) 2.3 对于子串 1998,记录第 2 组() 匹配的字符串 98 groups[4] = 2,groups[5] = 4
         *    (考虑分组) 如果有更多的分组,以此类推
         * 3. 同时记录 oldLast 的值为 groups[1] 的值,用来作为下次执行 find() 方法的匹配开始位置
         *
         * 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();
         *     }
         *
         * 1. 上述方法可以概括为返回 [groups[0], groups[1]) 之间的子串,类似 subString 方法
         *
         */
        while (matcher.find()) {
            System.out.println("找到:" + matcher.group(0));
            System.out.println("第 1 组 () 匹配到的值:" + matcher.group(1));
            System.out.println("第 2 组 () 匹配到的值:" + matcher.group(2));
        }
    }

输出结果:

找到:1998
第 1 组 () 匹配到的值:19
第 2 组 () 匹配到的值:98
找到:1999
第 1 组 () 匹配到的值:19
第 2 组 () 匹配到的值:99

正则表达式语法

正则表达式由各种元字符组成,元字符从功能上大致分为:

  • 限定符
  • 选择匹配符
  • 分组组合和反向引用符
  • 特殊字符
  • 字符匹配符
  • 定位符

转义符

首先需要知道转义符为 \\

  • 当需要使用正则表达式去检索某些特殊字符的时候,需要用到转移符号,否则检索不出结果,甚至会报错

就像使用 ( 去匹配 abc($abc(123( 会报错
Java 学习笔记(二十一)

在 ( 前加上 \ 即可

注意:在 Java 的正则表达式中,两个 \ 表示其它语言中的一个 \

需要用到转义符的字符有:

  • . * + ( ) $ / \ ? [ ] ^ { }

字符匹配符

Java 学习笔记(二十一)
Java 学习笔记(二十一)
其中

  • \\d{3} 等价于 \\d\\d\\d

除此之外,还有:

  • \\s 匹配任意空白字符(空格、制表符)
  • \\S 匹配任意非空白字符,与上一个相反
  • [abcd] 表示匹配 abcd 中的任意一个字符
  • [^abcd] 表示匹配不是 abcd 的任意一个字符

Java 的正则表达式默认区分大小写,如何实现不区分大小写?

  • (?i)abc 表示 abc 都不区分大小写
  • a(?i)bc 表示 bc 不区分大小写
  • a((?i)b)c 表示只有 b 不区分大小写
  • 也可以在 Pattern 的 compile 方法中加上参数 Pattern.CASE_INSENSIVE,如Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSIVE);

选择匹配符

在匹配某个字符时可以是选择性的,既可以匹配这个,又可以匹配那个。通俗理解就是逻辑表达式中的或操作,符号也和或一样,为 |
Java 学习笔记(二十一)

限定符

用于指定前面的字符和组合项连续出现多少次,比如前面 \\d{3} 等价于 \\d\\d\\d
Java 学习笔记(二十一)
Java 学习笔记(二十一)
注意:

  • Java 匹配是贪婪匹配,如果指定为a{3 ,4}后,待匹配文本为 aaaa456,那么会匹配到 4 个 a,即 aaaa,而不是 3 个 a
  • 同理,如果指定为a{3 ,4}后,待匹配文本为 aaaaaaa789,那么会先匹配到 4 个 a,即 aaaa,再匹配到 3 个 a,即 aaa;而如果待匹配文本为 aaaaaa678,那么只会匹配到 4 个 a,即 aaaa
  • +表示匹配出现大于等于 1 次的,如果指定为 1+,待匹配文本为 1111456,那么会直接匹配到 1111
  • *?同理

定位符

Java 学习笔记(二十一)

分组

Java 学习笔记(二十一)
例子:

public static void main(String[] args) {
        String content = "jieruigou NN GGG1237gou 9987gou";
        String regStr = "(?<g1>\\d\\d)(?<g2>\\d\\d)";

        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);

        while (matcher.find()) {
            System.out.println("找到:" + matcher.group(0));
            System.out.println("第 1 个分组内容:" + matcher.group(1));
            System.out.println("第 1 个分组内容(通过组名):" + matcher.group("g1"));
            System.out.println("第 2 个分组内容:" + matcher.group(2));
            System.out.println("第 2 个分组内容(通过组名):" + matcher.group("g2"));
        }
    }

输出结果:

找到:1237
第 1 个分组内容:12
第 1 个分组内容(通过组名):12
第 2 个分组内容:37
第 2 个分组内容(通过组名):37
找到:9987
第 1 个分组内容:99
第 1 个分组内容(通过组名):99
第 2 个分组内容:87
第 2 个分组内容(通过组名):87

非捕获分组

Java 学习笔记(二十一)
例子 1:
(?:pattern) 的使用

public static void main(String[] args) {
        String content = "hi杰瑞狗 jerry杰瑞dog 杰瑞队长hello";

        // 以下两种写法等价
//        String regStr = "杰瑞狗|杰瑞dog|杰瑞队长";
        String regStr = "杰瑞(?:狗|dog|队长)";


        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);

        while (matcher.find()) {
            System.out.println("找到:" + matcher.group(0));
        }
    }

输出结果

找到:杰瑞狗
找到:杰瑞dog
找到:杰瑞队长

例子 2:
(?=pattern) 的使用

public static void main(String[] args) {
        String content = "hi杰瑞狗 jerry杰瑞dog 杰瑞队长hello";

        // 以下两种写法等价
//        String regStr = "杰瑞狗|杰瑞dog|杰瑞队长";
//        String regStr = "杰瑞(?:狗|dog|队长)";
        String regStr = "杰瑞(?=dog|队长)";


        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);

        while (matcher.find()) {
            System.out.println("找到:" + matcher.group(0));
        }
    }

输出结果:

找到:杰瑞
找到:杰瑞

(?!pattern) 就相当于(?=pattern) 反过来的效果,(?=pattern) 匹配到的(?!pattern) 都匹配不到

注意:

  • 非捕获分组不能使用 matcher.group(1)

非贪心匹配

之前提过 Java 默认的是贪婪匹配,非贪婪匹配可以通过加 ? 来实现
Java 学习笔记(二十一)

应用实例

汉字验证

 public void isCharacter() {
        String content = "杰瑞狗";
        // 汉字的编码范围
        String regStr = "^[\u0391-\uffe5]+$";


        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);

        if (matcher.find()) {
            System.out.println("满足格式");
        } else {
            System.out.println("不满足格式");
        }
    }

邮编验证(不完全)

// 要求:六位数字
    public void isMailCode() {
        String content = "411320";
        // 汉字的编码范围
        String regStr = "^[1-9]\\d{5}$";


        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);

        if (matcher.find()) {
            System.out.println("满足格式");
        } else {
            System.out.println("不满足格式");
        }
    }

QQ 号验证

// 要求:1 - 9 开头的数(5位~10位)
    public void isQQId() {
        String content = "411320";
        // 汉字的编码范围
        String regStr = "^[1-9]\\d{4,9}$";


        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);

        if (matcher.find()) {
            System.out.println("满足格式");
        } else {
            System.out.println("不满足格式");
        }
    }

手机号验证

// 要求:1开头,第二位是 3 4 5 8 其中一个,共 11 位
    public void isPhoneNumber() {
        String content = "15966667777";
        // 汉字的编码范围
        String regStr = "^1[3|4|5|8]\\d{9}$";


        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);

        if (matcher.find()) {
            System.out.println("满足格式");
        } else {
            System.out.println("不满足格式");
        }
    }

URL 验证

public void isURL() {
        String content = "https://www.bilibili.com/video/BV1fh411y7R8?p=894&spm_id_from=pageDriver";

        /**
         * 分析思路
         * 1. 开头可能有 https:// 或者 http://
         * 2. 域名由 数字、字母、下划线、- 组成
         * 3. 之后的路径由 \ 开头,然后跟上字母、数字以及一些字符组成
         */
        String regStr = "^((http|https)://)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-?=&/.%#]*)?$";  // [.?*] 中括号中的字符表示匹配字符本身

        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);

        if (matcher.find()) {
            System.out.println("满足格式");
        } else {
            System.out.println("不满足格式");
        }
    }

常用类

Pattern

pattern 对象是一个正则表达式对象,没有公共构造器

若想创建 Pattern 对象,就需要调用它的静态方法 compile() 返回 Pattern 对象

该方法需要接受一个正则表达式作为它的第一个参数,如:

Pattern pattern = Pattern.compile("^1[3|4|5|8]\\d{9}$");

Pattern 类还有其他方法,如

  1. matches,用来验证输入字符串是否满足给定要求
public void testMatches() {
        String content = "hello jerry hello, gougougou";
        String regStr = "hello";

        // 若正则表达式能整体匹配给定文本,返回 true,否则返回 false
        boolean matches = Pattern.matches(regStr, content);
        System.out.println(matches ? "整体匹配成功" : "整体匹配失败");
    }

就像上面应用实例的各种验证,其实可以使用 matches 方法

事实上,该方法底层仍然是调用 Matcher 类的 matches 方法

Matcher

Matcher 对象是对输入字符串进行解释和匹配的引擎,与 Pattern 类一样,Matcher 也没有公共构造方法,需要调用 Pattern 对象的 matcher 方法获得 Matcher 对象

Matcher matcher = pattern.matcher(content);

Matcher 类的常用方法有:
Java 学习笔记(二十一)
Java 学习笔记(二十一)

例子 1:

public void testMethod() {
        String content = "hello jerry dog jack jessi hello jim hello";
        String regStr = "hello";

        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()) {
            System.out.println("=========");
            // 当前匹配到的子串的开始索引,相当于 groups[0]
            System.out.println(matcher.start());
            // 当前匹配到的子串的结束索引 + 1,相当于 groups[1]
            System.out.println(matcher.end());
            System.out.println("找到:" + content.substring(matcher.start(), matcher.end()));
        }
        // 整体匹配方法,校验某个字符串是否满足某个规则
        System.out.println("整体匹配=" + matcher.matches());
    }

输出结果:

=========
0
5
找到:hello
=========
27
32
找到:hello
=========
37
42
找到:hello
整体匹配=false

例子 2:
请将字符串 “hello jerry dog jack jessi hello jim hello” 中的 jerry 换成 杰瑞狗

public void testExchange() {
        String content = "hello jerry dog jack jessi hello jerry jim hello";
        String regStr = "jerry";

        Pattern pattern = Pattern.compile(regStr);
        Matcher matcher = pattern.matcher(content);

        // replaceAll 方法会返回替换后的新字符串,并不修改原来的字符串
        String newContent = matcher.replaceAll("杰瑞狗");
        System.out.println("newContent = " + newContent);
    }

输出结果:

newContent = hello 杰瑞狗 dog jack jessi hello 杰瑞狗 jim hello

PatternSyntaxException

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

反向引用

重新回到一开始提到的问题:

  • 从给定的文本中找出所有四个数字连在一起的子串,并且这四个数字中,第一个与第四个相同,第二个与第三个相同,比如 1221,3443

可以发现之前介绍的内容不能完成这个功能,所以需要介绍新方法–反向引用

首先需要明确三个概念

  • 分组:
    可以用小括号将正则表达式包起来,每个包起来的内容都可以视为一个分组
  • 捕获:
    把正则表达式中子表达式/分组匹配的内容保存到内存中,分组默认以数字编号区分,也可以进行显示命名。默认情况下,分组 0 为整个正则表达式的匹配结果,然后按从左至右将分组分为 1、 2、 以此类推
  • 反向引用:
    小括号的内容被捕获后,可以在这个括号之后(右侧)被使用,从而写出功能更为复杂的正则表达式,这就称为反向引用。反向引用既可以在正则表达式内部引用(通过 \\分组号 引用),也可以在外部引用(通过 $分组号 引用)

那么现在可以通过反向引用上面提出的问题:

public static void main(String[] args) {
        String content = "jerry dog3443 dog1234 jerry 1221 hello";
        String regStr = "(\\d)(\\d)\\2\\1";

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

再来一个例子:
在字符串中检索编号,形式为:12321-333444111,即以五位数开头,然后接一个 - ,之后接一个九位数,要求每三位要相同

public void testNum() {
        String content = "jerry12321-444555999 dog3443 dog1234 jerry 1221 hello";
        String regStr = "\\d{5}-(\\d)\\1{2}(\\d)\\2{2}(\\d)\\3{2}";

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

结巴去重案例

把类似:”我…我要…学学学学…java编程!“
通过正则表达式修改成”我要学java编程!“

public static void main(String[] args) {
        String content = "我...我要...学学学学...java编程!";

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

        System.out.println(content);

        // 去掉重复的字,方法一
        // 先用 (.)\\1+ 匹配连续相同的字
        pattern = Pattern.compile("(.)\\1+");
        matcher = pattern.matcher(content);
        while (matcher.find()) {
            System.out.println("找到=" + matcher.group(0));
        }
        // 再用反向引用 $1 替换匹配到的内容
        String newContent = matcher.replaceAll("$1");
        System.out.println("newContent = " + newContent);

        // 去掉重复的字,方法二
        content = Pattern.compile("(.)\\1+").matcher(content).replaceAll("$1");
        System.out.println("content = " + content);
    }

输出结果:

我我要学学学学java编程!
找到=我我
找到=学学学学
newContent = 我要学java编程!
content = 我要学java编程!

在 String 类中使用正则表达式

替换

String 类的 replaceAll(String regex, String replacement) 方法,可以直接使用正则表达式进行替换

例子:

public static void main(String[] args) {
        // 将下面文本中的 JDK1.3、JDK1.4 替换成 JDK
        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的计算能力有了大幅提升,与J2SE1.3相比," +
                "其多了近62%的类和接口。在这些新特性当中,还提供了广泛的XML支持、安全套接字" +
                "(Socket)支持(通过SSL与TLS协议)、全新的I/OAPI、正则表达式、日志与断言" +
                "。2004年9月30日,J2SE1.5发布,成为Java语言发展史上的又一里程碑。为了表示" +
                "该版本的重要性,J2SE 1.5更名为Java SE 5.0(内部版本号1.5.0),代号为“Ti" +
                "ger”,Tiger包含了从1996年发布1.0版本以来的最重大的更新,其中包括泛型支持、" +
                "基本类型的自动装箱、改进的循环、枚举类型、格式化I/O及可变参数。";
        // 使用 String 类的方法
        content = content.replaceAll("JDK1\\.3|JDK1\\.4", "JDK");
        System.out.println(content);
    }

判断

String 类的 matches(String regex) 方法

例子:
判断给定的手机号是否是 138 / 139 开头

public void testMatches() {
        String content = "13866666666";
        if (content.matches("13(8|9)\\d{8}")) {
            System.out.println("符合要求!");
        } else {
            System.out.println("不符合要求!");
        }
    }

分割

String 类的 split(String regex) 方法

例子:
按照 - + # ~ 分割字符串

public void testSplit() {
        String content = "我+是-一~个#字-符+串";
        String[] split = content.split("~|\\+|-|#");
        for (String s : split) {
            System.out.println("s = " + s);
        }
    }
上一篇:Django csrf


下一篇:flex之 align-content