首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >简单的正则表达式入门

简单的正则表达式入门

作者头像
王图思睿
发布2021-06-16 15:44:19
9400
发布2021-06-16 15:44:19
举报
文章被收录于专栏:膨胀的面包膨胀的面包

什么是正则表达式

正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为 regex、regexp 或 RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。

初窥门径

先来看看正则表达式的精确匹配。一个普通的字符串,比如 abc,它如果用来做正则表达式匹配的话,只能匹配自己。也就是说它只能匹配字符串 abc,不能匹配 ab,Abc 等其他任何字符串。

System.out.println("abc".matches("abc")); // 输出为 true
System.out.println("ab".matches("abc")); // 输出为 false
System.out.println("Abc".matches("abc")); // 输出为 false

不过这好像没什么用,需要精确匹配的话,我们可以用 String.equals() 函数,不需要用正则。

如果需要匹配的字符串含有特殊字符,那就需要用 转义。比如 a&b,在用正则表达式匹配时,需要使用 a&b,又由于在 Java 字符串中, 也是特殊字符,它也需要转义,所以 a&b 对应的 Java 字符串是 a\&b,它是用来匹配 a&b 的。

System.out.println("a&b".matches("a\\&b")); // 输出为 true

再比如 d 在正则表达式中表示匹配任意数字,d 是 digital 的简写。比如 00d 就可以匹配 000, 007,008 等等。

那么,00d 可以匹配 0066 吗?

答案是不能,d 只能匹配单个数字。

那要怎么才能匹配多个数字呢?

你可以写多次,比如 dd 就能匹配两个数字,ddd 能匹配三个数字,需要匹配几个数字就写几次就行了。

System.out.println("1".matches("\\d\\d")); // 输出为 false
System.out.println("11".matches("\\d\\d")); // 输出为 true
System.out.println("111".matches("\\d\\d")); // 输出为 false

但是如果要匹配 10000 个数字呢?总不能写一万次吧?

其实如果要匹配多个数字,只需要在 d 后面打上花括号 {},{n} 表示匹配 n 次。d{10000} 就表示匹配 10000 个数字。

趁热打铁,如果要匹配 n ~ m 次,用 {n,m} 即可,如果要匹配至少 n 次,用 {n,} 即可。需要注意 , 后不能有空格。

System.out.println("1".matches("\\d{1,2}")); // 输出为 true
System.out.println("12".matches("\\d{1,2}")); // 输出为 true
System.out.println("123".matches("\\d{1,2}")); // 输出为 false

System.out.println("123".matches("\\d{2,}")); // 输出为 true

那么按照这个写法,如果要匹配最多 m 次,是不是用 {,m} ?

No,最多 m 次需要这么写吗?直接用 {0,m} 不就行了吗?只是因为正无穷不好表示我们才用的 {n,},在正则里根本没有 {,m} 这样的写法。

小有所成

正则的基础规则中,除了 d,还有 w 和 s,w 是 word 的简写,表示匹配一个常用字符,包括字母、数字、下划线。s 是 space 的简写,表示匹配一个空格,包括三种:

  • 空格键打出来的空格
  • Tab 键打出来的空格
  • 回车键打出来的空格

Tab 键打出来的空格和回车键打出来的空格是指 t 和 n

System.out.println("LeetCode_666".matches("\\w{12}")); // 输出为 true
System.out.println("\t \n".matches("\\s{3}")); // 输出为 true
System.out.println("Leet\tCode 666".matches("\\w{4}\\s\\w{4}\\s\\d{3}")); // 输出为 true

更进一步

记住上面三个规则之后,你还可以顺带获得几个新的规则。因为正则规定:将字母换成大写,就表示相反的意思。用 d 你可以匹配一个数字,D 则表示匹配一个非数字。

System.out.println("a".matches("\\d")); // 输出为 false
System.out.println("1".matches("\\d")); // 输出为 true

System.out.println("a".matches("\\D")); // 输出为 true
System.out.println("1".matches("\\D")); // 输出为 false

这非常好记。类似地,W 可以匹配 w 不能匹配的字符,S 可以匹配 s 不能匹配的字符。

渐入佳境

有时候,我们对某些位置的字符没有要求,仅需要占个位置即可。这时候我们就可以用 . 字符。

System.out.println("a0b".matches("a.b")); // 输出为 true
System.out.println("a_b".matches("a.b")); // 输出为 true
System.out.println("a b".matches("a.b")); // 输出为 true

也可以理解为:. 可以匹配任意字符。

还记得之前说的 {n} 表示匹配 n 次吗?有时候,我们对匹配的次数没有要求,匹配任意次均可,这时,我们就可以用 * 字符。

System.out.println("1".matches("\\d*")); // 输出为 true
System.out.println("123".matches("\\d*")); // 输出为 true
System.out.println("".matches("\\d*")); // 输出为 true

为什么第三个表达式也会输出 true 呢?明明没有出现数字啊?

那意味着出现了 0 次, 是指 可以匹配任意次,包括 0 次。也就是说, 等价于 {0,}

实际比较常见的需求应该是某个字符至少出现一次,那就可以用 + 匹配,+ 表示 至少匹配一次。它等价于 {1,}

System.out.println("1".matches("\\d+")); // 输出为 true
System.out.println("123".matches("\\d+")); // 输出为 true
System.out.println("".matches("\\d+")); // 输出为 false

还有一种场景,如果某个字符要么匹配 0 次,要么匹配 1 次,我们就可以用 ? 匹配。它等价于 {0,1}

System.out.println("".matches("\\d?")); // 输出为 true
System.out.println("1".matches("\\d?")); // 输出为 true
System.out.println("123".matches("\\d?")); // 输出为 false

总结:

. 匹配任意字符;
* 匹配任意次,包括 0 次;
+ 号匹配至少 1 次;
? 匹配 0 次或 1 次;

心浮气躁

是不是感觉自己已经掌握了够多的匹配规则,足以应付所有的字符串匹配场景了?

可先别得意得太早,我再考考你吧。看看匹配电话号码的程序,如果我们规定电话号码不能以 0 开头,应该怎么写正则表达式呢?

这样的场景需要用 [] 来匹配,[] 用于匹配指定范围内的字符,比如[123456789] 可以匹配 1~9

那么上边问题的正则匹配规则是 [123456789]d{10}

但是,这里还有一个语法糖,[123456789] 写起来太麻烦,可以写作 [1-9]

当然这个语法也可以用于字母,比如 [a-g] 表示 [abcdefg],[U-Z] 表示 [UVWXYZ]

如果既可以是数字 1~9,又可以是字母 a~g,还可以是字母 U~Z,你还可以这么写:[1-9a-gU-Z]

System.out.println("1".matches("[1-9a-gU-Z]")); // 输出为 true
System.out.println("b".matches("[1-9a-gU-Z]")); // 输出为 true
System.out.println("X".matches("[1-9a-gU-Z]")); // 输出为 true
System.out.println("A".matches("[1-9a-gU-Z]")); // 输出为 false

如果是 0~1,8~9 可以这样组合吗?

那当然也是可以的,[0-18-9] 正是你想要的。由于正则一次只匹配一个字符,所以这样写并不会有歧义,也就是说计算机不会把这种写法误解成要匹配 0~18 之类的。

System.out.println("1".matches("[0-18-9]")); // 输出为 true
System.out.println("5".matches("[0-18-9]")); // 输出为 false

但是,这种需求,你写 [0189] 不是更简洁吗?

还有一种写法可以实现这一点,那就是用 或 运算符,正则的 或 运算符是 |,[0189] 也可以写作 0|1|8|9。

System.out.println("1".matches("0|1|8|9")); // 输出为 true
System.out.println("5".matches("0|1|8|9")); // 输出为 false

或 可以实现更多的功能,它并不局限于单个字符,比如:

System.out.println("abc".matches("abc|ABC")); // 输出为 true
System.out.println("ABC".matches("abc|ABC")); // 输出为 true
System.out.println("123".matches("abc|ABC")); // 输出为 false

如果我想排除某些字符呢?比如这个位置不能是 [123]。我记得你之前说正则以大写表示取反,[] 要怎么大写呢?

[] 可没有大写之说,[] 取反的方式是:[^],比如不能是 [123] 的表示方法为 1 或者 2

新手教程到这里就结束了,这已经足够你应付许多应用场景了。

探囊取物

下面就是进阶教程拉,考虑一个实际需求,有许许多多以下格式的字符串,你需要用正则表达式匹配出其姓名和年龄。

  • Name:Aurora Age:18
  • 其中还夹杂着一些无关紧要的数据
  • Name:Bob Age:20
  • 错误的数据有着各种各样错误的格式
  • Name:Cassin Age:22
  • ...

这应该已经难不倒你了。观察字符串的规则,只需要用 Name:\w+\s*Age:\d{1,3} 就能匹配了。

System.out.println("Name:Aurora   Age:18".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 true
System.out.println("其中还夹杂着一些无关紧要的数据".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 false
System.out.println("Name:Bob      Age:20".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 true
System.out.println("错误的数据有着各种各样错误的格式".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 false
System.out.println("Name:Cassin   Age:22".matches("Name:\\w+\\s*Age:\\d{1,3}")); // 输出为 true

一般来说,下一步你要做的就是取出这些表达式中的姓名和年龄,以便把它们存到数据库中。

你可以用 indexOf 和 subString 函数来取这些值。

但你已经掌握了正则的力量,正则里有更简洁的取值方式。

Pattern pattern = Pattern.compile("Name:(\\w+)\\s*Age:(\\d{1,3})");
Matcher matcher = pattern.matcher("Name:Aurora   Age:18");
if(matcher.matches()) {
    String group1 = matcher.group(1);
    String group2 = matcher.group(2);
    System.out.println(group1);   // 输出为 Aurora
    System.out.println(group2);   // 输出为 18
}

只要用 () 将需要取值的地方括起来,传给 Pattern 对象,再用 Pattern 对象匹配后获得的 Matcher 对象来取值就行了。每个匹配的值将会按照顺序保存在 Matcher 对象的 group 中。

上边语句用 () 把 \w+ 和 \d{1,3} 分别括起来了,判断 Pattern 对象与字符串是否匹配的方法是 Matcher.matches(),如果匹配成功,这个函数将返回 true,如果匹配失败,则返回 false。

这里是不是写错了,为什么 group 是从下标 1 开始取值的,计算机不都从 0 开始数吗?

并没有写错,这是因为 group(0) 被用来保存整个匹配的字符串了。

System.out.println(matcher.group(0));   // 输出为 Name:Aurora   Age:18

每次调用 String.matches 函数,都会新建出一个 Pattern 对象。所以如果要用同一个正则表达式多次匹配字符串的话,最佳的做法不是直接调用 String.matches 方法,而应该先用正则表达式新建一个 Pattern 对象,然后反复使用,以提高程序运行效率。

// 错误的做法,每次都会新建一个 Pattern,效率低
boolean result1 = "Name:Aurora   Age:18".matches("Name:(\\w+)\\s*Age:
(\\d{1,3})"); 
boolean result2 = "Name:Bob      Age:20".matches("Name:(\\w+)\\s*Age:
(\\d{1,3})");
boolean result3 = "Name:Cassin   Age:22".matches("Name:(\\w+)\\s*Age:
(\\d{1,3})");
                                                 
// 正确的做法,复用同一个 Pattern,效率高
Pattern pattern = Pattern.compile("Name:(\\w+)\\s*Age:(\\d{1,3})");
boolean result4 = pattern.matcher("Name:Aurora    Age:18").matches();
boolean result5 = pattern.matcher("Name:Bob      Age:20").matches();
boolean result6 = pattern.matcher("Name:Cassin   Age:22").matches();

移花接木

接下来再考虑一个实际场景:你有一个让用户输入标签的输入框,用户可以输入多个标签。可是你并没有提示用户,标签之前用什么间隔符号隔开。

用户的输入五花八门,有用逗号的,有用分号的,有用空格的,还有用制表符的......

  • 二分,回溯,递归,分治
  • 搜索;查找;旋转;遍历
  • 数论 图论 逻辑 概率

我们可以用正则表达式模糊匹配,只要能匹配成功,就以其分割。

System.out.println(Arrays.toString("二分,回溯,递归,分治".split("[,;\\s]
+")));
System.out.println(Arrays.toString("搜索;查找;旋转;遍历".split("[,;\\s]
+")));
System.out.println(Arrays.toString("数论 图论 逻辑 概率".split("[,;\\s]
+")));

输出为:

[二分, 回溯, 递归, 分治]
[搜索, 查找, 旋转, 遍历]
[数论, 图论, 逻辑, 概率]

我们还可以用正则表达式模糊匹配,将符合规则的字符串全部替换掉。比如就现在这个例子,我们可以把用户输入的所有数据统一规范为使用 ; 分隔,那我们就可以这样写。

System.out.println("二分,回溯,递归,分治".replaceAll("[,;\\s]+", ";"));
System.out.println("搜索;查找;旋转;遍历".replaceAll("[,;\\s]+", ";"));
System.out.println("数论 图论 逻辑 概率".replaceAll("[,;\\s]+", ";"));

输出为:

二分;回溯;递归;分治
搜索;查找;旋转;遍历
数论;图论;逻辑;概率

还不止这一点,在 replaceAll 的第二个参数中,我们可以通过 1,2,...来反向引用匹配到的子串。只要将需要引用的部分用 () 括起来就可以了。

System.out.println("二分,回溯,递归,分治".replaceAll("([,;\\s]+)", "---$1---"));
System.out.println("搜索;查找;旋转;遍历".replaceAll("([,;\\s]+)", "---$1---"));
System.out.println("数论 图论 逻辑 概率".replaceAll("([,;\\s]+)", "---$1---"));

输出为:

二分---,---回溯---,---递归---,---分治
搜索---;---查找---;---旋转---;---遍历
数论--- ---图论--- ---逻辑--- ---概率

有时候我们不需要替换,只需要将正则匹配出来的部分添加一些前缀或后缀,就可以用这种方式!

蓦然回首

看这样一道题:给你一些字符串,统计其末尾 e 的个数

  • LeetCode
  • LeetCodeeee
  • LeetCodeee

看起来并不难,用 (\w+)(e*) 匹配,再取 group(2) 判断即可。

Pattern pattern = Pattern.compile("(\\w+)(e*)");
Matcher matcher = pattern.matcher("LeetCode");
if (matcher.matches()) {
    String group1 = matcher.group(1);
    String group2 = matcher.group(2);
    System.out.println("group1 = " + group1 + ", length = " + group1.length());
    System.out.println("group2 = " + group2 + ", length = " + group2.length());
}

输出为:

group1 = LeetCode, length = 8
group2 = , length = 0

这是因为 e 仍然属于 w 能匹配的范畴,正则表达式默认会尽可能多地向后匹配,我们将其称之为 贪婪匹配。

贪婪匹配和贪心算法原理是一致的。与之对应的匹配方式叫做 非贪婪匹配,非贪婪匹配 会在能匹配目标字符串的前提下,尽可能少的向后匹配。

那么,要怎样指定匹配方式为非贪婪匹配呢?

也很简单,在需要非贪婪匹配的正则表达式后面加个 ? 即可表示非贪婪匹配。

Pattern pattern = Pattern.compile("(\\w+?)(e*)");
Matcher matcher = pattern.matcher("LeetCode");
if (matcher.matches()) {
    String group1 = matcher.group(1);
    String group2 = matcher.group(2);
    System.out.println("group1 = " + group1 + ", length = " + group1.length());
    System.out.println("group2 = " + group2 + ", length = " + group2.length());
}

运行程序,输出如下:

group1 = LeetCod, length = 7
group2 = e, length = 1

这里也用的是 ?,我记得之前 ? 表示的是匹配 0 次或者 1 次,两个符号不会混淆吗?

不会混淆的,你仔细想一想就能明白了,如果只有一个字符,那就不存在贪婪不贪婪的问题,如果匹配多次,那么表示非贪婪匹配的 ? 前面必有一个标志匹配次数的符号。所以不会出现混淆。

为什么这里没有匹配成 group1 等于 L,group2 等于 ee?

如果这样匹配的话,字符串 LeetCode 就无法和正则表达式匹配起来。怪不得非贪婪匹配的定义是 在能匹配目标字符串的前提下,尽可能少的向后匹配。

via: https://mp.weixin.qq.com/s/zxQ-itkICaZlhevvwuBGsw

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是正则表达式
    • 初窥门径
      • 小有所成
        • 更进一步
          • 渐入佳境
            • 心浮气躁
              • 探囊取物
                • 移花接木
                  • 蓦然回首
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档