前端共享正则表达式前端派

正则详解--程序员必备

2017-06-13  本文已影响371人  前端渣渣

转自: JS正则表达式一条龙讲解,从原理和语法到JS正则、ES6正则扩展,最后再到正则实践思路

温馨提示:文章很长很长,保持耐心,必要时可以跳着看,当然用来查也是不错的。

正则啊,就像一座灯塔,当你在字符串的海洋不知所措的时候,总能给你一点思路;正则啊,就像一台验钞机,在你不知道用户提交的钞票真假的时候,总能帮你一眼识别;正则啊,就像一个手电筒,在你需要找什么玩意的时候,总能帮你get你要的东西...

—— 节选自 Stinson 同学的语文排比句练习《正则》

欣赏了一段文学节选后,我们正式来梳理一遍JS中的正则,本文的首要目的是,防止我经常忘记正则的一些用法,故梳理和写下来加强熟练度和用作参考,次要目的是与君共勉,如有纰漏,请不吝赐教,良辰谢过。

本文既然取题为“一条龙”,就要对得起”龙”,故将包括正则原理、语法一览、JS(ES5)中的正则、ES6对正则的扩展、实践正则的思路,我尽量深入尽量浅出地去讲这些东西(搞得好像真能深入浅出一样的),如果你只想知道怎么应用,那么看第二、三、五部分,基本就能满足你的需求了,如果想掌握JS中的正则的,那么还是委屈你跟着我的思路来吧,嘿嘿嘿!

JS正则一条龙讲解

一、原理概论

在一开始用正则的时候,就觉得神奇,计算机究竟是怎么根据一个正则表达式来匹配字符串的?直到后来我遇到了一本书叫《计算理论》,看到了正则、DFA、NFA的概念和相互间的联系,才有一些恍然小悟的意思。

但如果真的要从原理上吃透正则表达式,那么恐怕最好的方式是:

  1. 首先去找一本专门讲正则的书去看看,O’REILLY的“动物总动员”系列里就有;2. 再自己实现一个正则引擎。

而本文的重点在于JS中正则的应用,故原理仅作简单介绍(因为我也没写过正则引擎,也不深入),一来大致“糊弄下”像我一样的好奇宝宝们对正则原理的疑惑,二来知道一些原理方面基本的知识,对于理解语法和写正则是大有裨益的。

1. 正则引擎

为什么正则能有效,因为有引擎,这和为什么JS能执行一样,有JS引擎,所谓正则引擎,可以理解为根据你的正则表达式用算法去模拟一台机器,这台机器有很多状态,通过读取待测的字符串,在这些状态间跳来跳去,如果最后停在了“终结状态”(Happy Ending),那么就Say I Do,否则Say You Are a Good Man。如此将一个正则表达式转换为一个可在有限的步数中计算出结果的机器,那么就实现了引擎。

正则的引擎大致可分为两类:DFA和NFA

  1. DFA (Deterministic finite automaton) 确定型有穷自动机2. NFA (Non-deterministic finite automaton) 非确定型有穷自动机,大部分都是NFA

这里的“确定型”指,对于某个确定字符的输入,这台机器的状态会确定地从a跳到b,“非确定型”指,对于某个确定字符的输入,这台机器可能有好几种状态的跳法;这里的“有穷”指,状态是有限的,可以在有限的步数内确定某个字符串是被接受还是发好人卡的;这里的“自动机”,可以理解为,一旦这台机器的规则设定完成,就可以自行判断了,不要人看。

DFA引擎不需要进行回溯,所以匹配效率一般情况下要高,但是它并不支持捕获组,于是也就不支持反向引用和$这种形式的引用,也不支持环视(Lookaround)、非贪婪模式等一些NFA引擎特有的特性。

如果想更详细地了解正则、DFA、NFA,那么可以去看一下《计算理论》,然后你可以根据某个正则表达式自己画出一台自动机。

2. 知识储备

这一小节对于你理解正则表达式很有用,尤其是明白什么是字符,什么是位置。

2.1 正则眼中的字符串——n个字符,n+1个位置

字符和位置

在上面的“笑声”字符串中,一共有8个字符,这是你能看到的,还有9个位置,这是聪明的人才能看到的。为什么要有字符还要有位置呢?因为位置是可以被匹配的。

那么进一步我们再来理解“占有字符”和“零宽度”:

占有字符是互斥的,零宽度是非互斥的。也就是一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。举个栗子,比如/aa/是匹配不了a的,这个字符串中的a只能由正则的第一个a字符匹配,而不能同时由第二个a匹配(废话);但是位置是可以多个匹配的,比如/\b\ba/是可以匹配a的,虽然正则表达式里有2个表示单词开头位置的\b元字符,这两个\b是可以同时匹配位置0(在这个例子中)的。

注意:我们说字符和位置是面向字符串说的,而说占有字符和零宽度是面向正则说的。

2.2 控制权和传动

这两个词可能在搜一些博文或者资料的时候会遇到,这里做一个解释先:

控制权是指哪一个正则子表达式(可能为一个普通字符、元字符或元字符序列组成)在匹配字符串,那么控制权就在哪。

传动是指正则引擎的一种机制,传动装置将定位正则从字符串的哪里开始匹配。

正则表达式当开始匹配的时候,一般是由一个子表达式获取控制权,从字符串中的某一个位置开始尝试匹配,一个子表达式开始尝试匹配的位置,是从前一子表达匹配成功的结束位置开始的。

举一个栗子,read(?=ing)ing\sbook匹配reading book,我们把这个正则看成5个子表达式read(?=ing)ing\sbook,当然你也可以吧read看做4个单独字符的子表达式,只是我们这里为了方便这么看待。read从位置0开始匹配到位置4,后面的(?=ing)继续从位置4开始匹配,发现位置4后面确实是ing,于是断言匹配成功,也就是整一个(?=ing)就是匹配了位置4这一个位置而已(这里更能理解什么是零宽了吧),然后后面的ing再从位置4开始匹配到位置7,然后\s再从位置7匹配到位置8,最后的book从位置8匹配到位置12,整一个匹配完成。

3. 匹配之旅“浅”度游(可跳过)

说了那么多,我们把自己当做一个正则引擎,一步一步以最小的单位——“字符”和“位置”——去看一下正则匹配的过程,举几个栗子。

3.1 基本匹配

正则表达式:easy
源字符串:So easy

匹配过程:首先由正则表达式字符`e`取得控制权,从字符串的位置0开始匹配,遇到字符串字符‘S’,匹配失败,然后正则引擎向前传动,从位置1开始尝试,遇到字符串字符‘o’,匹配失败,继续传动,后面的空格自然也失败,于是从位置3开始尝试匹配,成功匹配字符串字符‘e’,控制权交给正则表达式子表达式(这里也是一个字符)`a`,尝试从上次匹配成功的**结束**位置4开始匹配,成功匹配字符串字符‘a’,后面一直如此匹配到‘y’,然后匹配完成,匹配结果为`easy`。

3.2 零宽匹配

正则:^(?=<span class="hljs-string">[aeiou]</span>)<span class="hljs-string">[a-z]</span>+$
源字符串:apple`</pre>

首先这个正则表示:匹配这样一个从头到尾完整的字符串,这整一个字符串仅由小写字母组成,并且以a、e、i、o、u这5个字母任一字母开头。

匹配过程:首先正则的^(表示字符串开始的位置)获取控制权,从位置0开始匹配,匹配成功,控制权交给(?=[aeiou]),这个子表达式要求该位置右边必须是元音小写字母中的一个,零宽子表达式相互间不互斥,所以从位置0开始尝试匹配,右侧是字符串的‘a’,符合因此匹配成功,所以(?=[aeiou])匹配此处的位置0匹配成功,控制权交给[a-z]+,从位置0开始匹配,字符串‘apple’中的每个字符都匹配成功,匹配到字符串末尾,控制权交回正则的$,尝试匹配字符串结束位置,成功,至此,整个匹配完成。

3.3 贪婪匹配和非贪婪匹配

<pre class="hljs dust"><span class="xml">正则1:</span><span class="hljs-template-variable">{.*}</span><span class="xml"> 正则2:</span><span class="hljs-template-variable">{.*?}</span><span class="xml"> 源字符串:</span><span class="hljs-template-variable">{233}</span><span class="xml"></span></pre>

这里有两个正则,在限定符(语法会讲什么是限定符)后面加?符号表示忽略优先量词,也就是非贪婪匹配,这个栗子我剥得快一点。

首先开头的{匹配,两个正则都是一样的表现。

正则1的'.'为贪婪匹配,所以一直匹配余下字符串'233}',匹配到字符串结束位置,只是每次匹配,都记录一个备选状态,为了以后回溯,每次匹配有两条路,选择了匹配这条路,但记一下这里还可以有不匹配这条路,如果前面死胡同了,可以退回来*,此时控制权交还给正则的},去匹配字符串结束位置,失败,于是回溯,意思就是说前面的.*你吃的太多了,吐一个出来,于是控制权回给.*,吐出一个}(其实是用了前面记录的备选状态,尝试不用.*去匹配'}'),控制权再给正则的},这次匹配就成功了。

正则2的.*?为非贪婪匹配,尽可能少地匹配,所以匹配'233}'的每一个字符的时候,都是尝试不匹配,但是一但控制权交还给最后的}就发现出问题了,赶紧回溯乖乖匹配,于是每一个字符都如此,最终匹配成功。

云里雾里?这就对了!可以移步去下面推荐的博客看看:

想详细了解贪婪和非贪婪匹配原理以及获取更多正则相关原理,除了看书之外,推荐去一个CSDN的博客 雁过无痕-博客频道 - CSDN.NET ,讲解得很详细和透彻

二、语法一览

正则的语法相信许多人已经看过deerchao写的30分钟入门教程,我也是从那篇文字中入门的,deerchao从语法逻辑的角度以.NET正则的标准来讲述了正则语法,而我想重新组织一遍,以便于应用的角度、以JS为宿主语言来重新梳理一遍语法,这将便于我们把语言描述翻译成正则表达式

下面这张一览图(可能需要放大),整理了常用的正则语法,并且将JS不支持的语法特性以红色标注出来了(正文将不会描述这些不支持的特性),语法部分的详细描述也将根据下面的图,从上到下,从左到右的顺序来梳理,尽量不啰嗦。

常用正则语法一览横版有标题

1. 要用某类常见字符——简单元字符

为什么这里要加简单2个字,因为在正则中,\d\w这样的叫元字符,而{n,m}(?!exp)这样的也叫元字符,所以元字符是在正则中有特定意义的标识,而这一小节讲的是简单的一些元字符。

加了u修饰符,会改变一些正则的行为:

3. y修饰符

y修饰符的作用与g修饰符类似,也是全局匹配,开始从位置0开始,后一次匹配都从上一次匹配成功的下一个位置开始。

不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始。

所以/a/y去匹配"ba"会匹配失败,因为y修饰符要求,在剩余位置第一个位置(这里是位置0)开始就要匹配。

ES6对正则的加强,可以看这篇

五、应用正则的实践思路

应用正则,一般是要先想到正则(废话),只要看到和“找”相关的需求并且这个源是可以被字符串化的,就可以想到用正则试试。

一般在应用正则有两类情况,一是验证类问题,另一类是搜索、提取、替换类问题。验证,最常见的如表单验证;搜索,以某些设定的命令加关键词去搜索;提取,从某段文字中提取什么,或者从某个JSON对象中提取什么(因为JSON对象可以字符串化啊);替换,模板引擎中用到。

1. 验证类问题

验证类问题是我们最常遇到的,这个时候其实源字符串长什么样我们是不知道,鬼知道萌萌哒的用户会做出什么邪恶的事情来,推荐的方式是这样的:

  1. 首先用白话描述清楚你要怎样的字符串,描述好了之后,就开脑洞地想用户可能输入什么奇怪的东西,就是自己举例,拿一张纸可举一大堆的,有接受的和不接受的(这个是你知道的),这个过程中可能你会去修改之前的描述;
  2. 把你的描述拆解开来,翻译成正则表达式;
  3. 测试你的正则表达式对你之前举的例子的判断是不是和你预期一致,这里就推荐用在线的JS正则测试去做,不要自己去一遍遍写了。

2. 搜索、提取、替换类问题

这类问题,一般我们是知道源文本的格式或者大致内容的,所以在解决这类问题时一般已经会有一些测试的源数据,我们要从这些源数据中提取出什么、或者替换什么。

  1. 找到这些手上的源数据中你需要的部分;
  2. 观察这些部分的特征,这些部分本身的特征以及这些部分周围的特征,比如这部分前一个符号一定是一个逗号,后一个符号一定是一个冒号,总之就是找规律;
  3. 考察你找的特征,首先能不能确切地标识出你要的部分,不会少也不会多,然后考虑下以后的源数据也是如此么,以后会不会这些特征就没有了;
  4. 组织你对要找的这部分的描述,描述清楚经过你考察的特征;
  5. 翻译成正则表达式;
  6. 测试。
上一篇下一篇

猜你喜欢

热点阅读