正则表达式看这篇就够了!

130 阅读7分钟

正则表达式基础与进阶知识点

一、基础知识点

(一)元字符

image.png 元字符是构造正则表达式的基本元素。例如,可以用以下方式匹配特定字符串:

  • \babc^abc匹配有abc开头的字符串。
String str = "abcdef";
System.out.println(str.matches("\babc"));
System.out.println(str.matches("^abc"));
  • ^\d\d\d\d\d\d\d\d$匹配 8 位数字的 QQ 号码。
String qqNumber = "12345678";
System.out.println(qqNumber.matches("^\d\d\d\d\d\d\d\d$"));
  • ^1\d\d\d\d\d\d\d\d\d\d$匹配 1 开头 11 位数字的手机号码。
String phoneNumber = "12345678910";
System.out.println(phoneNumber.matches("^1\d\d\d\d\d\d\d\d\d\d$"));

(二)重复限定符

image.png 为处理重复问题,正则表达式中有一些重复限定符。如:

  • ^\d{8}$匹配 8 位数字的 QQ 号码。
String qqNumber = "12345678";
System.out.println(qqNumber.matches("^\d{8}$"));
  • ^1\d{10}$匹配 1 开头 11 位数字的手机号码。
String phoneNumber = "12345678910";
System.out.println(phoneNumber.matches("^1\d{10}$"));
  • ^\d{14,18}$匹配银行卡号是 14 - 18 位的数字。
  • ^ab*$匹配以a开头的,0 个或多个b结尾的字符串。
String str1 = "a";
String str2 = "ab";
String str3 = "abbb";
System.out.println(str1.matches("^ab*$"));
System.out.println(str2.matches("^ab*$"));
System.out.println(str3.matches("^ab*$"));

image.png

(三)分组

*限定符作用于其左边最近的一个字符,若要让ab同时被限定,可以用小括号()做分组,使括号中的内容作为一个整体。例如,用^(ab)*匹配字符串中包含 0 到多个ab开头的内容。

String str = "ababab";
System.out.println(str.matches("^(ab)*"));

(四)转义

如果要匹配的字符串中本身就包含小括号,会产生冲突。正则提供了转义的方式,在要转义的字符前面加个斜杠``,将其转义成普通的字符。例如,用^((ab))*匹配以(ab)开头的字符串。

String str = "(ab)(ab)";
System.out.println(str.matches("^\(ab\)*"));

(五)条件或

正则用符号|表示或,也叫分支条件。例如,用^(130|131|132|155|156|185|186|145|176)\d{8}$匹配联通手机号码。

String phoneNumber = "13012345678";
System.out.println(phoneNumber.matches("^(130|131|132|155|156|185|186|145|176)\d{8}$"));

(六)区间

正则提供元字符中括号[]表示区间条件。例如,限定 0 到 9 可以写成[0 - 9],限定 A - Z 写成[A - Z],限定某些数字[165]。可以将联通手机号码的正则表达式写成^((13[0 - 2])|(15[56])|(18[5 - 6])|145|176)\d{8}$

String phoneNumber = "13112345678";
System.out.println(phoneNumber.matches("^((13[0 - 2])|(15[56])|(18[5 - 6])|145|176)\d{8}$"));

二、进阶知识点

(一)零宽断言

一、零宽断言的概念
  1. 断言:在正则中,断言可以指明在指定的内容的前面或后面会出现满足指定规则的内容,如同人类的 “断定”。例如在字符串 "ss1aa2bb3" 中,正则可以用断言找出aa2前面有bb3,或者后面有ss1
  2. 零宽:在正则中,断言只是匹配位置,不占字符,也就是说,匹配结果里不会返回断言本身。
二、零宽断言的作用及示例
(一)正向先行断言(正前瞻)
  1. 语法(?=pattern),作用是匹配pattern表达式的前面内容,不返回本身。

  2. 示例

    • 假设要用爬虫抓取文章阅读量,文章阅读量的结构为<span class="read-count">阅读数:641</span>,如果要获取其中的数字内容,可以使用正向先行断言。
    • 首先,匹配所有内容的正则表达式为String reg=".+(?=</span>)";,但这会匹配到整个<span class="read-count">阅读数:641</span>
    • 然后,只匹配数字内容的正则表达式为String reg="\d+(?=</span>)";,这样就能准确地获取到数字641
String test = "<span class="read-count">阅读数:641</span>";
String reg = "\d+(?=</span>)";
Pattern pattern = Pattern.compile(reg);
Matcher mc = pattern.matcher(test);
while (mc.find()) {
    System.out.println(mc.group());
}
(二)正向后行断言(正后顾)
  1. 语法(?<=pattern),作用是匹配pattern表达式的后面的内容,不返回本身。

  2. 示例:对于同样的文章阅读量的例子,也可以用正向后行断言来处理。

    • 正则表达式为String reg="(?<=<span class="read-count">阅读数:)\d+";
String test = "<span class="read-count">阅读数:641</span>";
String reg = "(?<=<span class="read-count">阅读数:)\d+";
Pattern pattern = Pattern.compile(reg);
Matcher mc = pattern.matcher(test);
while (mc.find()) {
    System.out.println(mc.group());
}
(三)负向先行断言(负前瞻)
  1. 语法(?!pattern),作用是匹配非pattern表达式的前面内容,不返回本身。
  2. 示例:比如有一句 “我爱祖国,我是祖国的花朵”,现在要找到不是 ' 的花朵 ' 前面的祖国,可以用正则表达式祖国(?!的花朵)
(四)负向后行断言(负后顾)
  1. 语法(?<!pattern),作用是匹配非pattern表达式的后面内容,不返回本身。
  2. 示例(?<!我是)祖国
(五)强密码校验示例
  1. 正则表达式^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[@#!$%^&*()]).{8,16}$

  2. 解释

    • {8,16}表示首先需要一个 8 - 16 位的字符串。
    • .*[A-Z]表示其次需要做限定,先限定大写字母,即指定任意字符串加一个大写字母。
    • ^(?=.*[A-Z])表示断言这个字符串以开始符开始,且其中包含大写字母。
    • ^(?=.*[A-Z]).{8,16}$在断言的基础上,加上字符串限制(断言本身不占用字符)。
    • ^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[@#!$%^&*()]).{8,16}$增加小写字母及数字符号断言。
    • ^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[@#!$%^&*()])[\x21-\x7E]{8,16}$将任意字符改为限定 ascii 码表 33 到 126 位的字符。

(二)捕获和非捕获

  1. 捕获组

    • 定义:匹配子表达式的内容,把匹配结果保存到内存中数字编号或显示命名的组里,以深度优先进行编号,之后可以通过序号或名称来使用这些匹配结果。
    • 数字编号捕获组:语法为(exp),从表达式左侧开始,每出现一个左括号和它对应的右括号之间的内容为一个分组,第 0 组为整个表达式,第一组开始为分组。例如,固定电话020 - 85653333的正则表达式为(0\d{2}) - (\d{8})
String test = "020 - 85653333";
String reg = "(0\d{2})-(\d{8})";
Pattern pattern = Pattern.compile(reg);
Matcher mc = pattern.matcher(test);
if (mc.find()) {
    System.out.println("分组的个数有:" + mc.groupCount());
    for (int i = 0; i <= mc.groupCount(); i++) {
        System.out.println("第" + i + "个分组为:" + mc.group(i));
    }
}
  • 命名编号捕获组:语法为(?<name>exp),分组的命名由表达式中的name指定。例如,区号可写成(?<quhao>\0\d{2}) - (?<haoma>\d{8})
String test = "020 - 85653333";
String reg = "(?<quhao>0\d{2})-(?<haoma>\d{8})";
Pattern pattern = Pattern.compile(reg);
Matcher mc = pattern.matcher(test);
if (mc.find()) {
    System.out.println("分组的个数有:" + mc.groupCount());
    System.out.println(mc.group("quhao"));
    System.out.println(mc.group("haoma"));
}
  1. 非捕获组:语法为(?:exp),用来标识那些不需要捕获的分组。
String test = "020 - 85653333 - 123";
String reg = "(?:0\d{2})-(\d{8})-(\d*)";
Pattern pattern = Pattern.compile(reg);
Matcher mc = pattern.matcher(test);
if (mc.find()) {
    System.out.println("分组的个数有:" + mc.groupCount());
    for (int i = 0; i <= mc.groupCount(); i++) {
        System.out.println("第" + i + "个分组为:" + mc.group(i));
    }
}

(三)反向引用

反向引用是一种在正则表达式中非常有用的技术,它允许我们引用之前捕获的内容。

捕获会返回一个捕获组,这个分组保存在内存中,不仅可以在正则表达式外部通过程序进行引用,也可以在正则表达式内部进行引用,这种引用方式就是反向引用。

根据捕获组的命名规则,反向引用可分为:

  • 数字编号组反向引用:\k\number

  • 命名编号组反向引用:\k或者'name'

捕获组通常是和反向引用一起使用的。

例如,要查找一串字母 "aabbbbgbddesddfiid" 里成对的字母,可以按照以下思路:

  1. 匹配到一个字母。

  2. 匹配下一个字母,检查是否和上一个字母是否一样。

  3. 如果一样,则匹配成功,否则失败。

在这个过程中,当匹配下一个字母时,需要用到上一个字母。这时可以利用捕获把上一个匹配成功的内容用来作为本次匹配的条件。

首先匹配一个字母:\w,我们需要做成分组才能捕获,因此写成(\w),这样这个表达式就有一个捕获组:(\w)

然后我们要用这个捕获组作为条件,那就可以写成(\w)\1。这里的\1是什么意思呢?还记得捕获组有两种命名方式吗,一种是根据捕获分组顺序命名,一种是自定义命名来作为捕获组的命名。在默认情况下都是以数字来命名,而且数字命名的顺序是从 1 开始的。因此要引用第一个捕获组,根据反向引用的数字命名规则就需要\1

以下是代码示例:

String test = "aabbbbgbddesddfiid";
Pattern pattern = Pattern.compile("(\w)\1");
Matcher mc = pattern.matcher(test);
while (mc.find()) {
    System.out.println(mc.group());
}
aa
bb
bb
dd
dd
ii

(四)正则回溯

  1. 回溯算法:回溯法是一种通用的计算机算法,用于查找某些计算问题的所有(或某些)解决方案,逐步构建候选解决方案,在确定候选不可能时放弃候选完成有效的解决方案。在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。

  2. 正则回溯:正则引擎主要有 DFA 和 NFA 两类。NFA 速度较慢且实现复杂,但功能强大,如支持反向引用。像 JavaScript、Java、PHP、Python、C# 等语言的正则引擎都是 NFA 型,NFA 正则引擎的实现过程中使用了回溯。

  3. 示例

    • 没有回溯的正则:正则表达式ab{1,3}c去匹配文本 'abbc',正则引擎先匹配a,尽可能多地匹配b,再匹配c,完成匹配。
    • 有正则回溯的正则:正则表达式ab{1,3}bc去匹配文本 'abbc',正则引擎先匹配a,尽可能多地匹配b,去匹配b时发现没b了,赶紧回溯,少匹配个b,再去匹配bc,完成匹配。

(五)贪婪和非贪婪

  1. 贪婪

    • 定义:当正则表达式中包含能接受重复的限定符时,通常匹配尽可能多的字符,这叫贪婪匹配。
    • 特性:一次性读入整个字符串进行匹配,每当不匹配就舍弃最右边一个字符,继续匹配,直到匹配成功或者把整个字符串舍弃完为止。多个贪婪量词在一起时,如果字符串能满足各自最大程度的匹配就互不干扰,否则从左到右优先最大数量的满足,剩余再分配下一个量词匹配。
    • 示例:用\d{3,6}匹配 3 到 6 位数字,若字符串中有 6 个数字,会全部匹配到。用(\d{1,2})(\d{3,4})匹配61762828 176 2991 87321时,“617628” 是前面的\d{1,2}匹配出了 61,后面的匹配出了 7628;"2991" 是前面的\d{1,2}匹配出了 2,后面的匹配出了 991;"87321" 是前面的\d{1,2}匹配出了 87,后面的匹配出了 321。
String test = "61762828 176 2991 87321";
String reg = "(\d{1,2})(\d{3,4})";
Pattern pattern = Pattern.compile(reg);
Matcher mc = pattern.matcher(test);
while (mc.find()) {
    System.out.println("匹配结果:" + mc.group(0));
}
  1. 懒惰(非贪婪)

    • 定义:当正则表达式中包含能接受重复的限定符时,通常匹配尽可能少的字符,这叫懒惰匹配。
    • 特性:从左到右,从字符串的最左边开始匹配,每次试图不读入字符匹配,匹配成功则完成匹配,否则读入一个字符再匹配,直到匹配成功或者把字符串的字符匹配完为止。懒惰量词是在贪婪量词后面加个 “?”。
    • 示例:用(\d{1,2}?)(\d{3,4})匹配61762828 176 2991 87321时,“61762” 是左边的懒惰匹配出 6,右边的贪婪匹配出 1762;"2991" 是左边的懒惰匹配出 2,右边的贪婪匹配出 991;"87321" 左边的懒惰匹配出 8,右边的贪婪匹配出 7321。
String test = "61762828 176 2991 87321";
String reg = "(\d{1,2}?)(\d{3,4})";
Pattern pattern = Pattern.compile(reg);
Matcher mc = pattern.matcher(test);
while (mc.find()) {
    System.out.println("匹配结果:" + mc.group(0));
}

(六)反义

如果不想匹配某些字符,正则提供了一些常用的反义元字符。

image.png