【Java常用类】1-10 正则表达式

§1-10 正则表达式

1-10.1 正则表达式组成

正则表达式(regular expression),是一个表达一个字符串或模式的标志和字符的序列。

正则表达式可以校验字符串是否满足一定的规则,并用来校验数据格式的合法性,也可以用于在一段长文本中查找满足要求的内容。

正则表达式常常可用于校验邮箱格式、网址格式、用户手机号格式、身份证号格式、地址格式、密码格式等是否正确。

java.util.regex 包下,提供了两个类用于模式匹配和文本处理。

1-10.2 Pattern 类和 Matcher

Pattern 类和 Matcher 类分别用于创建模式表达式(正则表达式)对象和模式匹配,二者常常搭配使用。

  • PatternPattern 类用于编译正则表达式。

    构造方法:构造并编译一个正则表达式

    Pattern pattern = Pattern.compile(String regex);
    
  • MatcherMatcher 类用于在给定的文本中查找与模式匹配的文本。

    构造方法:对已有的 Pattern 对象,在给定文本中查找并匹配,一般使用 Pattern 对象中的 matcher() 方法返回 Matcher 对象

    Matcher matcher = pattern.matcher(String input);
    

    查找方法

    方法 描述
    boolean find() 从输入序列中尝试查找下一个与模式匹配的子序列
    boolean find(int start) 重置此匹配器,从输入序列中的指定索引处开始尝试查找下一个与模式匹配的子序列
    boolean lookingAt() 总是从区域开头开始的输入序列与该模式匹配
    boolean matches() 将整个输入序列区域与模式相匹配

    索引方法

    方法 描述
    int start() 返回上一次匹配时,首个匹配的字符处索引
    int start(int group) 返回上一次匹配时,对给定的组所捕获的子序列的起始索引
    int end() 返回最后一个所匹配字符的偏移量
    int end(int group) 返回上一次匹配时,对给定的组所捕获的子序列最后一个字符的偏移量

    捕获组

    方法 描述
    String group() 返回上一次匹配时,所匹配的子序列(可能为空)
    String group(int group) 返回上一次匹配时,由捕获组捕获的子序列(可能为空)
    int groupCount() 返回匹配器模式中捕获组的数量

    更改状态

    方法 描述
    Matcher reset() 重置此匹配器
    Matcher reset(CharSequence input) 重置此匹配器,并让匹配器在下次开始匹配时匹配新的输入序列

注意

  • 创建 Matcher 的另外一种方法是使用连点式;

      Matcher matcher = Pattern.compile(regex).matcher(input);
    

    上述语句与下列语句的效果等价:

    str.matches(regex);
    Pattern.matches(regex, input);
    

    其中,使用字符串调用的方式默认匹配单行,在默认匹配单行的情况下,^$ (见下文解释)可以略去。

    • matches()lookingAt() 方法都可用于匹配查找,但 matches() 匹配整个序列,而 lookingAt() 不保证,也不要求匹配整个序列;

    • find() 方法在匹配成功时,会在底层记录所匹配的字串的起始索引和结束索引 + 1,下一次调用时,从结束索引开始查找子串,重复这一过程;

    • 若匹配成功,则可使用 start(), end(), group() 方法获得更多信息;

    • group(0) 表示一个特殊的组,它总是代表整个表达式,该组不包括在 groupCount() 的返回值中;

      因此,表达式 matcher.group(0) 等价于 matcher.group()

    • reset() 方法在重置匹配器时,会丢弃其所有显式状态信息,将追加位置设为 0,并将匹配器区域设为默认值(整个字符序列);

    • 匹配器所携带的正则表达式在创建好后不可变;

1-10.3 匹配模式元素

Pattern 类中列出了全部匹配模式的元素,用于构成正则表达式,详见官方文档。

字符(characters):

正则表达式 匹配内容
x 字符 x
\\ 反斜杠
\0n, \0nn, \0mnn 八进制表示的字符(0 <= m <= 3,0 <= n <= 7
\xhh, \0xhhhh 十六进制表示的字符
\x{h...h} 十六进制表示的字符(Character.MIN_CODE_POINT <= 0xh...h <= Character.MAX_CODE_POINT
\N{name} Unicode 字符名称为 name 的字符
\t 制表符,'\u0009'
\n 换行符,'\u000A'
\r 回车符,'\u000D'
\f 换页符,'\u000C'
\a 警告(响铃),'\u0007'
\e 转义字符,'\u001B'
\cx 对应的控制字符 x

字符类(character classes):

正则表达式 匹配内容
[abc] a, bc
[^abc] 任何除了 a, bc 的其他字符(取反)
[a-zA-Z] azAZ (包含)
[a-d[m-p]] admp
[a-z&&[def]] azdef 的交集,即 d, e, f
[a-z&&[^bc]] az 与非 bc 的交集,即 [ad-z]
[a-z&&[^m-p]] az ,但排除 mp

预定义字符(predefined character classes):

正则表达式 匹配内容
. 一个任何字符
\d 一个数字 [0-9]
\D 一个非数字 [^0-9]
\h 一个横向空白字符,[\t\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]
\H 一个非横向空白字符
\s 一个空白字符 [\t\r\n\f\x0B]
\S 一个非空白字符 [^\s]
\v 一个垂直空白字符,[\n\x0B\f\r\x85\u2028\u2029]
\V 一个非垂直空白字符,[^\v]
\w 一个英文、数字、下划线 [a-zA-Z0-9_]
\W 一个非英文、数字、下划线 [^a-zA-Z0-9_]

边界匹配器(boundary matchers):

正则表达式 含义
^ 行开头
$ 行末尾
\b 单词边界
\b{g} Unicode 扩展字形簇边界
\B 非单词边界
\A 输入的开头
\G 上一次匹配的末尾

贪婪数量词(greedy quantifiers):

数量词 含义
X? X 出现 一次 或 零次
X* X 出现 零次 或 多次
X+ X 出现 一次 或 多次
X{n} X 恰好出现 n
X{n,} X 至少出现 n
X{n,m} X 至少出现 n 次,至多出现 m

逻辑运算(logical operations):

逻辑运算 描述
XY XY
X|Y XY
(X) X,位于一个捕获组中

特殊构成(special constructs):

正则表达式 含义
(?<name>X) X,位于一个命名捕获组 name
(?:X) X,位于一个非捕获组中
(?idmsuxU-idmsuxU) 无,但会将匹配标识符 i, d, m, s, u, x, U 打开或关闭
(?idmsuxU-idmsuxU:X) X,位于一个非命名捕获组中,将匹配标识符打开或关闭
(?=X) X,零宽度断言,正向预查
(?!X) X,零宽度断言,负向预查
(?<=X) X,零宽度断言,正向回查
(?<!X) X,零宽度断言,负向回查
(?>X) X,作为独立的,非捕获组

匹配标识符(match flags):

标识符 对应常量 含义
i CASE_INSENSITIVE 不区分大小写
d UNIX_LINES Unix 行模式
m MULTILINE 多行模式(在含有^$的表达式中有意义)
s DOTALL 匹配包括换行符在内的所有字符
u UNICODE_CASE Unicode 字符折叠
x COMMENTS 允许模式中出现空白字符和内嵌评论(#),这些字符将会被忽略直至行末
U UNICODE_CHARACTER_CLASS 启用 Unicode 版本的与定义字符类和 POSIX 字符类,会有性能损耗

注意

  • 若要匹配中文字符,一种方法是使用 Unicode 指定字符的区间范围:[\u4E00-\u9FA5]

  • 另一种匹配中文字符的方法是使用 Unicode 的编码对每个中文字符作精确匹配;

  • Java 正则表达式对中文支持十分友好,因此可直接在表达式中输入中文直接匹配;

  • 根据 Java 语言规范的要求,Java 源代码中的反斜杠解释为 Unicode 转义或其它字符转义,因此必须在字符串字面值中连用两个反斜杠,表示该正则表达式受到保护;

  • 特殊构成中的匹配标识符之间用连字符 - 连接;

  • 捕获组(capture group):由一对圆括号创建,捕获组通过从左到右计算左括号来编号,从 1 开始;

    示例((A)(B(C))) 有四个组,分别为 ((A)(B(C))), (A), (B(C)), (C)

    在正则表达式内,可以使用 \<groupNumber> 再次使用某一编号的捕获组;

    在正则表达式外,若要访问某一编号的捕获组,可使用 $<groupNumber> 访问;

  • 捕获组可以有名字,名字所支持的字符范围是大小写字母和数字,且第一个字符必须为字母;

  • 非捕获组(non-capture group):不会单独捕获匹配的文本,只是用来辅助其他捕获组的匹配,其匹配结果可能会由其他捕获组捕获;

    不像捕获组的数据,可以重复利用(使用 \$ ),非捕获组的数据不可再利用

    开头为 (? 的组,要么为纯非捕获组,并不捕获文本,也不计入总组数,要么为命名捕获组(?<name>X>));

  • 零宽度断言(zero-width assertion):用于检查字符串是否满足条件的一种特殊模式,不会将该条件匹配的字符包含在最终结果中,下列的正向/负向预查/回查都是零宽度断言;

  • 正向预查(positive lookahead):(?=...) 匹配紧随其后的字符串,只记录匹配结果,不包含在结果之中,为非捕获组;

  • 正向回查(positive lookbehind):(?<=...) 匹配紧随其前的字符串,只记录匹配结果,不包含在结果中,为非捕获组;

  • 负向预查(negative lookahead):(?!...) 不匹配紧随其后的字符串,为非捕获组;

  • 负向回查(negative lookbehind):(?<!...) 不匹配紧随其前的字符串,为非捕获组;

1-10.4 练习使用正则表达式校验文本

使用正则表达式完成如下需求

  • 校验中国大陆手机号格式;
  • 校验邮箱地址合法性;
  • 校验密码合法性(8~16位,必须含有大小写和数字,至少一个特殊符号);
  • 校验中国大陆身份证号格式;
  • 校验网址格式合法性;

练习1:校验中国大陆手机号格式

public boolean checkPhoneNumber(String phone) {
    if (phone == null) {
        return false;
    }

    return phone.matches("(\\+86)?1[3-9]\\d{9}");
}

练习2:校验邮箱地址合法性

public boolean checkEmail(String email) {
    if (email == null) {
        return false;
    }

    return email.matches("^[a-zA-Z0-9.%+-_]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,3}$");
}

练习3:校验密码合法性

private static boolean checkPassword(String password) {
    if (password == null) {
        return false;
    }

    return password.matches(
        "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\\d!@#$%^&*()_+]{8,16}$");
    }

练习4:校验二代身份证格式合法性

public boolean checkId(String id) {
    if (id == null) {
        return false;
    }

    //先判断基本格式
    if (id.matches("^[1-9]\\d{5}(19|20)\\d{9}[\\dxX]$")) {
        String birthDateStr = id.substring(6,14);
        
        //判断日期是否合法
        try {
            LocalDate bithDate = LocalDate.of(Integer.parseInt(birthDateStr.substring(0,4)),
                                              Integer.parseInt(birthDateStr.substring(4,6)),
                                              Integer.parseInt(birthDateStr.substring(6)));
        } catch (DateTimeException e) {
            return false;
        }

    } else {
        return false;
    }

    return true;
}

练习5:校验网址合法性

private static boolean checkURL(String url) {
    if (url == null) {
        return false;
    }

    //不区分大小写
    return url.matches("^(?i)(http://|https://)?(www\\.)?[\\w.\\-]+\\..+$");
}

扩展练习:严格验证身份证号合法性

若要严格验证身份证号的合法性,需要将前十七位的值依次抽取,加权后取余,将得到的余数与最后一位校验码做比较。

有关算法逻辑见文章:实名认证前传之身份证号码编码规则 - 知乎

public boolean checkId(String id) {
    if (id == null) {
        return false;
    }

    //先判断基本格式
    if (id.matches("^[1-9]\\d{5}(18|19|20)\\d{9}[\\dxX]$")) {
        String birthDateStr = id.substring(6, 14);
        //判断日期合法性
        try {
            LocalDate bithDate = LocalDate.of(Integer.parseInt(birthDateStr.substring(0, 4)),
                                              Integer.parseInt(birthDateStr.substring(4, 6)), Integer.parseInt(birthDateStr.substring(6)));
        } catch (DateTimeException e) {
            return false;
        }
    } else {
        return false;
    }

    //严格校验合法性:
    int weighted = Integer.parseInt(String.valueOf(id.charAt(0))) * 7 + Integer.parseInt(String.valueOf(id.charAt(1))) * 9
        + Integer.parseInt(String.valueOf(id.charAt(2))) * 10 + Integer.parseInt(String.valueOf(id.charAt(3))) * 5
        + Integer.parseInt(String.valueOf(id.charAt(4))) * 8 + Integer.parseInt(String.valueOf(id.charAt(5))) * 4
        + Integer.parseInt(String.valueOf(id.charAt(6))) * 2 + Integer.parseInt(String.valueOf(id.charAt(7)))
        + Integer.parseInt(String.valueOf(id.charAt(8))) * 6 + Integer.parseInt(String.valueOf(id.charAt(9))) * 3
        + Integer.parseInt(String.valueOf(id.charAt(10))) * 7 + Integer.parseInt(String.valueOf(id.charAt(11))) * 9
        + Integer.parseInt(String.valueOf(id.charAt(12))) * 10 + Integer.parseInt(String.valueOf(id.charAt(13))) * 5
        + Integer.parseInt(String.valueOf(id.charAt(14))) * 8 + Integer.parseInt(String.valueOf(id.charAt(15))) * 4
        + Integer.parseInt(String.valueOf(id.charAt(16))) * 2;

    //获取余数和校验码
    int remainder = weighted % 11;
    int checkCode;
    try {
        checkCode = Integer.parseInt(id.substring(id.length()-1));
    } catch (NumberFormatException e) {
        checkCode = 10;
    }

    //计算比较校验码
    return getCheckCode(remainder) == checkCode;
}

余数与校验码的映射关系:

public int getCheckCode(int remainder) {
        switch (remainder) {
            case 0: return 1;
            case 1: return 0;
            case 2: return 10;
            case 3: return 9;
            case 4: return 8;
            case 5: return 7;
            case 6: return 6;
            case 7: return 5;
            case 8: return 4;
            case 9: return 3;
            default: return 2;	//10
        }
    }

1-10.5 练习使用正则表达式条件查找文本

满足下列要求

Java 自从 95 年问世以来,经历了很多版本,目前企业中用的最多的是 Java8 和 Java11,因为这两个是长期支持版本,下一个长期支持版本是 Java17,相信在未来不久 Java17 也会逐渐登上历史舞台。

  • 在上述文本中,找到所有的 JavaXX
  • 随机更改上述文本中 Java 的字母大小写,使用条件爬取,匹配所有的 JavaXX,但只输出 Java(大小写不敏感);
  • 提取带有版本号的 Java
  • 提取不带版本号的 Java

要求1

String origin = "Java 自从 95 年问世以来,经历了很多版本,目前企业中用的最多的是 Java8 和 Java11," +
    "因为这两个是长期支持版本,下一个长期支持版本是 Java17,相信在未来不久 Java17 也会逐渐登上历史舞台。";

//采用连点式直接获取匹配器对象
Matcher matcher = Pattern.compile("Java\\d{0,2}").matcher(origin);
//使用 find() 方法查找子串,若成功,起始索引和结束索引都会更新,前者指向匹配子串的起始处,后者指向匹配子串的末尾索引+1
//下一次继续调用时,从结束索引继续查找
//利用循环去查找,直至返回 false,即再也找不到了
while (matcher.find()) {
    System.out.println(matcher.group());    //匹配成功可用 start, end, group 获取更多信息
}

要求2

//大小写不敏感,匹配 JavaXX,只输出 Java:使用断言匹配,不捕获,只匹配
String origin = "JAva 自从 95 年问世以来,经历了很多版本,目前企业中用的最多的是 JaVa8 和 JAVA11," +
    "因为这两个是长期支持版本,下一个长期支持版本是 Java17,相信在未来不久 JavA17 也会逐渐登上历史舞台。";

//正向预查是零宽度断言,一定不会包含结果
//(? 非捕获组,辅助其他捕获组匹配,不会单独捕获,但会被辅助捕获
Matcher matcher = Pattern.compile("((?i)Java)(?=\\d{1,2})").matcher(origin);
while (matcher.find()) {
    System.out.println(matcher.group());
}

要求3

String origin = "Java 自从 95 年问世以来,经历了很多版本,目前企业中用的最多的是 Java8 和 Java11," +
    "因为这两个是长期支持版本,下一个长期支持版本是 Java17,相信在未来不久 Java17 也会逐渐登上历史舞台。";

//提取带版本号的 Java,使用非捕获组辅助匹配,最终可能被一起捕获
Matcher matcher = Pattern.compile("((?i)Java)(?:\\d{1,2})").matcher(origin);
while (matcher.find()) {
    System.out.println(matcher.group());
}

要求4

String origin = "Java 自从 95 年问世以来,经历了很多版本,目前企业中用的最多的是 Java8 和 Java11," +
    "因为这两个是长期支持版本,下一个长期支持版本是 Java17,相信在未来不久 Java17 也会逐渐登上历史舞台。";

//爬取除了版本号的Java
//负向断言预查:不匹配
Matcher matcher = Pattern.compile("((?i)Java)(?!\\d{1,2})").matcher(origin);
while (matcher.find()) {
    System.out.println(matcher.group());
}

满足下列要求

爬取下列文本中的关键信息:

来黑马程序员学习 Java,

手机号: 18512516758 , 18512508907

或者联系邮箱: boniu@itcast.cn

座机电话: 01036517895 , 010-98951256

邮箱: bozai@itcast.cn

热线电话: 400-618-9090 , 400-618-4000 ,4006184000 ,4006189090

实现

//抓取信息:获得关键信息
String text = "来黑马程序员学习 Java,\n" +
    "手机号: 18512516758 , 18512508907\n" +
    "或者联系邮箱: boniu@itcast.cn\n" +
    "座机电话: 01036517895 , 010-98951256\n" +
    "热线电话: 400-618-9090 , 400-618-4000 ,4006184000 ,4006189090\n";

String regex = "(1[3-9]\\d{9})|(\\w+@\\w+\\.\\w+)|(010-?\\d{8})|(400-?\\d{3}-?\\d{4})";
Matcher matcher2 = Pattern.compile(regex).matcher(text);

while (matcher2.find()) {
    System.out.println(matcher2.group());
}

1-10.6 练习使用正则表达式(非)贪婪爬取文本

贪婪爬取:意如其名,尽可能多地爬取数据;

非贪婪爬取:尽可能少地爬取数据。

一般而言,Java 默认为贪婪爬取,若要实现非贪婪爬取,只需要在贪婪数量词(+, *)后加上 ? 即可。

满足以下要求

  • 尽可能多地爬取 abbbbb
  • 尽可能少地爬取 ab

爬取文本:abbbbbbbbbbbbaaaaaaaaaaaaaaaaaa

实现

String text = "abbbbbbbbbbbbaaaaaaaaaaaaaaaaaa";

Matcher greedy = Pattern.compile("ab+").matcher(text);
Matcher nonGreedy = Pattern.compile("ab+?").matcher(text);

while (greedy.find()) {
    System.out.println(greedy.group());
}
while (nonGreedy.find()) {
    System.out.println(nonGreedy.group());
}

1-10.7 正则表达式在 String 类中方法的应用

String 类中,除了 matches() 方法,replaceAll()split() 方法也支持正则表达式。前者将字符串中满足正则表达式的内容全部替换为新内容,后者使用正则表达式分割字符串。

练习1:使用正则表达式替换文本,将非中文字符替换为 " and "

李华qwerty_uioop韩梅梅qwerty_uiop罗宾

实现

String text = "李华qwerty_uioop韩梅梅qwerty_uiop罗宾";

//全部替换
String res = text.replaceAll("\\w+", " and ");
System.out.println("替换后:" + res);

练习2:使用正则表达式对上述文本分割,将三人姓名分隔开

String[] arr = text.split("\\w+");
for (String x: arr) {
    System.out.println(x);
}

String 类中还有一些其他支持正则表达式的方法。一般而言,支持正则表达式的方法,其形参名称为 regex,即正则表达式。

1-10.8 捕获组与非捕获组练习

练习1:在正则表达式内再次使用捕获组的数据匹配文本

  • 字符串首尾各 1 个字符相同;
  • 字符串首尾多个字符相同;
  • 字符串首尾相同,且内部字符相同;

实现1

System.out.println("a123a".matches("(.).*\\1"));
System.out.println("b456b".matches("(.).*\\1"));
System.out.println("1abc1".matches("(.).*\\1"));
System.out.println("a123b".matches("(.).*\\1"));

实现2

System.out.println("abc123abc".matches("(.+).*\\1"));
System.out.println("abcdefabc".matches("(.+).*\\1"));
System.out.println("123abc123".matches("(.+).*\\1"));
System.out.println("123abc456".matches("(.+).*\\1"));

实现3

System.out.println("aaaaaaaa".matches("((.)\\2*).*\\1"));
System.out.println("aaaa1234aaaa".matches("((.)\\2*).*\\1"));
System.out.println("aaaa1234bbdd".matches("((.)\\2*).*\\1"));

练习2:在正则表达式外部使用捕获组的内容

删去重复字符,只保留 1 个;

实现

System.out.println("我要学学学学编编编编编程程程程程程程程".replaceAll("(.)\\1+","$1"));

1-10.9 外部链接

posted @ 2023-07-23 18:41  Zebt  阅读(415)  评论(0)    收藏  举报