开心菜鸟笔记系列---正则表达式笔记(入门篇)
一、基础篇:
在支持ASCII码的语言中,如JavaScript,“w”等价于[a-zA-Z0-9_] ; 在支持Unicode的语言中,如.NET,默认情况下,“w”除可以匹配[a-zA-Z0-9_]外,还可以匹配一些Unicode字符集,如汉字,全角数字等等。
在Java中,“w”的表现是比较奇怪的,Java是支持Unicode的,但Java的正则中的“w”却是等价于[a-zA-Z0-9_]的。
基础应用 “”一般应用在需要匹配某一单词字符组成的子串,但这一字符不能包含在同样由单词字符组成的更长的子串中。
比如要替换掉一段英文中的单词“to”,而“today”显然不在替换的范围内,所以正则可以用“to”来限定。
用得比较多的场景是在HTML标签的匹配中,用以区分相互包含的标签,比如要过滤掉<b>、</b>、<p…>、<img…>等标签,但要保留<br />标签,正则可以写成“<(/?b|p|img)[^>]*>”。
二、正则表达式NFA引擎匹配原理:
1)正则引擎大体上可分为不同的两类:DFA和NFA,而NFA又基本上可以分为传统型NFA和POSIX NFA。
DFA: DFA引擎因为不需要回溯,所以匹配快速,但不支持捕获组,所以也就不支持反向引用和$number这种引用方式,目前使用DFA引擎的语言和工具主要有awk、egrep 和 lex。
NFA: POSIX NFA主要指符合POSIX标准的NFA引擎,它的特点主要是提供longest-leftmost匹配,也就是在找到最左侧最长匹配之前,它将继续回溯。同DFA一样,非贪婪模式或者说忽略优先量词对于POSIX NFA同样是没有意义的。
DFA不支持的功能:
捕获组、反向引用和$number引用方式;
环视(Lookaround,(?<=…)、(?<!…)、(?=…)、(?!…)),或者有的有文章叫做预搜索;
忽略优化量词(??、*?、+?、{m,n}?、{m,}?),或者有的文章叫做非贪婪模式;
占有优先量词(?+、*+、++、{m,n}+、{m,}+,目前仅Java和PCRE支持),固化分组(?>…)。
2)占有字符:
正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;
3)、零宽度: 如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是零宽度的。
占有字符是互斥的,也就是一个字符,同一时间只能由一个子表达式匹配。 零宽度是非互斥的,而一个位置,却可以同时由多个零宽度的子表达式匹配。
4)、 控制权和传动
正则的匹配过程,通常情况下都是由一个子表达式(可能为一个普通字符、元字符或元字符序列组成)取得控制权,从字符串的某一位置开始尝试匹配,
一个子表达式开始尝试匹配的位置,是从前一子表达匹配成功的结束位置开始的。
如正则表达式: (子表达式一)(子表达式二) 假设(子表达式一)为零宽度表达式,由于它匹配开始和结束的位置是同一个,如位置0,那么(子表达式二)是从位置0开始尝试匹配的。 假设(子表达式一)为占有字符的表达式,由于它匹配开始和结束的位置不是同一个,如匹配成功开始于位置0,结束于位置2,那么(子表达式二)是从位置2开始尝试匹配的。 而对于整个表达式来说,通常是由字符串位置0开始尝试匹配的。如果在位置0开始的尝试,匹配到字符串某一位置时整个表达式匹配失败,那么引擎会使正则向前传动,整个表达式从位置1开始重新尝试匹配,依此类推,直到报告匹配成功或尝试到最后一个位置后报告匹配失败。
5)、量词‘?’:
两种情况:
字符串:abc 正则:ab?c
a:?量词匹配成功:
由a开始获取控制权,开始匹配a,匹配成功,交出控制权 b?,?量词会优先匹配,b?匹配b成功,控制权交给c,同时记下备选状态,c去匹配c,匹配成功。 此时正则表达式匹配完成,报告匹配成功。 字符串:ac 正则:ab?c
b:?量词匹配失败:
由a开始获取控制权,开始匹配a,匹配成功,交出控制权b?,?量词会优先匹配,b?,,同时记下备选状,匹配c,匹配失败,此时进行回溯,找到备选状态,“b?”忽略匹配 控制权交给C,c去匹配,匹配成功,备选状态丢失。 此时正则表达式匹配完成,报告匹配成功。
c、量词??:
源字符串:abc
正则表达式:ab??c
量词“??”属于忽略优先量词,在可匹配可不匹配时,会先选择不匹配,只有这种选择会使整个表达式无法匹配成功时,才会尝试进行匹配。这里的量词“??”是用来修饰字符“b”的,所以“b??”是一个整体。 匹配过程: 首先由字符“a”取得控制权,从位置0开始匹配,由“a”来匹配“a”,匹配成功,控制权交给字符“b??”;先尝试忽略匹配,即“b??”不进行匹配,同时记录一个备选状态,控制权交给“c”;由“c”来匹配“b”,匹配失败,此时进行回溯,找到记录的备选状态,“b??”尝试匹配,即“b??”来匹配“b”,匹配成功,把控制权交给“c”;由“c”来匹配“c”,匹配成功。 此时正则表达式匹配完成,报告匹配成功。匹配结果为“abc”,开始位置为0,结束位置为3。其中“b??”匹配字 零宽带匹配: 源字符串:a12 正则表达式:^(?=[a-z])[a-z0-9]+$ 元字符“^”和“$”匹配的只是位置,顺序环视“(?=[a-z])”只进行匹配,并不占有字符,也不将匹配的内容保存到最终的匹配结果,所以都是零宽度的。 这个正则的意义就是匹配由字母或数字组成的,第一个字符是字母的字符串。
d、匹配过程:
首先由元字符“^”取得控制权,从位置0开始匹配,“^”匹配的就是开始位置“位置0”,匹配成功,控制权交给顺序环视“(?=[a-z])”; “(?=[a-z])”要求它所在位置右侧必须是字母才能匹配成功,零宽度的子表达式之间是不互斥的,即同一个位置可以同时由多个零宽度子表达式匹配,所以它也是从位置0尝试进行匹配,位置0的右侧是字符“a”,符合要求,匹配成功,控制权交给“[a-z0-9]+”; 因为“(?=[a-z])”只进行匹配,并不将匹配到的内容保存到最后结果,并且“(?=[a-z])”匹配成功的位置是位置0,所以“[a-z0-9]+”也是从位置0开始尝试匹配的,“[a-z0-9]+”首先尝试匹配“a”,匹配成功,继续尝试匹配,可以成功匹配接下来的“1”和“2”,此时已经匹配到位置3,位置3的右侧已没有字符,这时会把控制权交给“$”; 元字符“$”从位置3开始尝试匹配,它匹配的是结束位置,也就是“位置3”,匹配成功。 此时正则表达式匹配完成,报告匹配成功。匹配结果为“a12”,开始位置为0,结束位置为3。其中“^”匹配位置0,“(?=[a-z])”匹配位置0,“[a-z0-9]+”匹配字符串“a12”,“$”匹配位置3。
三 、环视表达式:
匹配的结果不保存到最终的匹配结果,叫零宽带断言。 环视匹配的的最终结果就是一个位置
表达式 说明
(?<=Expression) 逆序肯定环视,表示所在位置左侧能够匹配Expression
(?<!Expression) 逆序否定环视,表示所在位置左侧不能匹配Expression
(?=Expression) 顺序肯定环视,表示所在位置右侧能够匹配Expression
(?!Expression) 顺序否定环视,表示所在位置右侧不能匹配Expression
一、顺序环视:
对于顺序肯定环视(?=Expression)来说,当子表达式Expression匹配成功时,(?=Expression)匹配成功,并报告(?=Expression)匹配当前位置成功。 对于顺序否定环视(?!Expression)来说,当子表达式Expression匹配成功时,(?!Expression)匹配失败;当子表达式Expression匹配失败时,(?!Expression)匹配成功,并报告(?!Expression)匹配当前位置成功; 顺序环视相当于在当前位置右侧附加一个条件,所以它的匹配尝试是从当前位置开始的,然后向右尝试匹配,直到某一位置使得匹配成功或失败为止。 顺序环视尝试匹配的起点是确定的,就是当前位置,而匹配的终点是不确定的。
二、逆序环视:
对于逆序肯定环视(?<=Expression)来说,当子表达式Expression匹配成功时,(?<=Expression)匹配成功,并报告(?<=Expression)匹配当前位置成功。 对于逆序否定环视(?<!Expression)来说,当子表达式Expression匹配成功时,(?<!Expression)匹配失败;当子表达式Expression匹配失败时,(?<!Expression)匹配成功,并报告(?<!Expression)匹配当前位置成功;
而逆序环视的特殊处在于,它相当于在当前位置左侧附加一个条件,所以它不是在当前位置开始尝试匹配的,而是从当前位置左侧某一位置开始,匹配到当前位置为止,报告匹配成功或失败。 逆序环视匹配的起点是不确定的,是当前位置左侧某一位置,而匹配的终点是确定的,就是当前位置。 长度确定时,引擎可以向左查找固定长度的位置作为起点开始尝试匹配,而如果长度不确定时,就要从当前位置向左逐个位置开始尝试匹配,不成功则回溯,再向左侧位置进行尝试匹配,然后重复以上过程,直到匹配成功,或是尝试到位置0处以后,报告匹配失败, “{m}”没有放在匹配优先量词中,同样的,“{m}?”虽然被部分语言所支持,但是也没有放在忽略优先量词中, 主要是因为这两种量词,实现的效果是一样的,只有被修饰的子表达式匹配m次才能匹配成功,且没有可供回溯的状态, 所以也不存在是匹配优先还是忽略优先的问题,也就不在本文的讨论范围内。事实上即使讨论也没有意义的,只要知道它们的匹配行为也就是了
例子: (?s)/*(?<=/*).*?(?=*/)*/: 匹配注释
四、 贪婪和非贪婪:
正则表达式:<div>.*</div>
字符串:aa<div>test1</div>bb<div>test2</div>cc
贪婪与非贪婪模式影响的是被量词修饰的子表达式的匹配行为,贪婪模式在整个表达式匹配成功的前提下,尽可能多的匹配,而非贪婪模式在整个表达式匹配成功的前提下,尽可能少的匹配。非贪婪模式只被部分NFA引擎所支持。
正则表达式:<div>.*</div>
字符串:aa<div>test1</div>bb<div>test2</div>cc
被忽略优先量词修饰的子表达式使用的就是非贪婪模式
正则表达式:
<div>.*</div>bb 贪婪
<div>.*?</div>cc 非贪婪
字符串:aa<div>test1</div>bb<div>test2</div>cc
总结:
贪婪模式才真正的影响着子表达式的匹配行为,如果整个表达式匹配失败,贪婪模式只会影响匹配过程,对匹配结果的影响无从谈起。
非贪婪模式才真正的影响着子表达式的匹配行为,如果整个表达式匹配失败,非贪婪模式无法影响子表达式的匹配行为。
在一情况,贪婪是效率更高,而有的情况非贪婪效率是高于,那么平时写正则,即要考虑到正则的准确性,又要考虑效率,毕竟正则效率很低。
"[^"]"这样效率就高的多了. 它们的应用场景通常是不同的,所以效率上一般不具有可比性。
贪婪模式还有一点优势,就是在匹配失败时,贪婪模式可以更快速的报告失败,从而提升匹配效率。
以上讨论的是匹配成功的演进过程,而对于一个正则表达式,在匹配失败的情况下,如果能够以最快的速度报告匹配失败,也会提升匹配效率,这或许是我们设计正则过程中最容易忽略的。 一般来说是非贪婪模式效率高些,而对于数量较大源字符串,或是复杂的正则表达式,一般来说是贪婪模式效率高些。
五、 捕获组:
捕获组就是把正则表达式中子表达式匹配的内容,保存到内存中以数字编号或显式命名的组里, 方便后面引用。当然,这种引用既可以是在正则表达式内部,也可以是在正则表达式外部。 普通捕获组:(Expression) 都支持 命名捕获组:(?<name>Expression) 命名捕获组目前只有.NET、PHP、Python等部分语言支持。
另外需要说明的一点是,除(Expression)和(?<name>Expression)语法外,其它的(?...)语法都不是捕获组。
一、普通捕获组: 组的编号是按照“(”出现的顺序,从左到右,从1开始进行编号的 。
二、再按命名捕获组中“(”出现的先后顺序,从左到右
三、混合方式的捕获组编号:
首先按照普通捕获组中“(”出现的先后顺序,从左到右,从1开始进行编号,当普通捕获组编号完成后, 再按命名捕获组中“(”出现的先后顺序,从左到右,接着普通捕获组的编号值继续进行编号。
对捕获组的引用一般有以下几种:
1) 正则表达式中,对前面捕获组捕获的内容进行引用,称为反向引用;
2) 正则表达式中,(?(name)yes|no)的条件判断结构;
3) 在程序中,对捕获组捕获内容的引用。
2013-10-19
交流 QQ群:130719662