Java中的正则表达式 regex

前言

    正则表达式不仅仅是Java的技术,在任何一门编程语言中都会存在,是一种通用的IT技术,其理念和用法在任何编程语言中基本一致,除了有一些由于语言不同而导致的一些语法不同正则表达式,主要用于匹配(查找 替换 计数)字符串中的数据的,也叫做文本匹配技术。

一、正则表达式

    经常会需要编写代码来验证用户输人,比如验证输人是否是一个数字,是否是一个全部小写的字符串,或者社会安全号。如何编写这种类型的代码呢?一个简单而有效的做法是使用正则表达式来完成这个任务。
    正則表达式(regular expression,简写为 regex) 是一个字符串,用来描述匹配一个字符串集合的模式。对于字符串处理来说,正则表达式是一个强大的工具。可以使用正则表达式来匹配、替换和分割字符串。
    比如:给定一个字符串"13088889090",如何判断它是手机号?

public boolean isPhoneNumber(String s) {
    if (s.length() != 11) {
        return false;
    }
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        if (c < '0' || c > '9') {
            return false;
        }
    }
    return true;
}

    对于上述问题的求解而言,我们只解决了长度的问题,内容必须是数字字符的问题,但是,对于手机号这个特殊的数字而言
    第1位能否为0?前3位表示对应公司(联通 移动 电信)123 109 110??? 前3位它不是真随机的?就现在这两个问题而言,如果再去用代码做的话,会比较复杂一些;
    比如:对于给定给一个字符串s,如何判断该字符串是邮箱地址呢?

public boolean isEmail(String s) {
    ......
}

    “zh@xianoupeng.com” “346530074@qq.com” "H_LaK_123@haha.xixi.lala.gov"
    为了每一种判断逻辑都要编写代码实现是太繁琐了,正则表达式就是出来解决这些问题的!

    如果用正则表达式,去匹配手机号的话

public boolean isPhoneNumber(String s) {
    //确定了长度为11 并且也确定了必须是数字字符
    return s.matches("\\d{11}");
}

    从目前的角度而言,问题的解决确实变简单了,代码可读性的难度就上来了
    在Java标准库中java.uitl.regex包里内置了正则表达式引擎,在Java程序中使用正则非常方便,使用的正则表达式就是一个描述规则的字符串,所以,我们只需要编写正确的规则,我们就可以让正则表达式引擎去判断目标字符串是否符合规则即可。
    比如:判断一个年份 "20##"年,我们写出规则如下:
    一个4个字符:2,0 ,0~9任意一个数字 ,0~9任意一个数字;
     对应的正则表达式:20\d\d 其中\d表示的是任意一个数字的意思;
     但是在Java中,正则表达式要用字符串来表示,"20\d\d"这样写是不对的,因为在Java中 " \ “是转义字符,最终应该这么写"20\\d\\d”。

二、匹配规则

     正则表达式的匹配规则是从左到右按规则匹配的。我们首先来看如何用正则表达式进行精确匹配:
     对于正则表达式 abc 来说,它只能精确地匹配字符串 “abc” ,不能匹配 “ab” , “Abc” , “abcd” 等其他任何字符串。
    如果正则表达式有特殊字符,那就需要用 \ 转义。例如,正则表达式 a\&c ,其中 \& 是用来匹配特殊字符 & 的,它能精确匹配字符串 “a&c” ,但不能匹配 “ac” 、 “a-c” 、 “a&&c” 等。
    要注意正则表达式在Java代码中也是一个字符串,所以,对于正则表达式 a\&c 来说,对应的Java字符串是 “a\\&c”因为 \ 也是Java字符串的转义字符两个 \\ 实际上表示的是一个 \

        String regex01 = "20\\d\\d";
        System.out.println("2019".matches(regex01));
        System.out.println("2100".matches(regex01));
        System.out.println("20AB".matches(regex01));

        String regex02 = "abc";
        System.out.println("abc".matches(regex02));
        System.out.println("ab".matches(regex02));

        String regex03 = "a\\&c";
        System.out.println("a&c".matches(regex03));
        System.out.println("ac".matches(regex03));

    如果想匹配非ASCII字符,例如中文,那就用 \u#### 的十六进制表示,例如: a\u548cc 匹配字符串 “a和c” ,中文字符 的Unicode编码是 548c

        String regex04 = "a\\u548cc";
        System.out.println("a和c".matches(regex04));
        System.out.println("a哈c".matches(regex04));

三、匹配任意字符

    精确匹配实际上用处不大,因为我们直接用 String.equals() 就可以做到。大多数情况下,我们想要的匹配规则更多的是模糊匹配。我们可以用 “ . ”匹配一个任意字符
    例如,正则表达式 a.c 中间的 . 可以匹配一个任意字符,例如,下面的字符串都可以被匹配:

  • abc” ,因为 . 可以匹配字符 b
  • "a&c" ,因为 . 可以匹配字符 &
  • acc” ,因为 . 可以匹配字符 c

    但它不能匹配 “ac” 、 “a&&c” ,因为 . 匹配一个字符且仅限一个字符。

String regex05 = "a.c";
System.out.println("abc".matches(regex05));//true
System.out.println("a&c".matches(regex05));//true
System.out.println("acc".matches(regex05));//true
System.out.println("ac".matches(regex05));//false
System.out.println("a&&c".matches(regex05));//false

四、匹配数字

    用 . 可以匹配任意字符,这个口子开得有点大。如果我们只想匹配 0 ~ 9 这样的数字,可以用 \d 匹配。例如,正则表达式 00\d 可以匹配:

  • “007” ,因为 \d 可以匹配字符 7 ;
  • “008” ,因为 \d 可以匹配字符 8 。

    它不能匹配 “00A” , “0077” ,因为 \d 仅限单个数字字符。

String regex06 = "Demo\\d\\d\\.java";
System.out.println("Demo11.java".matches(regex06));//true
System.out.println("Demo123.java".matches(regex06));//false
System.out.println("Demo.java".matches(regex06));//false
System.out.println("Demo12.avi".matches(regex06));//false

五、匹配常用字符

    用 \w 可以匹配一个字母、数字或下划线,w的意思是word。例如, java\w 可以匹配:

  • “javac” ,因为 \w 可以匹配英文字符 c ;
  • “java9” ,因为 \w 可以匹配数字字符 9 ;
  • “java_” ,因为 \w 可以匹配下划线 _ 。

    它不能匹配 “java#” , "java " ,因为 \w 不能匹配 # 、空格等字符。

String regex07 = "java\\w";
System.out.println("java_".matches(regex07));//true
System.out.println("javac".matches(regex07));//true
System.out.println("java".matches(regex07));//false
System.out.println("java!".matches(regex07));//false

六、匹配空格字符

    用 \s 可以匹配一个空格字符,注意空格字符不但包括空格 " “`,还包括tab字符(在Java中用制表符”\t"表示)。 例如,a\sc可以匹配:

  • “a c” ,因为 \s 可以匹配空格字符" "
  • “a c” ,因为 \s 可以匹配tab字符 \t 。

    它不能匹配 “ac” , “abc” 等。

String regex08 = "a\\sc";
System.out.println("a c".matches(regex08));//true
System.out.println("a  c".matches(regex08));//false
System.out.println("ac".matches(regex08));//false
System.out.println("a   c".matches(regex08));//false
System.out.println("a\tc".matches(regex08));//true

七、匹配非数字

    用 \d 可以匹配一个数字,而 \D 则匹配一个非数字。例如, 00\D 可以匹配:

  • “00A” ,因为 \D 可以匹配非数字字符 A ;
  • “00#” ,因为 \D 可以匹配非数字字符 # 。

     00\d 可以匹配的字符串 “007” , “008” 等, 00\D 是不能匹配的。
    类似的, \W 可以匹配 \w 不能匹配的字符, \S 可以匹配 \s 不能匹配的字符,这几个正好是反着来的。

所以说:

  • \d任意一个数字 \D任意一个非数字;
  • \s任意一个空格 \S任意一个非空格;
  • \w任意一个数字字母下划线 \W任意一个非数字字母下划线
String regex09 = "\\d\\w\\W\\D";
System.out.println("1234".matches(regex09));//false
System.out.println("9w!c".matches(regex09));//true

八、重复匹配

    我们用 \d 可以匹配一个数字,例如, A\d 可以匹配 “A0” , “A1” ,如果要匹配多个数字,比如 “A380” ,怎么办?

    修饰符 * 可以匹配任意个字符,包括0个字符。我们用 A\d* 可以匹配:

  • A :因为 \d* 可以匹配0个数字;
  • A0 :因为 \d* 可以匹配1个数字 0 ;
  • A380 :因为 \d* 可以匹配多个数字 380 。
String regex10 = "A*";  //连续0个或多个A
System.out.println("A".matches(regex10));
System.out.println("Animal".matches(regex10));
System.out.println("A123123".matches(regex10));
System.out.println("AAAAAAA".matches(regex10));
String regex11 = "A\\d*";
System.out.println("A12313123123".matches(regex11));
System.out.println("A".matches(regex11));
System.out.println("Aasdhjahsjdk1123".matches(regex11));
System.out.println("".matches(regex11));

    修饰符 + 可以匹配至少一个字符。我们用 A\d+ 可以匹配:

  • A0 :因为 \d+ 可以匹配1个数字 0 ;
  • A380 :因为 \d+ 可以匹配多个数字 380 。

    但它无法匹配 “A” ,因为修饰符 + 要求至少一个字符。

String regex12 = "A\\d+";
System.out.println("A12313123123".matches(regex12));
System.out.println("A".matches(regex12));
System.out.println("Aasdhjahsjdk1123".matches(regex12));
System.out.println("".matches(regex12));

    修饰符 ? 可以匹配0个或一个字符。我们用 A\d? 可以匹配:

  • A :因为 \d? 可以匹配0个数字;
  • A0 :因为 \d+ 可以匹配1个数字 0 。

    但它无法匹配 “A33” ,因为修饰符 ? 超过1个字符就不能匹配了。

String regex13 = "A\\d?";
System.out.println("A12313123123".matches(regex13));
System.out.println("A".matches(regex13));
System.out.println("A1".matches(regex13));
System.out.println("".matches(regex13));

    如果我们想精确指定n个字符怎么办?用修饰符 {n} 就可以。 A\d{3} 可以精确匹配:

  • A380 :因为 \d{3} 可以匹配3个数字 380 。

    如果我们想指定匹配n~m个字符怎么办?用修饰符 {n,m} 就可以。 A\d{3,5} 可以精确匹配:

  • A380 :因为 \d{3,5} 可以匹配3个数字 380 ;
  • A3800 :因为 \d{3,5} 可以匹配4个数字 3800 ;
  • A38000 :因为 \d{3,5} 可以匹配5个数字 38000 。

    如果没有上限,那么修饰符 {n,} 就可以匹配至少n个字符。

System.out.println("A123".matches("A\\d{3}"));
System.out.println("A123".matches("A\\d{3,5}"));
System.out.println("A12345".matches("A\\d{3,5}"));
System.out.println("A123".matches("A\\d{4,}"));

    比如匹配一个座机号 010-33167854 3个数字-8个数字 \d{3}\-\d{8}

System.out.println("010-12345678".matches("\\d{3}\\-\\d{8}"));
System.out.println("0101-2345678".matches("\\d{3}\\-\\d{8}"));

九、正则表达式小总结

正则表达式       规则              可以匹配
A               固定字符A           "A"
\u548c          指定Unicode字符     "和"
.               任意一个字符         "a" "1" " "
\d              一个数字字符0~9      "0"~"9"
\w              一个数字或字母或下划线
\W              非\w
\D              非\d
\s              一个空格字符(\t ' ')
\S              非\s
A*              A有任意个           "" "A" "AA" "AAA"
A+              A至少一个           "A" "AA" "AAA"
A?              A0个或1个           "" "A"
A{3}            A出现3次            "AAA"
A{3,5}          A出现3~5次         "AAA"  "AAAA"  "AAAAA"
A{2,}           A出现至少2次       "AA"  "AAA"  "AAAA"
A{0,3}          A出现0~3次         ""  "A"  "AA"  "AAA"

十、匹配开头和结尾

    用正则表达式进行多行匹配时,我们用 ^ 表示开头$ 表示结尾。例如, ^A\d{3}$ ,可以匹配 “A001” 、 “A380” 。

String regex01 = "^Demo\\w*\\.java$";
System.out.println("Demo01.java".matches(regex01));//true

十一、匹配指定范围

    如果我们规定一个7~8位数字的电话号码不能以 0 开头,应该怎么写匹配规则呢? \d{7,8} 是不行的,因为第一个 \d 可以匹配到 0 。
    使用 […] 可以匹配范围内的字符,例如, [123456789] 可以匹配 1 ~ 9 ,这样就可以写出上述电话号码的规则: [123456789]\d{6,7}
    把所有字符全列出来太麻烦, […] 还有一种写法,直接写 [1-9] 就可以。
    要匹配大小写不限的十六进制数,比如 1A2b3c ,我们可以这样写: [0-9a-fA-F] ,它表示一共可以匹配以下任意范围的字符:

  • 0-9 :字符 0 ~ 9 ;
  • a-f :字符 a ~ f ;
  • A-F :字符 A ~ F 。

    如果要匹配6位十六进制数,前面讲过的 {n} 仍然可以继续配合使用: [0-9a-fA-F]{6}
    […] 还有一种排除法,即不包含指定范围的字符。假设我们要匹配任意字符,但不包括数字,可以写 [^1-9]{3}

  • 可以匹配 “ABC” ,因为不包含字符 1 ~ 9 ;
  • 可以匹配 “A00” ,因为不包含字符 1 ~ 9 ;
  • 不能匹配 “A01” ,因为包含字符 1 ;
  • 不能匹配 “A05” ,因为包含字符 5 。
String regex02 = "[2-9]\\d{5}";
System.out.println("345678".matches(regex02));//true
System.out.println("123456".matches(regex02));//false

String regex03 = "[0-9a-fA-F]{4}";
System.out.println("12ab".matches(regex03));//true
System.out.println("wc99".matches(regex03));//false

十二、逻辑或匹配规则

    用 | 连接的两个正则规则是或 规则,例如, AB|CD 表示可以匹配 AB 或 CD 。

String regex04 = ".*java|c|python|go.*";
System.out.println("I like java".matches(regex04));//true
System.out.println("I like c++".matches(regex04));//false

十三、使用括号

    现在我们想要匹配字符串 learn java 、 learn php 和 learn go 怎么办?一个最简单的规则是 learn\sjava|learn\sphp|learn\sgo ,但是这个规则太复杂了,可以把公共部分提出来,然后用 (…) 把子规则括起来表示成 learn\s(java|php|go)

String regex05 = "learn\\s(java|php|go)";
System.out.println("learn java".matches(regex05));//true
System.out.println("learn c++".matches(regex05));//false

复杂匹配规则主要有:
在这里插入图片描述

十四、分组匹配

    我们前面讲到的 (…) 可以用来把一个子规则括起来,这样写 learn\s(java|php|go) 就可以更方便地匹配长字符串了。
    实际上 (…) 还有一个重要作用,就是分组匹配。
    我们来看一下如何用正则匹配 区号-电话号 码这个规则。利用前面讲到的匹配规则,写出来很容易:

\d{3,4}-\d{6,8}

    虽然这个正则匹配规则很简单,但是往往匹配成功后,下一步是提取区号和电话号码,分别存入数据库。于是问题来了:如何提取匹配的子串?
    当然可以用 String 提供的 indexOf()substring() 这些方法,但它们从正则匹配的字符串中提取子串没有通用性,下一次要提取 learn\s(java|php) 还得改代码。
    正确的方法是用 (…) 先把要提取的规则分组,把上述正则表达式变为 (\d{3,4})- (\d{6,8})
    现在问题又来了:匹配后,如何按括号提取子串?
    现在我们没办法用 String.matches() 这样简单的判断方法了,必须引入 java.util.regex 包,底层实际上先创建一个关于正则表达式规则的对象Pattern, 然后规则对象Pattern对数据进行匹配,生成一个匹配对象Matcher,匹配的结果:都在Matcher里,有一个方法Matcher中的**mathces()**才是最终判断字符串是否匹配规则的结果boolean,如果匹配成功,就可以直接从 Matcher.group(index) 返回子串:

Pattern p = Pattern.compile("(\\d{3,4})-(\\d{6,8})");
Matcher m = p.matcher("010-12345678");
if (m.matches()) {
	//获取的结果是整个电话号
    System.out.println(m.group(0));
    //获取的第一个部分 区号
    System.out.println(m.group(1));
    //获取的第二个部分 电话
    System.out.println(m.group(2));
}

在这里插入图片描述

Pattern

    我们在前面的代码中用到的正则表达式代码是 String.matches() 方法,而我们在分组提取的代码中用的是 java.util.regex 包里面的 Pattern 类和 Matcher 类。实际上这两种代码本质上是一样的,因为 String.matches() 方法内部调用的就是 PatternMatcher 类的方法。
    但是反复使用 String.matches() 对同一个正则表达式进行多次匹配效率较低,因为每次都会创建出一样的 Pattern 对象。完全可以先创建出一个 Pattern 对象,然后反复使用,就可以实现编译一次,多次匹配:

public class Main { 
	public static void main(String[] args) { 
		Pattern pattern = Pattern.compile("(\\d{3,4})\\-(\\d{7,8})"); 
		pattern.matcher("010-12345678").matches(); // true 
		pattern.matcher("021-123456").matches(); // true 
		pattern.matcher("022#1234567").matches(); // false 
		// 获得Matcher对象: 
		Matcher matcher = pattern.matcher("010-12345678"); 
		if (matcher.matches()) { 
			String whole = matcher.group(0); // "010-12345678", 0表示匹配的整个字符串 
			String area = matcher.group(1); // "010", 1表示匹配的第1个子串 
			String tel = matcher.group(2); // "12345678", 2表示匹配的第2个子串 
			System.out.println(area); 
			System.out.println(tel); 
		}
	}
}
使用Matcher时,必须首先调用matches()判断是否匹配成功,匹配成功后,才能调用group()提取子串。 
利用提取子串的功能,我们轻松获得了区号和号码两部分。

正则表达式用 (...) 分组可以通过 Matcher 对象快速提取子串:

  • group(0) 表示匹配的整个字符串;
  • group(1) 表示第1个子串;
  • group(2) 表示第2个子串,以此类推。

十五、非贪婪匹配

    在介绍非贪婪匹配前,我们先看一个简单的问题:
    给定一个字符串表示的数字,判断该数字末尾 0 的个数。例如:

  • "123000" :3个 0
  • "10100" :2个 0
  • "1001":0个0

    可以很容易地写出该正则表达式:(\d+)(0*),Java代码如下:

Pattern p1 = Pattern.compile("(\\d+)(0*)");
Matcher m1 = p1.matcher("123000");
if (m1.matches()) {
     System.out.println("group1 = " + m1.group(1));
     System.out.println("group2 = " + m1.group(2));
}

在这里插入图片描述
    然而打印的第二个子串是空字符串" "

实际上,我们期望分组匹配结果是:
在这里插入图片描述
但实际的分组匹配结果是这样的:
在这里插入图片描述
    仔细观察上述实际匹配结果,实际上它是完全合理的,因为 \d+确实可以匹配后面任意个 0
     这是因为正则表达式默认使用贪婪匹配:任何一个规则,它总是尽可能多地向后匹配,因此,\d+ 总是会把后面的 0 包含进来。
    要让 \d+ 尽量少匹配,让 0* 尽量多匹配,我们就必须让 \d+ 使用非贪婪匹配在规则 \d+ 后面加个 ? 即可表示非贪婪匹配。 我们改写正则表达式如下:

Pattern p1 = Pattern.compile("(\\d+?)(0*)");
Matcher m1 = p1.matcher("123000");
if (m1.matches()) {
     System.out.println("group1 = " + m1.group(1));
     System.out.println("group2 = " + m1.group(2));
}

结果:
在这里插入图片描述
    因此,给定一个匹配规则,加上 ?后就变成了非贪婪匹配。
    我们再来看这个正则表达式 (\d??)(9*) ,注意 \d? 表示匹配0个或1个数字,后面第二个 ? 表示非贪婪匹配,因此,给定字符串"9999"匹配到的两个子串分别是"""9999" 因为对于\d?来说,可以匹配1个9 ,也可以匹配0个 9 ,但是因为后面的 ? 表示非贪婪匹配,它就会尽可能少的匹配,结果是匹配了0个 9

十六、分割字符串

    使用正则表达式分割字符串可以实现更加灵活的功能。 String.split() 方法传入的正是正则表达式。我们来看下面的代码:

System.out.println(Arrays.toString("a b c".split("\\s")));
System.out.println(Arrays.toString("a  b      c".split("\\s")));
System.out.println(Arrays.toString("a  b      c".split("\\s+")));
System.out.println(Arrays.toString("a  ,  b ;; c ,,;; d ,   e".split("[\\,\\;\\s]+")));

结果:
在这里插入图片描述
    如果我们想让用户输入一组标签,然后把标签提取出来,因为用户的输入往往是不规范的,这时,使用合适的正则表达式,就可以消除多个空格、混合 ,; 这些不规范的输入,直接提取出规范的字符串。

十七、搜索字符串

    使用正则表达式还可以搜索字符串,我们来看例子:

String s = "the quick brown fox jumps over the lazy dog";
Pattern p2 = Pattern.compile("\\wo\\w");
Matcher m2 = p2.matcher(s);
while (m2.find()) {
      //Matcher的start和end是从左到右每一个匹配到的字符串在原字符串中的起始位置
      String sub = s.substring(m2.start(),m2.end());
      System.out.println(sub);
}

结果:
在这里插入图片描述
    我们获取到 Matcher 对象后,不需要调用 matches() 方法(因为匹配整个串肯定返回false),而是反复调用 find() 方法,在整个串中搜索能匹配上\\wo\\w规则的子串,并打印出来。这种方式比 String.indexOf() 要灵活得多,因为我们搜索的规则是3个字符:中间必须是o,前后两个必须是字符 [A-Za-z0-9_]

十八、替换字符串

    使用正则表达式替换字符串可以直接调用 String.replaceAll()它的第一个参数是正则表达式,第二个参数是待替换的字符串。我们还是来看例子:

String s1 = "The quick\t\t brown fox jumps over the lazy dog."; 
s1 = s1.replaceAll("\\s+", " "); 
System.out.println(s1); // "The quick brown fox jumps over the lazy dog."

    上面的代码把不规范的连续空格分隔的句子变成了规范的句子。可见,灵活使用正则表达式可以大大降低代码量。

十九、反向引用

    如果我们要把搜索到的指定字符串按规则替换,比如前后各加一个 xxxx,这个时候,使用replaceAll()的时候,我们传入的第二个参数可以使用 $1$2 来反向引用匹配到的子串。例如:

String s1 = "the   quick   brown   fox     jumps   over     the     lazy    dog";
s1 = s1.replaceAll("\\s([a-z]{4})\\s","<h1>$1</h1>");//<h1>lazy</h1>
System.out.println(s1);

String s2 = "010-123456 0101-33445566 010-98712345";
//"010-123456" => "(010)-[123456]"
s2 = s2.replaceAll("(\\d{3,4})-(\\d{6,8})","($1)-[$2]");
System.out.println(s2);

结果:
在这里插入图片描述
    它实际上把任何4字符单词的前后用xxxx括起来。实现替换的关键就在于 " $1 ",它用匹配的分组子串 ([a-z]{4})替换了$1

  • 17
    点赞
  • 71
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值