Java正则表达式

1,496 阅读7分钟

摘要

正则表达式可以说是一种非常强大的文本,引用维基百科 的解释:正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。无论是前端还是后端,或者说是日常办公,都会或多或少的用到正则表达式。尤其当我们碰到字符串匹配的时候,或者文本替换的时候,正则表达式都可以非常方便的解决问题。

1.Java的转义字符

记得以前学c语言的时候,要想实现在输出语句的末尾换行的话,需要在最后面加上\n,那时候的大概理解就是在某些特定的字母前面加上\,那么这个字符就会有特别的含义。

1.1 为什么需要转义字符

由于某些字符,比如\n,\r,\b,\t等,无法用某一个键来代表它,所以我们就需要使用\+字符的形式代表他们。注意:在windows系统中\r\n表示一个换行符,在linux系统中用\n表示一个换行符。

1.2 正则表达式的规则

其实真正要学的,也就是这些基本的规则了。

常用元字符
字符
含义
字符
含义
^ 开始的位置 . 单个任意字符(不一定包括换行符)
$ 结束的位置 \w 单个字符(字母/数字/下划线/汉字)
\s 单个空白字符(\n\t\r) \d 单个数字字符
\b 单词的开始或结束
重复
字符
含义
字符
含义
* 0次或多次 + 1次或多次
0次或1次 {n} n次
{n,} >=n次 {n,m} n到m次
选择
字符
含义
字符
含义
[aeiou] 单个的a/e/i/o/u字符之一 [0-9] 单个数字字符
[A-Z] 单个大写字符 [A-Z0-9_] 大写字母或数字或者下划线
Hi|hi Hi或hi
反义
字符
含义
字符
含义
[^aeiou] 单个的除a/e/i/o/u字符之外的字符 [^x] 单个的非x字符
\W 单个的非\w(字母/数字/下划线/汉字) \S 单个的非\s(空白)
\D 单个的非\d(数字)字符 \B 非开头/结束位置

注意:

  • 1.当需要用到字符原本的含义的时候,都需要使用转义,比如\^\[\]
  • 2.注意,上面列举的各种规范在绝大多数情况下都是能正常工作的,但是由于正则表达式引擎的不同(比如JS、Pythong、Java),所以在细枝末节上,不同的引擎会有不同的实现。

1.3 看一些例子加深印象

  • 匹配5到12位的数字:^\d{5,12}$
  • 匹配0或者1-9开头的数字:^0|[1-9][0-9]*$
  • 匹配小数:^(-?)(\d\.\d+)?$
  • 匹配国内的电话号码:^\d{3}-\d{8}|\d{4}-\d{7}$
  • 匹配YYYY-MM-DD格式:^\d{4}-\d{1,2}-\d{1,2}$
  • 匹配Windows中匹配空白行:\n\s*\r

1.4 Java中的正则表达式

在String中的方法中,同样存在许多正则表达式的例子,比如split()、replaceAll()、replaceFirst()、以及matches()方法,这些方法本质上都是正则表达式。但是注意,在代码中尽量少的使用正则表达式,原因有两点:
1.正则表达式需要解析,解析的过程会调用Pattern.compile方法:

public static Pattern compile(String regex) {
        return new Pattern(regex, 0);
    }

这个方法返回一个Pattern实例,如果点进去看Pattern类的话可以看到,整个类中有5000多行代码,说明这个类的实例占用的内存还是挺大的。比较推荐的方法就是定义一个全局的正则表达式,这样就可以只编译一次。
2.正则表达式匹配字符串是非常低效的过程,他的本质就是通过回溯(类似走迷宫),将表达式逐个与字符串比较,知道找到匹配的字符串,所以容易发生调用栈的溢出。

1.5 贪婪与非贪婪

引用贪婪与非贪婪模式影响的是被量词修饰的子表达式的匹配行为,贪婪模式在整个表达式匹配成功的前提下,尽可能多的匹配,而非贪婪模式在整个表达式匹配成功的前提下,尽可能少的匹配。举例说明:
有这样一个特殊的字符串:String s = "a123a123a123a";
如果用贪婪匹配:

        String regex = "a\\w*a";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(s);
        if(matcher.find()){
            System.out.println(matcher.group());
        }

结果:

a123a123a123a

Process finished with exit code 0

也就是说当它匹配到第一个符合要求的字符串片段的时候,他不会停止,而是会往后面继续寻找,知道不符合要求时停止。
采用非贪婪模式匹配,将正则表达式写成: String regex = "a\\w*?a"; 结果:

a123a

Process finished with exit code 0

很明显,当它匹配到第一个符合要求的字符串片段时,就会立即返回。
注意:
\quad返回匹配到的结果之前,必须加上条件判断语句 if(matcher.find()),否则无论匹配结果是什么都会报错。

1.6 指定正则表达式的模式

  • (?i)——忽略大小写
  • (?m)——指定多行模式
  • (?s)——指定.可以匹配包括换行符在内的所有符号

2.正则表达式的分组与捕获

在前面所列举的所有例子中,都是在判断字符串是否匹配正则表达式的书写格式。但是当我们们想要拿到符合正则表达式规则的那部分字符串的时候,这就需要用到正则表达式的分组了。
前面一直没有讲到括号的用法,其实括号除了表示优先级之外,最重要的作用就是分组了,以这个例子举例:

 public static void main(String[] args) {
        String s = "a123-nras-de12-97de";
        String regex = "(a\\w+?)(-\\w+)";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(s);

        if(matcher.find()){
            System.out.println(matcher.group(0));
        }

    }

结果:

  • gropu(0)的结果:a123-nras
  • group(1)的结果:a123
  • gropu(2)的结果:-nras

从结果可以看出,分组的作用就是拿到匹配后的字符串在正则表达式中,对应的组的部分字符串。0代表整个匹配的字符串,1代表第一组括号中的部分字符串......
注意:

  • 括号的编号是只看左括号的。
  • 如果想忽略某组括号,只需要在括号中加入?:,举例:
public static void main(String[] args) {
        String s = "a123-nras-de12-97de";
        String regex = "(a\\w+?)(?:-\\w+)";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(s);

        if(matcher.find()){
            System.out.println(matcher.group(0));
            System.out.println(matcher.group(1));
            System.out.println(matcher.group(2));
        }

    }

上面的代码会报越界错误:

a123-nras
a123
Exception in thread "main" java.lang.IndexOutOfBoundsException: No group 2
	at java.util.regex.Matcher.group(Matcher.java:538)
	at hello.regix.RegexText.main(RegexText.java:16)

3.正则表达式实战

3.1 获取GC日志中的分区大小信息

一个记事本文件gcLog.txt中存储着JVM的GC信息(在运行参数的VM options中写入-XX:+PrintGCDetails即可获得):

Heap
 PSYoungGen      total 18432K, used 5519K [0x00000000eb580000, 0x00000000eca00000, 0x0000000100000000)
  eden space 15872K, 34% used [0x00000000eb580000,0x00000000ebae3ee8,0x00000000ec500000)
  from space 2560K, 0% used [0x00000000ec780000,0x00000000ec780000,0x00000000eca00000)
  to   space 2560K, 0% used [0x00000000ec500000,0x00000000ec500000,0x00000000ec780000)
 ParOldGen       total 42496K, used 0K [0x00000000c2000000, 0x00000000c4980000, 0x00000000eb580000)
  object space 42496K, 0% used [0x00000000c2000000,0x00000000c2000000,0x00000000c4980000)
 Metaspace       used 4684K, capacity 4754K, committed 4992K, reserved 1056768K
  class space    used 525K, capacity 563K, committed 640K, reserved 1048576K

现在我们想拿出JVM各部分的大小信息,代码如下:

public static void main(String[] args) throws IOException {
        Pattern pattern = Pattern.compile("\\s+(\\w+)\\s+(\\w*?)\\s*(\\w+)\\s+(\\d+K)");
        List<String> list = Files.readAllLines(new File("gcLog").toPath());
        list.stream().forEach(s -> {
            Matcher matcher = pattern.matcher(s);
            if (matcher.find()) {
                System.out.println(matcher.group(1) + matcher.group(2) + "_"
                        + matcher.group(3) + ":" + matcher.group(4));
            }
        });
    }

结果如下:

PSYoungGen_total:18432K
eden_space:15872K
from_space:2560K
to_space:2560K
ParOldGen_total:42496K
object_space:42496K
Metaspace_used:4684K
classspace_used:525K

代码地址:github.com/Scott-YuYan…

3.2 删除日志中的时间戳

String str =
                "[2019-08-01 21:24:41] bt3102 (11m:21s)\n"
                        + "[2019-08-01 21:24:42] TeamCity server version is 2019.1.1 (build 66192)\n"
                        + "[2019-08-01 21:24:43] Collecting changes in 2 VCS roots (22s)\n";

需要将[XXX]替换成"",这里可以使用replaceAll方法,但是由于字符串是多行的,所以需要在正则表达式中指定为多行模式:

String regex = "(?m)^\\[.*?]";
        return str.replaceAll(regex, "");

这里另外有两个训练正则表达式的题目,有兴趣的可以自己去尝试下,代码太多我就不贴出来了。github.com/hcsp/regula…

4.参考资料