正则表达式不要背

正则表达式一直是困扰不少程序员的一门技术,固然也包括曾经的我。大多数时候咱们在开发过程当中要用到某些正则表达式的时候,都会打开谷歌或百度直接搜索而后拷贝粘贴。当下一次再遇到相同问题的时候,一样的场景又再来一遍。做为一门用途很广的技术,我相信深刻理解正则表达式并能融会贯通是值得的。因此,但愿这篇文章能帮助你们理清思路,搞懂正则表达式各类符号之间的内在联系,造成知识体系,当下次再遇到正则表达式的时候能够不借助搜索引擎,本身解决。javascript

正则表达式究竟是什么

正则表达式(Regular Expression)其实就是一门工具,目的是为了字符串模式匹配,从而实现搜索和替换功能。它起源于上个20世纪50年代科学家在数学领域作的一些研究工做,后来才被引入到计算机领域中。从它的命名咱们能够知道,它是一种用来描述规则的表达式。而它的底层原理也十分简单,就是使用状态机的思想进行模式匹配。你们能够利用regexper.com这个工具很好地可视化本身写的正则表达式:java

/\d\w+/这个正则生成的状态机图:git

对于具体的算法实现,你们若是感兴趣能够阅读《算法导论》。程序员

从字符出发

咱们学习一个系统化的知识,必定要从其基础构成来了解。正则表达式的基本组成元素能够分为:字符和元字符。字符很好理解,就是基础的计算机字符编码,一般正则表达式里面使用的就是数字、英文字母。而元字符,也被称为特殊字符,是一些用来表示特殊语义的字符。如^表示非,|表示或等。利用这些元字符,才能构造出强大的表达式模式(pattern)。接下来,咱们就来从这些基本单位出发,来学习一下如何构建正则表达式。github

单个字符

最简单的正则表达式能够由简单的数字和字母组成,没有特殊的语义,纯粹就是一一对应的关系。如想在'apple'这个单词里找到‘a'这个字符,就直接用/a/这个正则就能够了。面试

可是若是想要匹配特殊字符的话,就得请出咱们第一个元字符**\**, 它是转义字符字符,顾名思义,就是让其后续的字符失去其原本的含义。举个例子:正则表达式

我想匹配*这个符号,因为*这个符号自己是个特殊字符,因此我要利用转义元字符\来让它失去其原本的含义:算法

/\*/
复制代码

若是原本这个字符不是特殊字符,使用转义符号就会让它拥有特殊的含义。咱们经常须要匹配一些特殊字符,好比空格,制表符,回车,换行等, 而这些就须要咱们使用转义字符来匹配。为了便于记忆,我整理了下面这个表格,并附上记忆方式:chrome

特殊字符 正则表达式 记忆方式
换行符 \n new line
换页符 \f form feed
回车符 \r return
空白符 \s space
制表符 \t tab
垂直制表符 \v vertical tab
回退符 [\b] backspace,之因此使用[]符号是避免和\b重复

多个字符

单个字符的映射关系是一对一的,即正则表达式的被用来筛选匹配的字符只有一个。而这显然是不够的,只要引入集合区间和通配符的方式就能够实现一对多的匹配了。编程

在正则表达式里,集合的定义方式是使用中括号[]。如/[123]/这个正则就能同时匹配1,2,3三个字符。那若是我想匹配全部的数字怎么办呢?从0写到9显然太太低效,因此元字符-就能够用来表示区间范围,利用/[0-9]/就能匹配全部的数字, /[a-z]/则能够匹配全部的英文小写字母。

即使有了集合和区间的定义方式,若是要同时匹配多个字符也仍是要一一列举,这是低效的。因此在正则表达式里衍生了一批用来同时匹配多个字符的简便正则表达式:

匹配区间 正则表达式 记忆方式
除了换行符以外的任何字符 . 句号,除了句子结束符
单个数字, [0-9] \d digit
除了[0-9] \D not digit
包括下划线在内的单个字符,[A-Za-z0-9_] \w word
非单字字符 \W not word
匹配空白字符,包括空格、制表符、换页符和换行符 \s space
匹配非空白字符 \S not space

循环与重复

一对一和一对多的字符匹配都讲完了。接下来,就该介绍如何同时匹配多个字符。要实现多个字符的匹配咱们只要屡次循环,重复使用咱们的以前的正则规则就能够了。那么根据循环次数的多与少,咱们能够分为0次,1次,屡次,特定次。

0 | 1

元字符?表明了匹配一个字符或0个字符。设想一下,若是你要匹配colorcolour这两个单词,就须要同时保证u这个字符是否出现都能被匹配到。因此你的正则表达式应该是这样的:/colou?r/

>= 0

元字符*用来表示匹配0个字符或无数个字符。一般用来过滤某些无关紧要的字符串。

>= 1

元字符+适用于要匹配同个字符出现1次或屡次的状况。

特定次数

在某些状况下,咱们须要匹配特定的重复次数,元字符{}用来给重复匹配设置精确的区间范围。如'a'我想匹配3次,那么我就使用/a{3}/这个正则,或者说'a'我想匹配至少两次就是用/a{2,}/这个正则。

如下是完整的语法:

- {x}: x次

- {min, max}: 介于min次到max次之间

- {min, }: 至少min次

- {0, max}: 至多max次
复制代码

因为这些元字符比较抽象,且容易混淆,因此我用了联想记忆的方式编了口诀能保证在用到的时候就能回忆起来。

匹配规则 元字符 联想方式
0次或1次 ? ,此事
0次或无数次 * 宇宙洪荒,辰宿列张:宇宙伊始,从无到有,最后星宿布满星空
1次或无数次 + 一加, +1
特定次数 {x}, {min, max} 能够想象成一个数轴,从一个点,到一个射线再到线段。min和max分别表示了左闭右闭区间的左界和右界

位置边界

上面咱们把字符的匹配都介绍完了,接着咱们还须要位置边界的匹配。在长文本字符串查找过程当中,咱们经常须要限制查询的位置。好比我只想在单词的开头结尾查找。

单词边界

单词是构成句子和文章的基本单位,一个常见的使用场景是把文章或句子中的特定单词找出来。如:

The cat scattered his food all over the room.
复制代码

我想找到cat这个单词,可是若是只是使用/cat/这个正则,就会同时匹配到catscattered这两处文本。这时候咱们就须要使用边界正则表达式\b,其中b是boundary的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。

上面的例子改写成/\bcat\b/这样就能匹配到cat这个单词了。

字符串边界

匹配完单词,咱们再来看一下一整个字符串的边界怎么匹配。元字符^用来匹配字符串的开头。而元字符$用来匹配字符串的末尾。注意的是在长文本里,若是要排除换行符的干扰,咱们要使用多行模式。试着匹配I am scq000这个句子:

I am scq000.
I am scq000.
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则表示整个表达式。

假设如今要在下面这个文本里匹配两个连续相同的单词,你要怎么作呢?

Hello what what is the first thing, and I am am scq000.
复制代码

利用回溯引用,咱们能够很容易地写出\b(\w+)\s\1这样的正则。

回溯引用在替换字符串中十分经常使用,语法上有些许区别,用$1,$2...来引用要被替换的字符串。下面以js代码做演示:

var str = 'abc abc 123';
str.replace(/(ab)c/g,'$1g');
// 获得结果 'abg abg 123'
复制代码

若是咱们不想子表达式被引用,可使用非捕获正则(?:regex)这样就能够避免浪费内存。

var str = 'scq000'.
str.replace(/(scq00)(?:0)/, '$1,$2')
// 返回scq00,$2
// 因为使用了非捕获正则,因此第二个引用没有值,这里直接替换为$2
复制代码

有时,咱们须要限制回溯引用的适用范围。那么经过前向查找和后向查找就能够达到这个目的。

前向查找

前向查找(lookahead)是用来限制后缀的。凡是以(?=regex)包含的子表达式在匹配过程当中都会用来限制前面的表达式的匹配。例如happy happily这两个单词,我想得到以happ开头的副词,那么就可使用happ(?=ily)来匹配。若是我想过滤全部以happ开头的副词,那么也能够采用负前向查找的正则happ(?!ily),就会匹配到happy单词的happ前缀。

后向查找

介绍完前向查找,接着咱们再来介绍一下它的反向操做:后向查找(lookbehind)。后向查找(lookbehind)是经过指定一个子表达式,而后从符合这个子表达式的位置出发开始查找符合规则的字串。举个简单的例子: applepeople都包含ple这个后缀,那么若是我只想找到appleple,该怎么作呢?咱们能够经过限制app这个前缀,就能惟一肯定ple这个单词了。

/(?<=app)ple/
复制代码

其中(?<=regex)的语法就是咱们这里要介绍的后向查找。regex指代的子表达式会做为限制项进行匹配,匹配到这个子表达式后,就会继续向查找。另一种限制匹配是利用(?<!regex) 语法,这里称为负后向查找。与正前向查找不一样的是,被指定的子表达式不能被匹配到。因而,在上面的例子中,若是想要查找appleple也能够这么写成/(?<!peo)ple

须要注意的,不是每种正则实现都支持后向查找。在javascript中是不支持的,因此若是有用到后向查找的状况,有一个思路是将字符串进行翻转,而后再使用前向查找,做完处理后再翻转回来。看一个简单的例子:

// 好比我想替换apple的ple为ply
var str = 'apple people';
str.split('').reverse().join('').replace(/elp(?=pa)/, 'ylp').split('').reverse().join('');
复制代码

ps: 感谢评论区提醒,从es2018以后,chrome中的正则表达式也支持反向查找了。不过,在实际项目中还须要注意对旧浏览器的支持,以防线上出现Bug。详情请查看http://kangax.github.io/compat-table/es2016plus/#test-RegExp_Lookbehind_Assertions

最后回顾一下这部份内容:

回溯查找 正则 记忆方式
引用 \0,\1,\2 和 $0, $1, $2 转义+数字
非捕获组 (?:) 引用表达式(()), 自己不被消费(?),引用(:)
前向查找 (?=) 引用子表达式(()),自己不被消费(?), 正向的查找(=)
前向负查找 (?!) 引用子表达式(()),自己不被消费(?), 负向的查找(!)
后向查找 (?<=) 引用子表达式(()),自己不被消费(?), 后向的(<,开口日后),正的查找(=)
后向负查找 (?<!) 引用子表达式(()),自己不被消费(?), 后向的(<,开口日后),负的查找(!)

逻辑处理

计算机科学就是一门包含逻辑的科学。让咱们回忆一下编程语言当中用到的三种逻辑关系,与或非。

在正则里面,默认的正则规则都是的关系因此这里不讨论。

关系,分为两种状况:一种是字符匹配,另外一种是子表达式匹配。在字符匹配的时候,须要使用^这个元字符。在这里要着重记忆一下:只有在[]内部使用的^才表示非的关系。子表达式匹配的非关系就要用到前面介绍的前向负查找子表达式(?!regex)或后向负查找子表达式(?<!regex)

或关系,一般给子表达式进行归类使用。好比,我同时匹配a,b两种状况就可使用(a|b)这样的子表达式。

逻辑关系 正则元字符
[^regex]和!
|

总结

对于正则来讲,符号之抽象每每让不少程序员却步。针对很差记忆的特色,我经过分类和联想的方式努力让其变得有意义。咱们先从一对一的单字符,再到多对多的子字符串介绍,而后经过分组、回溯引用和逻辑处理的方式来构建高级的正则表达式。

在最后,出个经常使用的正则面试题吧:请写出一个正则来处理数字千分位,如12345替换为12,345。请尝试本身推理演绎得出答案,而不是依靠搜索引擎:)。

——本文首发于我的公众号,转载请注明出处———

微信扫描二维码,关注个人公众号
最后,欢迎你们关注个人公众号,一块儿学习交流。