从字符出发
单个字符
特殊字符 | 正则表达式 | 记忆方式 |
---|---|---|
换行符 | \n | new line |
换页符 | \f | form feed |
回车符 | \r | return |
空白符 | \s | space |
制表符 | \t | tab |
垂直制表符 | \v | vertical tab |
回退符 | [\b] | backspace,之所以使用[]符号是避免和\b重复 |
多个字符
匹配区间 | 正则表达式 | 记忆方式 |
---|---|---|
除了换行符之外的任何字符 | . | 句号,除了句子结束符 |
单个数字, [0-9] | \d | digit |
除了[0-9] | \D | not digit |
包括下划线在内的单个字符,[A-Za-z0-9_] | \w | word |
非单字字符 | \W | not word |
匹配空白字符,包括空格、制表符、换页符和换行符 | \s | space |
匹配非空白字符 | \S | not space |
位置边界
单词边界
单词是构成句子和文章的基本单位,一个常见的使用场景是把文章或句子中的特定单词找出来。如:
1 | The cat scattered his food all over the room. |
我想找到cat
这个单词,但是如果只是使用/cat/
这个正则,就会同时匹配到cat
和scattered
这两处文本。这时候我们就需要使用边界正则表达式\b
,其中b是boundary的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。
上面的例子改写成/\bcat\b/
这样就能匹配到cat
这个单词了。
字符串边界
匹配完单词,我们再来看一下一整个字符串的边界怎么匹配。元字符^
用来匹配字符串的开头。而元字符$
用来匹配字符串的末尾。注意的是在长文本里,如果要排除换行符的干扰,我们要使用多行模式。试着匹配I am scq000
这个句子:
1 | I am scq000. |
我们可以使用/^I am scq000\.$/m
这样的正则表达式,其实m是multiple line的首字母。正则里面的模式除了m外比较常用的还有i和g。前者的意思是忽略大小写,后者的意思是找到所有符合的匹配。
边界和标志 | 正则表达式 | 记忆方式 |
---|---|---|
单词边界 | \b | boundary |
非单词边界 | \B | not boundary |
字符串开头 | ^ | 小头尖尖那么大个 |
字符串结尾 | $ | 终结者,美国科幻电影,美元符$ |
多行模式 | m标志 | multiple of lines |
忽略大小写 | i标志 | ignore case, case-insensitive |
全局模式 | g标志 | global |
子表达式
字符匹配我们介绍的差不多了,更加高级的用法就得用到子表达式了。通过嵌套递归和自身引用可以让正则发挥更强大的功能。
从简单到复杂的正则表达式演变通常要采用分组、回溯引用和逻辑处理的思想。利用这三种规则,可以推演出无限复杂的正则表达式。
分组
语法:(
,)
其中分组体现在:所有以(
和)
元字符所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。如果只是使用简单的(regex)
匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。
回溯引用
所谓回溯引用(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像\1
,\2
,….,其中\1
表示引用的第一个子表达式,\2
表示引用的第二个子表达式,以此类推。而\0
则表示整个表达式。
假设现在要在下面这个文本里匹配两个连续相同的单词,你要怎么做呢?
1 | Hello what what is the first thing, and I am am scq000. |
利用回溯引用,我们可以很容易地写出\b(\w+)\s\1
这样的正则。
回溯引用在替换字符串中十分常用,语法上有些许区别,用$1
,$2
…来引用要被替换的字符串。下面以js代码作演示:
1 | var str = 'abc abc 123'; |
如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)
这样就可以避免浪费内存。
1 | var str = 'scq000'. |
有时,我们需要限制回溯引用的适用范围。那么通过前向查找和后向查找就可以达到这个目的。
前向查找
前向查找(lookahead)是用来限制后缀的。凡是以(?=regex)
包含的子表达式在匹配过程中都会用来限制前面的表达式的匹配。例如happy happily
这两个单词,我想获得以happ
开头的副词,那么就可以使用happ(?=ily)
来匹配。如果我想过滤所有以happ
开头的副词,那么也可以采用负前向查找的正则happ(?!ily)
,就会匹配到happy
单词的happ
前缀。
后向查找
介绍完前向查找,接着我们再来介绍一下它的反向操作:后向查找(lookbehind)。后向查找(lookbehind)是通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字串。举个简单的例子: apple
和people
都包含ple
这个后缀,那么如果我只想找到apple
的ple
,该怎么做呢?我们可以通过限制app
这个前缀,就能唯一确定ple
这个单词了。
1 | /(?<=app)ple/ |
其中(?<=regex)
的语法就是我们这里要介绍的后向查找。regex
指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向后查找。另外一种限制匹配是利用(?<!regex)
语法,这里称为负后向查找。与正前向查找不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找apple
的ple
也可以这么写成/(?<!peo)ple
。
需要注意的,不是每种正则实现都支持后向查找。在javascript中是不支持的,所以如果有用到后向查找的情况,有一个思路是将字符串进行翻转,然后再使用前向查找,作完处理后再翻转回来。看一个简单的例子:
1 | // 比如我想替换apple的ple为ply |
最后回顾一下这部分内容:
回溯查找 | 正则 | 记忆方式 |
---|---|---|
引用 | \0,\1,\2 和 $0, $1, $2 | 转义+数字 |
非捕获组 | (?:) | 引用表达式(()), 本身不被消费(?),引用(:) |
前向查找 | (?=) | 引用子表达式(()),本身不被消费(?), 正向的查找(=) |
前向负查找 | (?!) | 引用子表达式(()),本身不被消费(?), 负向的查找(!) |
后向查找 | (?<=) | 引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),正的查找(=) |
后向负查找 | (?<!) | 引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),负的查找(!) |
逻辑处理
计算机科学就是一门包含逻辑的科学。让我们回忆一下编程语言当中用到的三种逻辑关系,与或非。
在正则里面,默认的正则规则都是与的关系所以这里不讨论。
而非关系,分为两种情况:一种是字符匹配,另一种是子表达式匹配。在字符匹配的时候,需要使用^
这个元字符。在这里要着重记忆一下:只有在[和]内部使用的^才表示非的关系。子表达式匹配的非关系就要用到前面介绍的前向负查找子表达式(?!regex)
或后向负查找子表达式(?<!regex)
。
或关系,通常给子表达式进行归类使用。比如,我同时匹配a,b两种情况就可以使用(a|b)
这样的子表达式。
逻辑关系 | 正则元字符 |
---|---|
与 | 无 |
非 | [^regex]和! |
或 | | |