正则引擎的分类
正则引擎的分类主要分两种:
DFA:egrep、awk、lex、flex
NFA:.NET、PHP、Perl、Ruby、Python、GNU Emacs、ed、sec、vi、grep等
NFA的历史比DFA久一点,但两种引擎都发展了20多年,产生了很多变体,POSIX的出现就是为了规范这种现象。POSIX不但规定了元字符的特性,而且规定了正则表达式应该用什么样的方式运作。
DFA符合POSIX的标准,但NFA如果要符合POSIX标准,就要作出相应的修改
所以引擎可分为:DFA、传统型NFA、POSIXNFA
测试引擎的类型
工具所采用的引擎的类型,决定了引擎能够支持的特性以及这些特性的用途和使用方式。
传统型NFA是使用最广泛的引擎,而且它很容易识别
法一:看看忽略优先量词是否得到支持,如果是,基本就可以确定是传统型NFA引擎了
法二:用nfa|nfa not来匹配nfa not,如果只有nfa匹配了,就是传统型NFA,如果整个nfa not都匹配了,要么是DFA,要么是POSIX NFA
DFA和POSIX NFA
法一:DFA不支持捕获型括号和回测
法二:用X(.+)+X来匹配形如=XX=================,如果需要花很长时间,就是NFA,如果时间很短就是DFA
Awk、lex、egrep等工具都不支持反向引用和$1功能
匹配基础 规则一
规则1:优先选择最左端的匹配结果
因为匹配先从需要查找的字符串的起始位置尝试匹配,所以起始位置最靠左的匹配结果总是优先于其他可能的匹配结果。
用cat来匹配The dragging belly indicates that your cat is too fat.匹配到的是indicates里面的cat。单词cat是能被匹配出来的,但是indicates中的cat出现得更早,所以得到的匹配是它。
如果引擎不能在字符串的开始位置能找到匹配结果,那么它就会从字符串的下一个位置开始尝试。
用fat|cat|belly|your来匹配The dragging belly indicatees that your cat is too fat.得到的匹配结果是belly
匹配基础 规则二
规则二:标准量词是匹配优先的。
完成复杂的正则表达式,我们需要使用星号、加号、问号等元字符。
从名字可以看出:标准匹配量词的结果“可能”不是所有可能匹配中最长的,但它们总是尝试匹配尽可能多的字符,直到匹配上限为止。
举个例子:用\b\w+s\b匹配包含s的字符串,比如regexes,\w+完全能匹配一整个单词regexes,但是如果\w+匹配了一整个单词,s就无法匹配了。为了完成匹配,\w+只能匹配regexes,把s\b留出来。
用[0-9]+来匹配march 1998中的所有数字,1匹配后,实际上已经满足了成功匹配的下限,但由于正则引擎匹配优先,所有会继续匹配998,直到匹配不到连续数字为止。
过度的匹配优先
匹配优先的特性使得量词匹配尽可能多的结果,但实际上它的工作原理是这样的:量词先匹配尽量多的东西,然后根据正则表达式其余的部分按情况“被迫”“交还”出匹配结果。上面的regexes例子就是这样的。但是交还不能破坏匹配成立的必须条件,比如加号的第一次匹配。
再来看看用^.*([0-9][0-9])匹配about 24 char long的过程:
.匹配了一整个字符串以后,第一个[0-9]要求必须匹配一个数字,所以.被迫交出最后一个字符g,但是这不能让[0-9]匹配,所以.继续交还,重复几次以后,交换的字符是4,这下能够匹配了。但是由于第二个[0-9]也要求匹配一个数字,所以,.又被迫交还一个字符2,两个[0-9]都匹配成功了,所以.*实际上匹配了about。
如果用^.[0-9]+来匹配copyright 2003,那么只有3会被[0-9]+匹配到,因为当有两个量词时,采取先到先得的规则,.被迫交还一个字符就能满足后面的+。所以0不会再被交还。
NFA引擎 表达式主导
我们用to (nite|knight|night)来匹配’tonight’。
正则表达式从t开始,每次检查一个字符,如果能够匹配,则开始匹配下一个字符。当遇到多选结构时,NFA的引擎会把nite、knight、night分别当作三个不同的独立的整体去进行匹配。
如果nite匹配不成功,则尝试knight,如果还不成功,最后再尝试night。
表达式的聚焦点从nite到knight再到night,是有表达式的元素控制的,所以这样的匹配方法叫做表达式主导。
DFA引擎 文本主导
与NFA不同,DFA在扫描字符串时,会记录“当前有效”的所有匹配可能而考察的下一个文本字符只会在“当前有效”的匹配可能中继续匹配,至于那些已经无效的匹配可能,就不去管它们。
Tonight
To (nite|knight|night)
这种方式称为文本主导,因为文本的每一个字符都在控制着引擎的行为。
比较NFA与DFA
一般情况下,文本主导的DFA引擎要快些,因为表达式主导的NFA引擎需要对同样的文本进行不同的子表达式的尝试,例如前面的例子,有三个多选分支,night这个文本就被重复进行匹配了三次,比较浪费时间。
另外,NFA引擎必须把整个正则表达式都经历完(一直到正则表达式的最后末尾),才知道匹配的结果。而DFA引擎的原理允许在匹配到没有“有效”的状态时,就可以知道全局匹配的结果了。
但NFA是表达式主导的,所有用户可以通过修改正则表达式来提高效率,所以讨论NFA引擎是很有趣的一件事。
回溯
NFA最重要的性质是,它会依次处理各个子表达式或组成的元素,遇到需要在两个可能成功的可能匹配中,进行选择时,它会选择其一,同时记住另外一个,以备稍后可能的需要。
需要作出选择的情形下包括量词(决定是否尝试另一次匹配)和多选结构(决定选择哪个多选分支,留下哪个稍后尝试)。
不论选择哪一个途径,如果它能匹配成功,而且正则表达式的余下部分也成功了,匹配即告完成,如果正则表达式中余下的部分最终匹配失败,引擎会知道需要回溯到之前作出选择的地方,选择其他备用的分支继续尝试。这样,引擎最终会尝试表达式的所有可能的途径,准确一点来说,是会尝试到匹配完成之前需要的所有途径。
Tonight
To (nitee|knight|night)
回溯的两个要点
面对众多的选择时,哪个分支应当向首先选择?
多选分支就是从左到右的顺序。
如果需要在“进行尝试”和“逃过尝试”之间选择,对于匹配优先量词,引擎会优先选择“进行尝试”,而对于忽略优先量词,会选择“跳过尝试”。
回溯进行时,应该选取哪个保存的状态?
距离当前最近储存的选项就是当本地失败强制回溯时返回的。其实就是后进先出,这个和计算机的堆结构相像。
如果你在每个岔路都撒一堆面包屑,那么如果前面是死路,你只需要沿原路返回,直到找到一堆面包屑为止。
备用状态
用NFA的术语来说,这些面包屑就是备用状态,他们用来标记:在需要的时候,匹配可以从这里重新开始尝试。他们保存了两个位置:正则表达式中的位置,和未尝试的分支在字符串中的位置。
回溯和匹配优先
在回溯的例子中,我们已经看到了?匹配优先和??忽略优先是怎么工作的了,现在来看看星号和加号。
如果认为x*和x?X?X?X?X?……基本等同,那么分析的过程和刚才就几乎是一模一样的,只不过重复了很多遍而已。
用[0-9]+去匹配a 1234 num,匹配完4以后,此时+号可以回溯的备选状态有四个:
1 234
12 34
123 4
1234
因为[0-9]+相当于[0-9][0-9]?[0-9]?………………
使用忽略优先量词
NFA支持忽略优先量词
?就是与对应的忽略优先量词。
+?就是与+对应的忽略优先量词。
??就是与?对应的忽略优先量词。
用.*?匹配Billionsand millions of
匹配优先、忽略优先和回溯的要旨
无论是匹配优先,还是忽略优先,都是为全局服务的,如果全局需要,这两种优先方式遇到“本地匹配失败”时,引擎都会回归到备选状态(找回面包屑),然后尝试尚未尝试的路径。所以无论是匹配优先还是忽略优先,只要引擎报告匹配失败,他必然已经尝试了所有可能。测试路径的顺序对于两种优先方式是不同的,但只有在测试了所有可能的路径以后,才会最终报告匹配失败。
如果匹配的结果是唯一的(路径只有一条),那么使用匹配优先还是忽略优先都能找到这个唯一的结果,只不过是测试的次序不同而已。
如果存在不止一个匹配结果:
The name “McDonald’s” is said “makudonarudo” in Japense.
.*匹配最长的结果,.*?匹配最短的结果
多选结构
Perl、PHP、Java、.NET以及其他语言使用的NFA引擎,遇到多选结构时,都是按照从左到右的顺序检查表达式的多选分支。如用subject|date,会先使用subject,如果能够匹配,就继续进行下去,不会再管date,如果不匹配,则回溯并且使用date,所以正则引擎会回溯到存在尚未尝试尝试的多选分支的地方。
当需要匹配Jan 31这样的日期时,我们需要的不是简单的Jan [0123][0-9],因为这样有可能会匹配到Jan 00或Jan 39,而且无法匹配Jan 7这样的日期。
一种方法是把日期拆开,用0?[1-9]匹配可能用0开头的前九天的日期,用[12][0-9]处理十号到二十九号,用3[01]处理最后两天。可是我们应该注意多选结构的顺序,如果用Jan (0?[1-9]|[12][0-9]|3[01])来匹配Jan 31,只会得到Jan 3,因为在第一个多选分支可以成功匹配3.所以我们把能够匹配的最短的数字放在最后面,问题就解决了:
Jan ([12][0-9]|3[01]|0?[1-9])
匹配IP地址
匹配一个ip地址,用点号隔开四个数,例如001.002.003.004
如果用[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*
来匹配显然这个表达式不够精致,它甚至可以只匹配三个点…。
第二个想法是把*换成+,这样就能确保有数字了,[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+
,当然这样的正则表达式依然不符合要求,因为他会匹配到1234.5678.91234.3215600564这样的组合。
我们应该限定点号之间只能有1个或2个或3个数字,对于支持区间量词的流派,可以用\d{1,3}\.\d{1,3}\.\d{1,3}
来匹配,对于不支持区间量词的,可以用\d\d?\d?
或者\d(\d?\d)?
来替代。
上面的表达式确实可以匹配ip地址了,但现在我们还要进一步,匹配有效的IP地址,该怎
么办呢?
我们关注字段中什么位置可以出现那些数字。IP地址的三位数是不超过255的,所以如果
一个字段只包含一个或者两个数字,就无需担心这个字段的值是否合法,因为一定是合法
的,这个用\d|\d\d就能应付。同样,我们也不必担心以0或者1开头的三位数,因为000-
199都是合法的,所以现在我们的表达式变成了\d|\d\d|[01]\d\d
。
如果以2开头的三位数字,小于255就是合法的,所以第二位数小于5就代表整个字段合法
,如果第二位是5,则第三位小于6就整个字段合法。这可以表示为2[0-4]\d|25[0-5]
。
现在我们的表达式就是\d|\d\d|[01]\d\d| 2[0-4]\d|25[0-5]
,然后还可以把前面三个分支
简化成[01]?\d\d?| 2[0-4]\d|25[0-5]
。
现在是可以匹配一个精准的IP地址了。
处理文件名
去掉文件名开头的路径:
把/user/local/bin/gcc
变成gcc,我们就可以用.*
匹配优先的特性,使得.*
匹配一整个路径
,然后再加上/,让正则表达式回溯到最后一个斜杠,也就是逼迫.*
交还字符直到遇到最后
一个斜杠。 具体的做法 =~s{^.*/}{}
从路径中获取文件名:
用[^/]*$
来从路径中获取最后的文件名。
可是大家如果完全明代前面我们说的内容,就会发现这个表达式包括了太多回溯。即使是
短短的/user/local/bin/gcc
,在获得匹配结果前,也经历了40多次回溯。
分隔路径和文件名:
用^(.*)/(.*)$
来分隔最后一个斜杠之前的和之后的内容。
如果更加精确一点,可以使用^(.*)/([^/]*)$
匹配对称的括号
为了匹配val=foo(bar(this),3.7)+2*(that-1);
中的(bar(this),3.7)
\(.*\)
\([^)]*\)
\([^()]*\)
第一个匹配的太长,第二个匹配的太短,第三个只能匹配(this)
正则表达式无法匹配任意深度的嵌套结构
但是可以匹配特定深度的嵌套括号
匹配HTML tag
最常见的方法是用<[^>]+>
来匹配HTML标签。这通常都能成功。
但是如果标签中又含有>,例如<input name=“dir”value=“>” >
这样子就匹配不成
功。
熟悉HTML的同学会发现,=号后面必须会出现单引号或者双引号,所以我们只需要把<>
中的内容分成引用根本和非引用文本,就可以完成匹配所有情况的标签。
<(“[^”]”|’[^’]’|[^”’>])*>
匹配HTML link
在HTML中,<a href=“http://www.oreilly.com”>O’Reilly</a>
我们用<a\b([^>]+)>(.*?)</a>
来分别提取链接和链接文本
然后$1=~m{href\s*=\s*(?:”([^”]*)”|’([^’]*)’|([^’”>\s]+))}xi
然后用$+
这个变量,这个变量储存的是$1、$2等数字变量中编号最靠后的变量。
这里就是我们想要的URL。
校验HTTP URL
现在我们得到了URL地址,来看看他是否是HTTP URL,如果是,就把它分解为主机名和
路径两部分。
主机名是^http://
之后和第一个反斜杠(如果有的话)之间的内容,而路径就是除此之外
的内容,而路径就是除此之外的内容:^http://([^/]+)(/.*)?$
URL中有可能包含端口号,它位于主机名和路径之间,以一个冒号开头
^http://([^/:]+)(:(\d+))?(/.*)?$