阅读 1146

正则表达式 - 从 0 到 1(前篇)

这是一篇针对正则表达式的完整介绍,将会分为两个部分,第一部分包含日常使用正则会高频率用到的正则语法,第二部分包含一些进阶语法。

很多软件都提供了文本的查找功能,通常使用 Ctrl+F/Command+F 快捷键来使用,但是一般这些软件提供的文本查找功能都非常基础,只能进行全字符匹配,比如输入“Hello”就只能匹配到“Hello”,而要按照一些特定规则查找的话,就无法实现了。正则表达式是一个用来查找匹配特定文本的工具,在软件开发过程中会经常使用到,它可以按照特定的规则进行文本的查找匹配。

正则通常使用一对斜杠 / 来标记,比如 /^Hello World$/,但是并不是所有地方都是使用 / 来标记的,只不过对于绝大多数编程语言来说是这样的,或者说,前后两个 / 并不属于正则,只有中间的部分才是。本文也将采取一般的形式,使用 / 来标记正则表达式。

目录

  • 一、全字匹配
  • 二、开头与结尾
  • 三、或者?
  • 四、任意字符匹配
  • 五、字符类
  • 六、自定义字符类
  • 七、重复
  • 八、分组
  • 九、分组引用

一、全字匹配

正则作为一个“高级”文本查找工具,对于一般软件的基本全字匹配功能肯定是涵盖在内的。要实现普通的全字匹配,只需要和普通的查找一样,直接写出要匹配的字符串即可,但是由于正则表达式有一些特殊的语法,因此有些符号是有特殊用处的,包括:\^$|.+*{?()[ 等,这些特殊符号的含义将在后面讲到,对于这些特殊符号,如果需要用到的话,需要在前面添加 \ 进行转义。

示例:

/Hello World/ 匹配 Hello World

二、开头与结尾

全字匹配的时候,有时我们要限制匹配结果所在的位置,比如,当你要在一堆手机号中查找 130 号段的号码,此时如果使用全字匹配,你可能会匹配到 18712341301 这样的结果,因为其中包含了 130 这个子串,但是我们需要的是 130 开头的号码,所以全字匹配不能满足需求了。此时,只要在全字匹配的正则中添加一个特殊符号 ^,这个特殊符号仅仅作为一个位置标记符,表示整个要匹配的目标字符串的开头,要注意这一点,它是一个表示开始位置的占位符。

示例:

/^130/ 仅匹配处于字符串开头位置的 130,比如 13012345678,而类似于 11302345678 这种虽然包含 130,但是不是在最开头包含的,则会匹配失败。

^ 匹配开始位置对应的是 $,表示要匹配的目标字符串的结尾,它也仅仅是一个表示位置的占位符。

示例:

/ed$/ 仅匹配处于字符串结尾的 ed,比如 openedclosed,而类似于 edgebedroom 这些虽然包含 ed,但是不在结尾的,则会匹配失败。

/^Hello World$/ 仅匹配既处于开头又处于结尾的 Hello World,因此只有一种情况是满足的:也就是整个匹配的目标字符串就只有 Hello World

三、或者?

有时,我们需要的匹配的内容并不直接确定,但是要求一定属于某个集合内。比如要匹配性别:,我们就不能直接使用全字匹配了。此时,我们可以使用一个特殊符号 |,表示“或者”。这样我们就可以将所有需要匹配的元素集合依次列出来,使用 | 分隔即可。要注意的是,匹配结果一定是二选一。

示例:

/男|女/ 可以匹配到一个字符串中包含的 或者 ,比如:性别:男,而如果目标字符串中即不包含 也不包含 ,则匹配失败。

/^男|女$/ 仅当目标字符串只有一个字“男”或者只有一个字“女”的时候可以匹配成功。而类似于 男女 这样两个字,虽然既包含了 也包含了 ,其中 处于开头位置, 处于结尾位置,但是并不存在“既处于开头又处于结尾的” ,所以是匹配失败的。

/one|two|three|four|five/ 匹配 1~5 的英文数。由此可见,你可以放任意多个需要匹配的元素列表,匹配结果将是列表中的任意一个。

四、任意字符匹配

有了 |,我们可以把想要匹配的子串全部列举出来,但有时,你会发现这是个大工程:比如,我们想要匹配手机号码,在中国,手机号码长度通常都是 11 位,并且开头三位表示号段,有 130131 等(并且还在增加),后面跟着 8 位数字。现在想要匹配这样的数字串,使用上面的方法就不太够了,你也许需要把所有可能的手机号全部列出来?

不过正则里有一个万能字符,使用特殊符号 . 表示(就是小数点),这个符号可以表示任意单个字符。

示例:

/^130........$/ 可以匹配 130 开头,后面跟着 8 位任意字符的字符串,比如 13012345678130abcdefgh

五、字符类

实际上,上面这个匹配手机号的例子有点粗糙,它虽然可以匹配到所有 130 开头的手机号,但也可以匹配到后面跟的不是数字的字符串,比如 130abcdefgh,此时,我们如果想要完完整整的匹配手机号,也就是后面 8 位要求一定是数字,这样的话就不能用 . 了,因为它表示任意字符,而我们只要 0~9 这十个字符。

正则表达式中存在一些预定义的“字符类”,比如这里我们只要匹配 0~9 这十个字符,在正则中可以使用 \d 表示,它与 . 类似,表示一个字符,不一样的是它仅仅表示 0~9 中的任意一个字符,有点类似于 /0|1|2|3|4|5|6|7|8|9/

示例:

/^130\d\d\d\d\d\d\d\d$/ 可以匹配 130 开头,后面跟着 8 位任意数字的字符串,比如 1301234567813087654321,而上个示例中的 130abcdefgh 将不再匹配,因为后面 8 位不是数字。

除了 \d,正则中还有两个比较常用的字符类:\w\s。其中 \w 在大部分正则引擎中表示 26 个大写英文字母 + 26 个小写英文字母 + 0~9 10 个数字 + 下划线字符 _,一共 63 个字符中的任意一个;而 \s 一般表示“空白字符”,比如 空格、 \t 制表符、\n 换行符、\f 换页符等。

但值得注意的是,上面这三个字符类 \d\w\s 具体包含哪些字符是根据不同的正则引擎实现而不同的!但是大体上来说,\d 就是数字,\w 是单词符号、\s 是空白符号。

在一些特定的正则引擎实现中,还会包含一些其他的字符类,具体要根据对应的正则引擎而决定。

有了 \d\w\s 三个字符类,正则还提供了他们的补集,使用他们的大写来表示,比如 \D 表示所有不是数字的字符、\W 表示所有不是单词符号的字符、\S 表示所有不是空白字符的字符。

六、自定义字符类

虽然说正则中提供了三个预定义的字符类,以及他们的补集,有的时候还是不太够用。比如,我们要匹配十六进制数,十六进制数除了包含 0~9 十个数字字符外,还包含 a~f (不区分大小写)六个(或者说十二个)英文字母,这样一来简单的 \d 就不够了,\w 又包含多余的字符会导致匹配的结果不准确。

正则提供了自定义字符类的方法,只要将所需的字符用中括号 [] 括起来,即可形成一个自定义的字符类。

示例:

/^[0123456789abcdefABCDEF]$/ 可以匹配一位十六进制数。

像上面这样把需要的字符列出来就可以形成一个自定义字符类,但是当字符数比较多的时候,这样全列出来就感觉有点麻烦了。在自定义字符类中,可以使用 - 符号来定义区间,可以直接指定从一个起始字符到一个结束字符。所以上面的例子可以改成这样:

示例:

/^[0-9a-fA-F]$/ 可以匹配一位十六进制数。

这样是不是就好多了呢?当然也可以指定更精细的区间,比如 /[0-37-9]/ 表示 0、1、2、3、7、8、9 组成的字符类。

与正则预置的字符类一样,自定义字符类同样支持取补集,只要在自定义字符类的第一个位置放置一个 ^ 符号(注意,这里的 ^ 不再是字符串开头的含义了),这样就表示不包含后面列出的字符组成的字符类。

示例:

/^[^a-z]$/ 匹配不是小写英文字母的单个字符,比如 数字 0、大写字母 A、特殊符号 @ 等。

在自定义字符类中,保留的特殊符号与第一节中说的那些不太一样了,只有 ]-^\ 有特殊用途,作为特殊符号,需要转义,而其他诸如 $| 之类的其他符号在自定义字符类中都不再需要转义(当然,你想继续转义也是可以的,但是会降低正则的可读性)。

当然,这些特殊符号也可以在他们没有实际用途的时候,不进行转义。这句话有点绕,具体来说,比如 -,在自定义字符类中表示区间范围,比如 /[a-c]/ 表示 abc 组成的字符类,但是如果你将它放在字符类的开头或者结尾,它将无法构成范围,比如 /[-a]/ 或者 /[a-]/ 就表示 a- 组成的字符类。再比如 ^ 放在自定义字符类的开头表示补集,但是放在其他位置就不再有特殊意义,就不需要转义了。而 \ 作为转义符,自身永远都需要被转义 \\

还有 ],这个作为一个定界符,表示自定义字符类的定义结束,但是如果将它放在自定义字符类的开头,比如 /[]a]/ 表示 ]a 组成的字符类;或是放在第二个位置,而第一个位置是 ^,比如 /[^]a/ 表示不包含 ]a 的字符类。但是注意,这在 JavaScript 中不适用!在 JavaScript 中,/[]/ 无论何时都表示一个永远无法匹配成功的空字符类,而 /[^]/ 表示可以匹配任意单个字符的字符类,所以在 JavaScript 中 ] 无论如何都有特殊意义,因此永远都需要使用 \ 进行转义!

大多数在自定义字符类外面的转义标记也可以在自定义字符类中生效,比如不可打印字符(换行 \n 之类的)、八进制转义符、十六进制转义符、Unicode 转义符。

示例:

/^[\^\]\-\\]$/ 匹配 ^]-\

在部分正则引擎中(比如 .NET、XPath 等),自定义字符类还支持减法。比如 /[a-z-[aeiou]]/ 表示匹配所有小写的辅音英文字母,也就是从 az 这所有的 26 个英文字母中去除 aeiou 这五个元音字母。

还有部分正则引擎(比如 Java、Ruby 等),自定义字符类还支持交集。比如 /[a-z&&[^aeiou]]/ 也表示匹配所有小写的辅音英文字母,但是原理和上面不一样,这个是要求字符既在 az 这 26 个英文字母之中,又不能在 aeiou 这五个原因字母中。

七、重复

到目前为止,所有的正则表达式都是“静态”的,也就是你写了什么就匹配到什么,顶多使用字符类来代替多种字符。但是看看上面匹配手机号的示例,后面跟着 8 个 \d,这非常尴尬,如果后面跟着更多的数字怎么办?

正则中提供了“重复”功能,可以将前面的一个匹配内容重复多次。使用特殊符号 +*,可以使得前面一个匹配单元重复匹配多次,其中 + 要求至少出现一次。

示例:

/^130\d+$/ 可以匹配 130 开头,后面至少跟一个数字的字符串,比如 1301130123456789123456789 之类的,后面可以跟任意多个数字,但是 130 则不能匹配。

/^130\d*$/ 可以匹配 130 开头,后面跟不跟数字都可以,但要跟的话必须是跟数字的字符串,比如 1301301130123456789123456789,而 130a 则无法匹配。

⚠ 注意:重复是仅对前一个最小匹配单元生效的,上面的例子中,\d 是最小的匹配单元,所以是对 \d 重复。对于自定义字符类,也是属于一个最小匹配单元。

+* 的重复次数是上不封顶的,所以在匹配手机号的时候就用不到了,因为手机号后面固定是 8 位数字。此时,可以使用更加灵活的重复控制方法:{m,n}(中间不能有空格),这表示最少重复 m 次,最多重复 n 次(注意 m 和 n 都是闭区间)。

特别的,如果 m 与 n 相等的话,则可以省略为 {m},表示固定重复 m 次;而如果 n 等于正无穷的话,可以省略 n 变为 {m,},表示最少重复 m 次,上不封顶。

示例:

/^130\d{2,5}$/ 可以匹配 130 开头,后面跟着至少 2 个,至多 5 个数字的字符串,比如:13012130123130123413012345

/^130\d{2,}$/ 可以匹配 130 开头,后面跟着至少 2 个数字的字符串,比如:1301213012345130123456789123456789

/^130\d{8}$/ 可以匹配 130 开头,后面跟着 8 个数字的字符串,比如:13012345678

由此可见,+ 实际上就是 {1,} 的简写形式,而 * 实际上就是 {0,} 的简写形式。

还有一个特殊的“重复”类型:?,这个实际上不算是“重复”了,但是属于“重复”的衍生品,它表示不出现,或是出现一次,也就是 {0,1} 的简写形式。

示例:

/^colou?r$/ 可以匹配 colour 或是 color

/^https?:\/\/$/ 可以匹配 http:// 或是 https://。这里 / 虽然不是特殊符号,但是本文中使用 / 来标记正则,所以为了避免 / 被解析为正则的边界,所以使用 \ 对其进行了转义。实际上如果你使用的正则引擎不是以 / 来标记正则的,那么这里就不需要进行转义,比如:|^https?://$|

八、分组

在上面的重复中,我们发现,重复只能对前一个最小匹配单元生效,如果我们想要更灵活的重复怎么办呢?比如圣诞老人来了,HoHoHo~,这里我们想要匹配这个 HoHoHo 要怎么办呢?

正则中使用小括号 () 进行分组,括号内可以看作一个独立的子正则表达式个体,整个括号将被作为一个匹配单元对待,因此,我们只要对一个 Ho 进行分组,然后重复对这个分组进行即可~

示例:

/^(Ho){3}~$/ 可以匹配 HoHoHo~

联系到前面的“或者”,| 会对整个正则表达式生效,如果我有一个类似于这样格式的数据:性别:%s,角色:%s,其中性别只有 两种,角色有 管理员游客 两种,现在我想使用正则匹配这个字符串,简单的使用 | 就办不到了,此时就只有将前后两个需要使用到 | 的部分分别进行分组。

示例:

/^性别:(男|女),角色:(管理员|游客)$/:匹配 性别:男,角色:管理员性别:男,角色:游客性别:女,角色:管理员性别:女,角色:游客

九、分组引用

到现在,我们基本上可以通吃绝大多数的情况了。在有了分组之后,我们可以解锁一个新的技能:引用一个分组。

正则的一个非常常用的功能就是分组引用。在上面的例子 /^性别:(男|女),角色:(管理员|游客)$/ 中我们可以看到有两个分组,第一个是 (男|女),第二个是 (管理员|游客),这样在匹配的时候就可以得到两个可以使用的分组。最直观的可以体现在各个编程语言的匹配结果中,通常匹配结果会以一个数组形式(或者是类数组)返回,通常数组的第 0 个元素为匹配到的整个字符子串,而从第 1 个元素开始,表示匹配到的第一个分组的内容,第 2 个元素表示匹配到的第二个分组的内容。

因此,字符串 "性别:男,角色:管理员" 使用 /^性别:(男|女),角色:(管理员|游客)$/ 匹配得到的结果是:

[
    "性别:男,角色:管理员",
    "男",
    "管理员"
]
复制代码

分组除了可以在匹配结果中使用外,还可以在正则内部使用,比如,我要匹配使用一对引号引起来的字符串,引号可以是英文单引号,也可以是英文双引号。比如 'hello'"world",如果我们简单的使用 /^['"]\w+['"]$/,虽然可以成功匹配这两种情况,但是对于 'hello""world' 这种前后括号不匹配的情况也可以匹配成功,这显然不是我们想要的。这时,使用分组的引用就可以解决这个问题:

示例:

/^(['"])\w+\1$/ 可以匹配由英文单引号或是英文双引号引起来的单词,并且确保前后的引号是匹配的。

这里我们为 \w 前面的引号进行了一次分组,此时这个分组会被编号为 1,这样在后面我们使用 \1 引用前面的这个分组,就可以确保前后引号一致了~

这里要注意一下八进制转义符,八进制转义符不太统一,但通常是 \ 后直接跟数字,这就与分组引用冲突了,因为分组引用也是 \ 后直接跟数字。所以一般建议是不要使用八进制转义符,八进制通常可以很容易转成十六进制,因此建议在使用字符编号表示字符的时候,不要使用八进制,而是使用十六进制!

比如字母 'a',编号为十进制为 97,八进制为 141,十六进制为 61。那么八进制表示为 \141,十六进制表示为 \x61,或者使用 Unicode 表示为 \u0061

关注下面的标签,发现更多相似文章
评论