正则表达式

572 阅读10分钟

本文摘录于:《正则表达式30分钟入门教程》

在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。
正则表达式就是用于描述这些规则的工具。
即,正则表达式就是记录文本规则的代码。

通常,处理正则表达式的工具会提供一个忽略大小写的选项

元字符 ( metacharacter )

代码说明
.匹配除换行符以外的任意字符
\w匹配字母或数字或下划线或汉字
\s匹配任意的空白符
\d匹配数字
\b匹配单词的开始或结束
^匹配字符串的开始
$匹配字符串的结束

例1: 在一篇英文小说里找 hi
\bhi\b
\b 能忽略掉 himhigh 等单词中的 hi ,实现精确查找

例2: 假如要找的是 hi 后面不远处跟着一个 Lucy
\bhi\b.*\bLucy\b
解释:先是找到 hi ,然后是任意个任意字符(但不能是换行),最后是 Lucy

例3: 匹配这样的字符串:以 0 开头,然后是两个数字,然后是一个连字号 - ,最后是 8 个数字
0\d\d-\d\d\d\d\d\d\d\d
这里的 - 不是元字符,只匹配它本身——连字符(或者减号,或者中横线,或者随怎么称呼它)
为了避免那么多烦人的重复,也可以这样写这个表达式: 0\d{2}-\d{8} 。这里 \d 后面的 {2}({8}) 的意思是前面 \d 必须连续重复匹配 2 次( 8 次)。

例4: 匹配以字母 a 开头的单词
\ba\w*\b
解释:先是某个单词开始处( \b ),然后是字母 a ,然后是任意数量的字母或数字( \w* ),最后是单词结束处( \b )

例5: 匹配 1 个或更多连续的数字
用: \d+

例6: 匹配刚好 6 个字符的单词
用: \b\w{6}\b

例7: 一个网站如果要求填写的 QQ 号必须为 5 位到 12 位数字
用: ^\d{5,12}$

字符转意

如果想查找元字符本身,比如查找 . ,或者 * ,就出现了问题:我没办法指定它们,因为它们会被解释成别的意思。
这时就得使用 \ 来取消这些字符的特殊意义。因此,应该使用 \.\* 。当然,要查找 \ 本身,得用 \\

重复

上面已经看过了前面的 * , + , {2} , {5,12} 这几个匹配重复的方式了。下面是正则表达式中所有的限定符(指定数量的代码):

代码/语法说明
*重复零次或更多次
+重复一次或更多次
重复零次或一次
{n}重复 n 次
{n,}重复 n 次或更多次
{n,m}重复 n 到 m 次

例8: 匹配Windows后面跟1个或更多数字
用: Windows\d+

例9: 匹配一行的第一个单词(或整个字符串的第一个单词,具体匹配哪个意思得看选项设置)
用: ^\w+

字符类

要想查找数字,字母或数字,空白,这些都是很简单的,因为已经有了对应这些字符集合的元字符

例10: 如果想匹配没有预定义元字符的字符集合(比如元音字母 a , e , i , o , u ),应该怎么办?
只需要在方括号里列出它们就行了
用: [aeiou] 就能匹配任何一个英文元音字母
用: [.?!] 可以匹配标点符号( .?! )
用: [0-9] 代表的含意与 \d 就是完全一致的————一位数字 用: [a-z0-9A-Z_] 也完全等同于英文环境下的 \w

例11: 下面是一个更复杂的表达式
\(?0\d{2}[) -]?\d{8}
解释:这个表达式可以匹配几种格式的电话号码,像 (010)88886666 ,或 022-22334455 ,或 02912345678 等
它首先是一个转义字符 \( ,它能出现 0 次或 1 次 ( ? ) ,然后是一个 0 ,后面跟着 2 个数字 ( \d{2} ) ,然后是 )- 或空格中的一个,它出现 1 次或不出现 ( ? ) ,最后是 8 个数字 ( \d{8} )

分支条件

不幸的是,例11 中的表达式也能匹配 010)12345678 或 (022-87654321 这样的“不正确”的格式
要解决这个问题,需要用到 分枝条件
正则表达式里的分枝条件指的是有几种规则,如果满足其中任意一种规则都应该当成匹配,具体方法是用 | 把不同的规则分隔开

例12: 匹配两种以连字号分隔的电话号码:一种是 3 位区号, 8 位本地号(如 010-12345678 ),一种是 4 位区号, 7 位本地号( 0376-2233445 )
用: 0\d{2}-\d{8}|0\d{3}-\d{7}

例13: 匹配 3 或 4 位区号的电话号码,其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用连字号或空格间隔,也可以没有间隔
用: \(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}|\(0\d{3}\)[- ]?\d{7}|0\d{3}[- ]?\d{7}

使用分枝条件时,要注意各个条件的顺序

例14: 匹配美国的邮政编码。美国邮编的规则是 5 位数字,或者用连字号间隔的 9 位数字
用: \d{5}-\d{4}|\d{5}
不能用这种: \d{5}|\d{5}-\d{4} ,它只会匹配 5 位的邮编(以及 9 位邮编的前 5 位)
解释:匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件

分组

目前我们已经知道怎么重复单个字符(直接在字符后面加上限定符就行了);但如果想要重复多个字符又该怎么办?
可以用小括号来指定 子表达式 (也叫做 分组 ),然后就能指定这个子表达式的重复次数了,也可以对子表达式进行其它一些操作

例15: 一个简单的IP地址匹配表达式, (\d{1,3}\.){3}\d{1,3}
解释: \d{1,3} 匹配 13 位的数字, (\d{1,3}\.){3} 匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复 3 次,最后再加上一个一到三位的数字 (\d{1,3})
可惜,它也将匹配 256.300.888.999 这种不可能存在的IP地址。
如果能使用算术比较的话,或许能简单地解决这个问题,但是正则表达式中并不提供关于数学的任何功能,所以只能使用冗长的分组,选择,字符类来描述一个正确的 IP 地址
用: ((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)

反义

有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到 反义

代码/语法说明
\W匹配任意不是字母,数字,下划线,汉字的字符
\S匹配任意不是空白符的字符
\D匹配任意非数字的字符
\B匹配不是单词开头或结束的位置
[^x]匹配除了 x 以外的任意字符
[^aeiou]匹配除了 aeiou 这几个字母以外的任意字符

例16: 匹配不包含空白符的字符串
用: \S+

例17: 匹配用尖括号括起来的以 a 开头的字符串
用: <a[^>]+>

后向引用

使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。
默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为 1 ,第二个为 2 ,以此类推。

后向引用 用于重复搜索前面某个分组匹配的文本。

例18: 分组1匹配的文本
用: \1

例19: 匹配重复的单词,像go go, 或者kitty kitty
用: \b(\w+)\b\s+\1\b
解释: 这个表达式首先是一个单词,也就是单词开始处和结束处之间的多于一个的字母或数字( \b(\w+)\b ),这个单词会被捕获到编号为 1 的分组中,然后是 1 个或几个空白符( \s+ ),最后是分组 1 中捕获的内容(也就是前面匹配的那个单词)( \1 )

也可以自己指定子表达式的组名。要指定一个子表达式的组名,要使用这样的语法: (?<Word>\w+) (或者把尖括号换成 ' 也行: (?'Word'\w+) ),这样就把 \w+ 的组名指定为 Word 了。要反向引用这个分组捕获的内容,可以使用 \k<Word> ,所以上一个例子也可以写成这样: \b(?<Word>\w+)\b\s+\k<Word>\b

image.png

零宽断言

接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像 \b , ^ , $ 那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为 零宽断言

(?=exp) 也叫 零宽度正预测先行断言 ,它断言自身出现的位置的后面能匹配表达式 exp

例20: 匹配以 ing 结尾的单词的前面部分(除了 ing 以外的部分),如查找 I'm singing while you're dancing.时, 匹配 sing 和 danc
用: \b\w+(?=ing\b)

(?<=exp) 也叫 零宽度正回顾后发断言 ,它断言自身出现的位置的前面能匹配表达式 exp

例21: 匹配以 re 开头的单词的后半部分(除了 re 以外的部分),例如在查找reading a book时,它匹配 ading
用: (?<=\bre)\w+\b

例22: 要给一个很长的数字中每三位间加一个逗号(当然是从右边加起了),可以这样查找需要在前面和里面添加逗号的部分
用:((?<=\d)\d{3})+\b
用它对 1234567890 进行查找时结果是 234567890

负向零宽度断言

前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)
但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?

例23: 想查找这样的单词———它里面出现了字母 q ,但是 q 后面跟的不是字母 u
用: \b\w*q[^u]\w*\b
但如果 q 出现在单词的结尾的话,像 Iraq , Benq ,这个表达式就会出错。
这是因为 [^u] 总要匹配一个字符,所以如果 q 是单词的最后一个字符的话,后面的 [^u] 将会匹配 q 后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的 \w*\b 将会匹配下一个单词,于是 \b\w*q[^u]\w*\b 就能匹配整个 Iraq fighting。
负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个问题: \b\w*q(?!u)\w*\b

(?!exp) 也叫 零宽度负预测先行断言,它断言此位置的后面不能匹配表达式 exp

例24: 匹配三位数字,而且这三位数字的后面不能是数字
用: \d{3}(?!\d)

例25: 匹配不包含连续字符串 abc 的单词
用: \b((?!abc)\w)+\b

(?<!exp) 也叫 零宽度负回顾后发断言,它断言此位置的前面不能匹配表达式 exp

例26: 匹配前面不是小写字母的七位数字
用:(?<![a-z])\d{7}

例27: 匹配不包含属性的简单 HTML 标签内里的内容
用:(?<=<(\w+)>).*(?=<\/\1>)
解释: (?<=<(\w+)>) 指定了这样的前缀——被尖括号括起来的单词(比如可能是 <b> ),然后是 .* (任意的字符串),最后是一个 后缀 (?=<\/\1>) 。注意后缀里的 \/ ,它用到了前面提过的字符转义; \1 则是一个反向引用,引用的正是捕获的第一组,前面的 (\w+) 匹配的内容,这样如果前缀实际上是 <b> 的话,后缀就是 </b> 了。整个表达式匹配的是 <b></b> 之间的内容(再次提醒,不包括前缀和后缀本身)

注释

小括号的另一种用途是通过语法 (?#comment) 来包含注释
例如:2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)
要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在 # 后面到这一行结束的所有文本都将被当成注释忽略掉。例如,我们可以前面的一个表达式写成这样:

     (?<=    # 断言要匹配的文本的前缀
     <(\w+)> # 查找尖括号括起来的内容
             # (即HTML/XML标签)
     )       # 前缀结束
     .*      # 匹配任意文本
     (?=     # 断言要匹配的文本的后缀
     </\1>   # 查找尖括号括起来的内容
             # 查找尖括号括起来的内容
     )       # 后缀结束

贪婪与懒惰

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。
以这个表达式为例: a.*b ,它将会匹配最长的以 a 开始,以 b 结束的字符串。如果用它来搜索 aabab 的话,它会匹配整个字符串 aabab 。这被称为贪婪匹配

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。
前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号 ? 。这样 .*? 就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。

例28: 匹配最短的,以 a 开始,以 b 结束的字符串
用: a.*?b
如果把它应用于 aabab 的话,它会匹配 aab (第一到第三个字符)和 ab (第四到第五个字符)

代码/语法说明
*?重复任意次,但尽可能少重复
+?重复1次或更多次,但尽可能少重复
??重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}?重复n次以上,但尽可能少重复

处理选项

平衡组/递归匹配