正则表达式regular expression ,是一种对字符串执行模式匹配的技术。

比如,我想要找出字符串中(或文章)所有四个数字连在一起的子串,这要怎么找呢?

可以自己遍历字符串,但是这样操作会不会很麻烦呢?或者说,我要找的东西非常复杂,这时使用正则表达式来匹配则是最佳的。

初体验

以我国党史为例,从中匹配出所有年份信息。

中华人民共和国历史始于公元1949年10月1日在北京举行的开国大典,先后制定并通过了《共同纲领》、《五四宪法》、《七五宪法》、《七八宪法》、《八二宪法》五部宪制性文件。截止2021年,中华人民共和国共经历了毛泽东、邓小平、江泽民、胡锦涛和习近平五位最高领导人,而华国锋曾担任过渡时期的领导人。截止2021年,中华人民共和国的历史以1978年12月中共十一届三中全为转折点,分为以社会主义转型及阶级斗争为主题的毛泽东时代(1949年至1976年)、与以现代化建设及改革开放为主题的邓小平时代和后邓小平时代(1978年起)。中国共产党是中华人民共和国建国以来唯一的执政党,并坚持对全国武装力量的绝对领导。

public class RegTheory {
    public static void main(String[] args) {
        String content = "中华人民共和国历史始于公元1949年10月1日在北京举行的开国大典,先后制定并通过了《共同纲领》、《五四宪法》、《七五宪法》、《七八宪法》、《八二宪法》五部宪制性文件。截止2021年,中华人民共和国共经历了毛泽东、邓小平、江泽民、胡锦涛和习近平五位最高领导人,而华国锋曾担任过渡时期的领导人。截止2021年,中华人民共和国的历史以1978年12月中共十一届三中全为转折点,分为以社会主义转型及阶级斗争为主题的毛泽东时代(1949年至1976年)、与以现代化建设及改革开放为主题的邓小平时代和后邓小平时代(1978年起)。中国共产党是中华人民共和国建国以来唯一的执政党,并坚持对全国武装力量的绝对领导。";
        // \\d表示一个任意的数字
        String regStr = "\\d\\d\\d\\d";
        // 创建正则表达式对象
        Pattern pattern = Pattern.compile(regStr);
        // 创建匹配器,按照上述正则表达式的规则去匹配字符串
        Matcher matcher = pattern.matcher(content);
        // 开始匹配
        while (matcher.find()) {
            System.out.println("找到了:"+matcher.group());
        }
    }
}

结果:

找到了:1949
找到了:2021
找到了:2021
找到了:1978
找到了:1949
找到了:1976
找到了:1978

简单分析

matcher.find()

  1. 根据指定的规则,定位满足规则的子字符串。按照上述字符串,则该定位1949。

  2. 找到后,将子字符串的开始索引记录到matcher对象的属性groups[0]中(13)

    /**
    * The storage used by groups. They may contain invalid values if
    * a group was skipped during the matching.
    */
    int[] groups;
    

    把该子字符串结束的索引+1值记录到groups[1]中(17)。

  3. 同时,记录oldLast属性,其值为该子字符串结束的索引+1的值(17)。下一次找的时候,就从这个位置开始匹配。

matcher.group()

点进源码,发现它与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();
}

这串代码可以拆开两部分看。

前面if是判断一些条件,就是找不到的情况。

后面getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString()这个是重点,它是截取了整个字符串中匹配项的子字符串。(groups[0], groups[1])是一次匹配结果嘛,现在我们找的都是group(0)。

语法

转义符 \\

需要用到默认符号的字符有:

(在写md的时候它就提示了……)

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

想匹配**.**,它在列表中需要被转义。

public static void main(String[] args) {
    String content = "abc*(a.bc(123)";
    String regexp = "\\.";
    Pattern pattern = Pattern.compile(regexp);
    Matcher matcher = pattern.matcher(content);
    while (matcher.find()) {
        System.out.println("找到了:" + matcher.group());
    }
}

字符匹配符

  • [ ]可接收的字符列表

    • [efgh]:表示e、f、g、h中任意一个符号
  • [^]不接收的字符列表

    • [^abc]:表示a、b、c以外的中任意一个符号
  • -连字符

    • A-Z:表示任意的A到Z的大写字母
  • . :除了**\n**以外的任何字符(即不包含换行符)

    • a..b:表示以a开头、以b结尾,中间包括任意两个字符的长度为4的字符串
    • 注:如果要匹配自身,则需要加\\转义
  • \\d:匹配单个数字

    • \\d{3}{\\d}?:表示包含3个或4个数字字符
    • {3}表示前面三个重复的,即等价于\\d\\d\\d
    • ?表示可以有也可以没有
  • \\D:匹配单个非数字

  • \\w:匹配单个数字、大小写字母字符

    • 等价于:[0-9a-zA-Z]
  • \\W:匹配非单个数字、大小写字母字符。(大写就是小写的取反)

  • \\s:匹配任何空白字符(空格、制表符等)

  • \\S:匹配任何非空白字符

选择匹配符

|:其实就是一个“或”嘛,多个匹配。

a|b,就是匹配a或者b。

限定符

限定其前面的字符和组合项出现的次数。

这些如果没有括号括起来,就与其最近的一个相匹配。

(感觉和编译原理很像啊……)

  • *:表示字符重复0次或重复n次。

    • (abc)*表示包含abc的任意个字符。可以匹配到abc abcabc
  • (?i):不区分大小写

    public static void main(String[] args) {
        String content = "abc*(ABC(123)";
        String regexp = "(?i)abc";
        Pattern pattern = Pattern.compile(regexp);
        Matcher matcher = pattern.matcher(content);
        while (matcher.find()) {
            System.out.println("找到了:" + matcher.group());
        }
    }
    

    这里就会匹配到abc、ABC。

    这个写在前面,等价于**((?i)abc)**。

    所以如果需要哪里不区分大小写,则需要加上括号。

    也可以在创建时指定不区分大小写:Pattern pattern = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE);

  • 重复前面的匹配符n次

    • \\d{3}等价于\\d\\d\\d
  • ?:匹配前面的0次或1次

    • {\\d}?表示匹配一个或者零个数字
  • {n, }:至少n个的匹配

  • {n, m}:至少n个至多m个的匹配

    • [abcd]{3, 5}:由a、b、c、d组成的任意长度不小于3不大于5的字符串
    • 贪婪,尽可能匹配多的,如上,则若有5个能够匹配上,那先匹配到5。(而且匹配完之后,相当于把它“删掉”了,后面匹配就不看它了)
    • 若不想贪婪,可以在后面再加一个**?**
  • +:表示出现1到任意多次(与*相似,但是*可以匹配0个)这里要求一定要有。

定位符

  • ^:指定起始字符
  • &:指定结束字符
    • ^[0-9]\\-[a-z]+$:表示数字开头、小写字母结尾的。
  • \\b:匹配目标字符串的边界
    • 就是往后找的意思?如下代码串
    • String content = "abcXXXabc fdabc";
    • String regexp = "abc\\b";
    • 那它就会匹配到XXX后面的abc,还有fd后面的abc,最开头的abc就不会匹配到。
  • \\B:就是与上面的相反。它会匹配到最开头的abc。

分组

对应回初体验的代码,修改正则式为:String regStr = "(\\d\\d)(\\d\\d)";

第一个括号对表示第一组、第二个括号表示第二组。

如上的1949,则它会分为两组来查找,(19)(49)。

这样的话

  • 最终匹配结果,仍然是groups[0]=13、groups[1]=17。但是!

  • 记录第一组:groups[2]=13、groups[3]=15

  • 记录第二组:groups[4]=15、groups[5]=17

(这些放置分组的工作好像是find()完成的,哎呀它的源码好精妙呀。)

上面的代码就是可以对一个匹配项的各个组里面的内容进行再处理。


命名分组

可以给分组取名字!

String regexp = "(?<g1>\\d\\d)(?<g2>\\d\\d)";

使用**?<name>**来确定一个分组的名字。

取数据的时候,它的group(name)是重载的,可以传分组名过去 。

非捕获分组

公共部分进行提取和简化。

(?:pattern):匹配pattern但不捕获匹配的子表达式(就是不匹配它,找到了也不理它的意思),算是一种简化?特别是对于**|**

比如,想要找industry或者industries,可以写成:industr(?:y|ies),这样就比industry|industries更简洁。

(它没有捕获分组噢,所以只用group()来)

(?=pattern):这个例子也和上面的相似

比如:Windows (?=95|98|2000),那我想要匹配到的就是Windows 95或Windows 98或Windows 2000前面的Windows。如果有一个Windows XP,它就不会被匹配到。

(?!pattern):取反嘛。与上面的意思相反,若上面不变,那Windows XP就会被匹配到。

反向引用

()分组引用后,这个括号的值可以在后面再引用,这就叫做反向引用。

它可以使用在:比如我要找4个数字,第一位和第四位相同,第二位和第三位相同,就可以分组,然后反向引用它。

当然,这个是内部的反向引用。


匹配两个连续相同数字:(\\d)\\1

匹配五个连续相同数字:(\\d)\\1{4}

匹配4个数字,第一位和第四位相同,第二位和第三位相同:(\\d)(\\d)\\2\\1

结巴去重

去掉无用的,可以使用**matcher.replaceAll("xxx")**来实现。

(.)\\1+,能找到重复的串。比如,“我我我我”就会被它匹配到,想办法把它干掉。

这里就要使用外部引用 :$n,然后使用matcher.replaceAll("$n")来替换找到的重复的串,就是用“我”来替换"我我我我"

(PS:外部引用,其实就是在正则表达式的外部,比如你在另一个matcher那里用了,那就是外部了)