正则表达式(Java)

正则表达式

0 为什么需要学?

我们先看几个非常常见的问题:

  • 如何判断字符串是否是有效的电话号码?例如:010-1234567123ABC45613510001000等;
  • 如何判断字符串是否是有效的电子邮件地址?例如:test@example.comtest#example等;
  • 如何判断字符串是否是有效的时间?例如:12:3409:6099:99等。

一种直观的想法是通过程序判断,这种方法需要为每种用例创建规则,然后用代码实现。下面是判断手机号的代码:

boolean isValidMobileNumber(String s) {
    // 是否是11位?
    if (s.length() != 11) {
        return false;
    }
    // 每一位都是0~9:
    for (int i=0; i<s.length(); i++) {
        char c = s.charAt(i);
        if (c < '0' || c > '9') {
            return false;
        }
    }
    return true;
}

上述代码仅仅做了非常粗略的判断,并未考虑首位数字不能为 0 等更详细的情况。

除了判断手机号,我们还需要判断电子邮件地址、电话、邮编等等:

boolean isValidMobileNumber(String s) { ... }
boolean isValidEmail(String s) { ... }
boolean isValidPhoneNumber(String s) { ... }
boolean isValidZipCode(String s) { ... }
...

为每一种判断逻辑编写代码实在是太繁琐了。有没有更简单的方法?

有!用正则表达式!

  • 正则表达式可以用字符串来描述规则,并用来匹配字符串。例如,判断手机号,正则表达式的写法为 \d{11}
boolean isValidMobileNumber(String s) {
    return s.matches("\\d{11}");
}
  • 使用正则表达式的好处有哪些?一个正则表达式就是一个描述规则的字符串,所以,只需要编写正确的规则,我们就可以让正则表达式引擎去判断目标字符串是否符合规则;

  • 正则表达式是一套标准,它可以用于任何语言。Java标准库的 java.util.regex 包内置了正则表达式引擎,在 Java 程序中使用正则表达式非常简单。

举个例子:要判断用户输入的年份是否是 20## 年,我们先写出规则如下:

一共有4个字符,分别是:200 ~ 9任意数字0 ~ 9任意数字

对应的正则表达式就是:20\d\d,其中 \d表示任意一个数字。

把正则表达式转换为Java字符串就变成了 20\\d\\d ( 注意Java字符串用\\表示\ )。

最后,用正则表达式匹配一个字符串的代码如下:

// 第一个【正则表达式】程序
public class HelloZhengZe {
    public static void main(String[] args) {
        String regex = "20\\d\\d";
        System.out.println("2019".matches(regex)); // true
        System.out.println("2100".matches(regex)); // false
    }
}

1 匹配规则

正则表达式的匹配规则是从左到右按规则匹配。我们首先来看如何使用正则表达式来做精确匹配。

对于正则表达式 abc 来说,它只能精确地匹配字符串 "abc"不能匹配 "ab""Abc""abcd"其他任何字符串

  • 如果正则表达式有特殊字符,那就需要用\转义。例如,正则表达式 a\&c,其中 \& 是用来匹配特殊字符 & 的,它能精确匹配字符串 "a&c",但不能匹配 "ac""a-c""a&&c"等。
public class Main {
    public static void main(String[] args) {
        String re1 = "abc";
        System.out.println("abc".matches(re1));  // true
        System.out.println("Abc".matches(re1));  // false
        System.out.println("abcd".matches(re1)); // false

        String re2 = "a\\&c"; // 对应的正则是a\&c
        System.out.println("a&c".matches(re2));  // true
        System.out.println("a-c".matches(re2));  // false
        System.out.println("a&&c".matches(re2)); // false
    }
}

要注意正则表达式在 Java 代码中也是一个字符串,所以,对于正则表达式 a\&c 来说,对应的 Java 字符串是 "a\\&c",因为 \ 也是Java 字符串的转义字符,两个 \\ 实际上表示的是一个 \

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

1.1 匹配任意字符

精确匹配实际上用处不大,因为我们直接用 String.equals() 就可以做到。大多数情况下,我们想要的匹配规则更多的是模糊匹配。我们可以用 . 匹配一个任意字符。

例如,正则表达式 a.c 中间的.可以匹配一个任意字符,例如,下面的字符串都可以被匹配:

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

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

1.2 匹配数字

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

  • "007",因为 \d 可以匹配字符 7
  • "008",因为 \d 可以匹配字符 8

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

1.3 匹配常用字符

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

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

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

1.4 匹配空格字符

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

  • "a c",因为\s可以匹配空格字符 ’ ';
  • "a c",因为\s可以匹配tab字符 \t

它不能匹配"ac""abc"等。

1.5 匹配非数字

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

  • "00A",因为\D可以匹配非数字字符A
  • "00#",因为\D可以匹配非数字字符#

00\d可以匹配的字符串"007""008"等,00\D是不能匹配的。

类似的,\W 可以匹配\w不能匹配的字符,\S可以匹配\s不能匹配的字符,这几个正好是反着来的。

1.6 重复匹配

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

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

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

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

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

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

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

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

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

如果我们想精确指定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个字符。

1.7 规则总结

单个字符的匹配规则如下:

正则表达式规则可以匹配
A指定字符A
\u548c指定Unicode字符
.任意字符ab&0
\d数字0~90~9
\w大小写字母,数字和下划线a`z`,`A`Z0~9_
\s空格、Tab键空格,Tab
\D非数字aA&_,……
\W非\w&@,……
\S非\saA&_,……

多个字符的匹配规则如下:

正则表达式规则可以匹配
A*任意个数字符空,AAAAAA,……
A+至少1个字符AAAAAA,……
A?0个或1个字符空,A
A{3}指定个数字符AAA
A{2,3}指定范围个数字符AAAAA
A{2,}至少n个字符AAAAAAAAA,……
A{0,3}最多n个字符空,AAAAAA

2 复杂的匹配规则

2.1 匹配开头和结尾

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

2.2 匹配指定范围

如果我们规定一个 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

2.3 或规则匹配

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

  • 【例】正则表达式 java|php
    • 它可以匹配 "java""php" ,但无法匹配 "go"
    • 要把 go 也加进来匹配,改写应为: java|php|go

2.4 使用括号

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

  • 上面的规则仍然不能匹配learn Javalearn Go这样的字符串。试着修改正则,使之能匹配大写字母开头的learn Javalearn Phplearn Go

2.5 规则总结

复杂匹配规则主要有:

正则表达式规则可以匹配
^开头字符串开头
$结尾字符串结束
[ABC][…] 内任意字符A,B,C
[A-F0-9xy]指定范围的字符A,……,F0,……,9xy
[^A-F]指定范围外的任意字符A~F
AB|CD|EFAB 或 CD或 EFABCDEF

3 分组匹配

我们前面讲到的 (...) 可以用来把一个子规则括起来,这样写 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 对象匹配,匹配后获得一个 Matcher 对象,如果匹配成功,就可以直接从 Matcher.group(index) 返回子串:

// import java.util.regex.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    public static void main(String[] args) {
        Pattern p = Pattern.compile("(\\d{3,4})\\-(\\d{7,8})");
        Matcher m = p.matcher("010-12345678");
        if (m.matches()) {
            String g = m.group(0);
            String g1 = m.group(1);
            String g2 = m.group(2);
            System.out.println(g);  // 010-12345678
            System.out.println(g1); // 010
            System.out.println(g2); //12345678 
        } else {
            System.out.println("匹配失败!");
        }
    }
}

要特别注意,Matcher.group(index)方法的参数用 1 表示第一个子串2 表示第二个子串0 表示整个正则匹配到的字符串

3.1 Pattern

我们在前面的代码中用到的正则表达式代码是 String.matches() 方法,而我们在分组提取的代码中用的是 java.util.regex 包里面的 Pattern 类和 Matcher 类。实际上这两种代码本质上是一样的,因为 String.matches() 方法内部调用的就是 PatternMatcher 类的方法。

但是反复使用 String.matches() 对同一个正则表达式进行多次匹配效率较低,因为每次都会创建出一样的 Pattern 对象。完全可以先创建出一个 Pattern 对象,然后反复使用,就可以实现编译一次,多次匹配;

  • 使用 Matcher 时,必须首先调用 matches() 判断是否匹配成功,匹配成功后,才能调用 group() 提取子串。

  • 利用提取子串的功能,我们轻松获得了区号和号码两部分。

// import java.util.regex.*;
import java.util.regex.Matcher;
import java.util.regex.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();   // false
        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);
        }
    }
}

【练习】利用分组匹配,从字符串"23:01:59"提取时、分、秒。

/**
 *@Copyright: TalentRY
 */

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    public static void main(String[] args) {
        Pattern p = Pattern.compile("(\\d{2})\\:(\\d{2})\\:(\\d{2})");
        Matcher m = p.matcher("28:01:99");
        if (m.matches()) {
            String a = m.group(1);
            String b = m.group(2);
            String c = m.group(3);
            if (Integer.parseInt(a) > 24 || Integer.parseInt(a) < 0) {
                System.out.println("时针超出范围");
            }else if (Integer.parseInt(b) > 60 || Integer.parseInt(b) < 0){
                System.out.println("分针超出范围");
            }else if (Integer.parseInt(c) > 60 || Integer.parseInt(c) < 0){
                System.out.println("秒针超出范围");
            }else {
                System.out.println(a);
                System.out.println(b);
                System.out.println(c);
            }
        } else {
            System.out.println("匹配失败!");
        }
    }
}

3.2 非贪婪匹配

在介绍非贪婪匹配前,我们先看一个简单的问题:

给定一个字符串表示的数字,判断该数字末尾0的个数。例如:

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

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

import java.util.regex.*;

public class Main {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("(\\d+)(0*)");
        Matcher matcher = pattern.matcher("1230000");
        if (matcher.matches()) {
            System.out.println("group1=" + matcher.group(1)); // "1230000"
            System.out.println("group2=" + matcher.group(2)); // ""
        }
    }
}

然而打印的第二个子串是空字符串""

实际上,我们期望分组匹配结果是:

input\d+0*
123000“123”“000”
10100“101”“00”
1001“1001”“”

但实际的分组匹配结果是这样的:

input\d+0*
123000“123000”“”
10100“10100”“”
1001“1001”“”

仔细观察上述实际匹配结果,实际上它是完全合理的,因为 \d+ 确实可以匹配后面任意个0

这是因为正则表达式默认使用贪婪匹配:任何一个规则,它总是尽可能多地向后匹配,因此,\d+ 总是会把后面的0包含进来。

要让\d+尽量少匹配,让 0* 尽量多匹配,我们就必须让 \d+ 使用非贪婪匹配。在规则 \d+ 后面加个 ? 即可表示非贪婪匹配。我们改写正则表达式如下:

import java.util.regex.*;

public class Main {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("(\\d+?)(0*)");
        Matcher matcher = pattern.matcher("1230000");
        if (matcher.matches()) {
            System.out.println("group1=" + matcher.group(1)); // "123"
            System.out.println("group2=" + matcher.group(2)); // "0000"
        }
    }
}

因此,给定一个匹配规则,加上?后就变成了非贪婪匹配。

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

3.3 小结

正则表达式匹配默认使用贪婪匹配,可以使用?表示对某一规则进行非贪婪匹配。

注意区分?的含义:\d??

4 搜索和替换

4.1 分割字符串

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

"a b c".split("\\s");              // { "a", "b", "c" }
"a b  c".split("\\s");             // { "a", "b", "", "c" }
"a, b ;; c".split("[\\,\\;\\s]+"); // { "a", "b", "c" }

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

4.2 搜索字符串

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

import java.util.regex.*;

public class Main {
    public static void main(String[] args) {
        String s = "the quick brown fox jumps over the lazy dog.";
        Pattern p = Pattern.compile("\\wo\\w");
        Matcher m = p.matcher(s);
        while (m.find()) {
            String sub = s.substring(m.start(), m.end());
            System.out.println(sub);
        }
    }
}

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

4.3 替换字符串

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

import java.util.regex.*;

public class Main {
    public static void main(String[] args) {
        String s = "The     quick\t\t brown   fox  jumps   over the  lazy dog.";
        String r = s.replaceAll("\\s+", " ");
        System.out.println(r); // "The quick brown fox jumps over the lazy dog."
    }
}

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

4.4 反向引用

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

import java.util.regex.*;

public class Main {
    public static void main(String[] args) {
        String s = "the quick brown fox jumps over the lazy dog.";
        String r = s.replaceAll("\\s([a-z]{4})\\s", " <b>$1</b> ");
        System.out.println(r);  // the quick brown fox jumps <b>over</b> the <b>lazy</b> dog.
    }
}

它实际上把任何4字符单词的前后用 <b>xxxx</b> 括起来。实现替换的关键就在于 " <b>$1</b> ",它用匹配的分组子串 ([a-z]{4}) 替换了 $1

[例] 模板引擎是指,定义一个字符串作为模板:

Hello, ${name}! You are learning ${lang}!

其中,以 ${key} 表示的是变量,也就是将要被替换的内容

当传入一个 Map<String, String> 给模板后,需要把对应的 key 替换为 Map 的 value。

例如,传入 Map 为:

{
    "name": "Bob",
    "lang": "Java"
}

然后,${name} 被替换为 Map 对应的值"Bob”,${lang} 被替换为 Map 对应的值 “Java”,最终输出的结果为:

Hello, Bob! You are learning Java!

请编写一个简单的模板引擎,利用正则表达式实现这个功能。

提示:参考 Matcher.appendReplacement()。appendReplacement 是把每个匹配到的部分和上一个匹配部分之间的一段切下来,用传入参数替换匹配部分,放入一个 StringBuffer 里头,

import java.util.Map;
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        String s = "Hello, ${name}! You are learning ${lang}!\n";
        Map<String, String> map = new HashMap<>();
        map.put("name", "Bob");
        map.put("lang", "Java");
        String r = s.replaceAll("\\$\\{name\\}", map.get("name")).replaceAll("\\$\\{lang\\}", map.get("lang"));
        System.out.println(r);
    }
}

4.5 小结

使用正则表达式可以:

  • 分割字符串:String.split()
  • 搜索子串:Matcher.find()
  • 替换字符串:String.replaceAll()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值