最近这段时间帮同窗处理一些文档, 涉及到一些结构化文档的工做大部分都得使用正则表达式, 以前对于正则的认识大多来源于语言书上那几页的介绍, 本身也没有用过几回。这里将我以前感到模糊的概念做个整理。由于对JS了解多点,因此也将JS中相关的正则特性概括下。注意本文将正则与JS中的正则分开讨论。html
正则表达式的解释引擎只有两种,字符驱动(text-directed)和正则驱动(regex-directed),基于这两种解释引擎算法的不一样,它们有时也被称为DFA(Deterministic finite automaton 肯定型有穷自动机)与NFA(Non-deterministic finite automaton非肯定型有穷自动机),当前大部分现代语言的正则解释器采用“正则驱动”引擎,这是由于NFA运行的回溯算法能够实现诸如‘惰性匹配(lazy quantifiers )’和‘后部引用(backreferences)’ 等很是有用的特性, 而DFA算法也就是字符驱动型引擎并不支持这些特性,固然复杂的算法必须付出性能的代价,字符驱动引擎的性能要强于正则驱动。各类语言中因为实现正则引擎的具体算法以及调用的正则库都有不一样,因此对于正则的支持也不一样,关于这些概念的参考mark在这里:
?Regex Engine Internals
?正则表达式匹配解析过程探讨分析(正则表达式匹配原理)正则表达式
零宽断言(zero-length assertions)这概念直译过于术语化(都有点像咒语了,囧rz), 逻辑不是太复杂,可是容易混淆模糊,在此记个笔记。零宽的意思是指该位置是不占宽度的,也就是只做断言判断,但不匹配实际的内容,好比\d(?=\.)
这个正向先行断言(这概念也概括在后)就只匹配点号以前的数字,可是它并不会匹配到这个点号,这个\d(?=\.)
括号中的匹配内容也就是零宽了。
零宽断言分为四种,分别是:正向先行(Positive Lookahead),正向回顾(Positive Lookbehind),负向先行(Negative Lookahead),负向回顾(Negative Lookbehind)。正向和负向的意思是断言括号中的内容是匹配仍是不匹配,好比上面栗子?中的(?=...)
就是正向先行断言的写法,该断言括号中的内容必须出现, 而负向也就是断言括号中的内容不能出现,负向先行的写法是这样:(?!...)
。 先行与回顾的意思是实际匹配的内容在断言内容的前面仍是后面,如下将逐一罗列这四种概念:算法
直接地说“零宽正向先行断言”所匹配的就是必须出现的断言内容的前面的内容,以前的点号前数字的栗子?\d(?=\.)
已经比较直观的展现了这个概念,可是须要注意的是:这里的先行或者回顾是基于位置,而不是字符!也就是说从点号的位置开始向前匹配, 好比咱们把这个正则表达式改为这样:(?=\.).
注意这里并无把实际要匹配的内容放在断言括号的前面,而是放到了后面, 这就匹配了从这个点号位置开始的任意字符(除换行符外),也就匹配到了这个点号, 固然一般这样写没什么意义, 但便于理解位置的含义。express
理解了前面这个“零宽正向先行断言”,随之而来的就比较好理解了。依照这个逻辑,零宽正向回顾断言所匹配的是必须出现的断言内容以后的内容,它的语法是:(?<=...)
好比(?<=\.)\w
所匹配的就是点号以后的ASCII字符。segmentfault
负向与正向意思相反, 正向是断言内容必须出现,而负向则是断言内容必须不出现。好比JavaScript and Java
这句话中使用Java(?!Script)
就只会匹配到Java
,由于该正则断言Java
后面不能出现Script
数组
正负向与先行回顾的概念都已在前面列出, 负向回顾的理解应该就很顺了。负向回顾的语法是:(?<!...)
。 好比(?<!Java)Script
该正则只匹配不是JavaScript
的Script
,它就能正确匹配ECMAScript
和Script
。浏览器
这里须要注意:在大多数语言中,先行断言中能够有任意的正则语法,好比能够用
+
或者*
这种不肯定重复的匹配,可是回顾断言中的限制就比较多,不少语言的回顾断言中不支持诸如+
和*
这种不定重复的匹配和后部引用,由于正则解释引擎在匹配回顾断言的时候,当回顾断言中的内容匹配成功后再去匹配断言后的实际内容时,它须要能计算出后退多少步才能判断出当前匹配是否与这个正则彻底匹配。有些语言压根就不支持回顾断言,好比JS;而 PHP, Delphi, R和 Ruby只支持在回顾断言中加入选择匹配,但选择匹配的内容长度必须相同,形如[ab|ba|cd];Java回顾断言限制很少,但没法在回顾断言中加入不定数量的重复,也就是不能在回顾断言中使用+
和*
; .Net支持在回顾断言中使用任意正则语法。(以上内容参考自下面的第一个mark参考中,可能存在时效的问题, 若有错误还请大神指出。)函数
正则零宽断言更多参考mark:
?Lookahead and Lookbehind Zero-Length Assertions性能
各类语言对于正则不一样支持参考:
?Comparison of regular expression enginesgoogle
经过设置正则表达式后的修饰符(flag)可开启对应的匹配模式:s
(单行模式)和m
(多行模式)。单行模式与多行模式这两名字容易让人误觉得二者是互相排斥的, 也就是误觉得开启了多行模式,那么就不是单行模式,而实际上单行模式或多行模式的开关并不会影响另外一个,两个模式能够同时用。
开关单行模式影响的是元字符.
的匹配。单行模式开启时,元字符.
匹配包括换行符\n
在内的任意字符,单行模式关闭时,元字符.
匹配不包括换行符\n
的任意字符。
多行模式影响的是元字符^
和$
。多行模式开启时,元字符^
能够匹配字符串开头(字符串的开始位置),也能够匹配行的开头(即换行符\n
以后的位置),元字符$
能够匹配字符串结尾(字符串的结束位置), 也能够匹配行的结尾(即换行符\n
以前的位置)。多行模式关闭时,元字符^
和$
只能匹配字符串的开头和结尾,不能匹配换行符\n
的以前和以后。也就是说若是没有这两个元字符,即便正则后加多行模式修饰符m
也无心义,多行模式的正则必须有这两个元字符中的一个或两个才有意义。
概念参考mark: ?正则表达式的多行模式与单行模式
附一个JS描述的多行模式:?multiline 属性(正则表达式)(JavaScript)
注意:JS中正则并不支持单行模式的开启
JS中的正则须要注意下正则对象的方法与String对象方法的微妙区别。
每一个正则对象实例有这几项属性global
(是否带有修饰符g的布尔值) ignoreCase
(是否带有修饰符i的布尔值) lastIndex
(对象方法匹配开始的位置索引,初始为0) multiline
(是否带有修饰符m的布尔值) source
(正则源码文本,注意源码文本不包括修饰符)。
ES6中RegExp对象实例新增的只读属性
sticky
能够判断正则后是否带修饰符y
(新增),设置y
修饰符的正则每次匹配,存在lastIndex
属性状况下lastIndex
不会自动归零,而且在正则表达式中隐式地加入了开头元字符^
,这样就使得正则表达式的匹配锚定在lastIndex
的位置。这种用法的详情可参考MDN文档的栗子?:?RegExp.prototype.sticky 以及?JavaScript:正则表达式的/y标识
JS中每一个RegExp对象实例有两个方法,分别是:exec()
与test()
。这两个方法运行逻辑几乎等价,可是exec
要复杂些,匹配失败返回null,匹配成功它将返回一个数组, 数组第一个元素是匹配整个正则的内容,以后的元素是正则中捕获组的匹配(注:使用非捕获组能够得到微弱的性能优点)举个栗子:
//捕获组() var pattern1 = /(www)\.(baidu)\.(com)/i //非捕获组(?:) var pattern2 = /(?:www)\.(?:baidu)\.(?:com)/i var str = 'www.baidu.com' pattern1.exec(str) //返回数组["www.baidu.com", "www", "baidu", "com"] pattern2.exec(str) //返回数组["www.baidu.com"]
注意:虽然exec()
和String的match()
方法返回的都是数组的实例,可是他们的返回值都有两个额外的属性:index
和input
分别表示匹配项在字符串中的位置和应用正则的整段字符串。
而test()
方法更为简单,匹配成功返回true,失败返回false。这里须要注意下这两个方法中的lastIndex
属性与正则表达式中的全局修饰符g
的关系。
当正则中存在全局修饰符g
时,RegExp对象的lastIndex
属性将保存下一次开始的匹配的位置,好比:
var pattern = /(www)\.(baidu)\.(com)/g var str2 = 'www.baidu.com www.google.com www.baidu.com' pattern.exec(str2) pattern.lastIndex //13
这意味着下次调用exec
方法它将从被匹配字符串索引13的位置开始匹配(也能够手动设置lastIndex的值)。test()
方法同理。正由于存在残留的lastIndex,因此使用正则对象的方法可能会致使一些意外的结果,不过在ES5中,正则表达式直接量的每次计算都会建立一个RegExp对象实例,他们各自拥有lastIndex,这就大大下降了意外的几率。
可是String对象中支持正则的方法却与正则对象的方法不一样,String方法忽略lastIndex,可是,若是存在全局修饰符g
,string方法将有些许不一样。
String方法中支持正则的有match()
,replace()
, search()
,split()
这四个方法。match()
方法接受一个字符串或正则做为参数,当参数是正则时, 正则是否带全局修饰符g
将影响该方法的返回值,该方法匹配不带全局修饰符的正则时,与RegExp对象实例方法exec()
的返回值同样, 匹配带全局修饰符的正则时,match()
方法将返回全局中所匹配到的全部内容而再也不将正则中的捕获组编码。好比前面的str2
若是使用String的match
方法的话其返回的数组中将是["www.baidu.com","www.baidu.com"]
。
replace(searchValue,replaceValue)
方法一样接受字符串或正则做为第一个参数,当搜索值是字符串的时候,它只会匹配一次,不少时候这并不是咱们要的结果, 通常咱们但愿全局匹配,因而可使用带全局修饰符的正则表达式。replace()
的替换值中的美圆符号$
有特殊含义,好比$1
表示正则表达式中第一个捕获组的匹配内容。它的意义取自RegExp
构造函数的属性(正则表达式每次的操做都会影响构造函数RegExp
的属性,该构造函数属性的详细参考能够看下《JS高级程序设计》中文第三版108页中的说明)下面借用《JS权威指南》核心参考中的栗子。
var name = 'Doe,John' name.replace(/(\w+)*,(\w+)/,'$2 $1')//$num表明捕获组的编号 "John Doe"
美圆符号的特殊含义概括:
$$ 替换对象$
$& 整个匹配的文本
$number 分组捕获的文本(从1开始,不是0哦)
$` 匹配以前的文本
$' 匹配以后的文本
search()
方法与String中indexOf
相似,接受一个正则或者字符串为参数,匹配成功返回第一个匹配的首字符位置, 失败则返回-1。
split(separator,howmany)
方法接受一个必选的分割位置做为第一个参数,第二个参数设置返回数组的最大长度。第一个参数能够是字符串或正则表达式,若是该参数是包含捕获组(带圆括号带子表达式)的正则表达式,那么返回的数组中包括与这些子表达式匹配的字串(但不包括与整个正则表达式匹配的文本)
举个例子?:
'hello world'.split(/(\w)\s/) //返回["hell", "o", "world"]
能够看到捕获组中的匹配("o")也在返回的数组中,可是整个正则的匹配被做为分隔位置。并不是全部浏览器都支持split()
这个特性。
MDN文档中并未说明该特性更多细节:?String.prototype.split()
这里也顺便转贴一下《JS高级程序设计》第三版中提到的JS(ES5)正则的局限性:
匹配字符串开始的结尾的A和Z锚(但支持以^和$来匹配字符串的开始的结尾)
向后查找(但支持向前查找)
并集和交集类
原子组
Unicode支持(单个字符除外)
命名的捕获组(但支持编号的捕获组)
s(single单行)和x(free-spacing无间隔)匹配模式
条件匹配
正则表达式注释
以上所提到的局限性中,可以使用正则库或多或少的抵消一些,好比XRegExp 调用该库以后就可使JS的正则支持可命名空间的捕获组以及注释等有用的特性, 关于JS的正则库XRegExp,我写了篇大体的介绍:?JavaScript正则库:XRegExp