精通正则表达式(元字符)
13年1月初在公司开了一次正则表达式的讲座,在这里希望把那一个月努力专研的一些东西分享一下,先分享一本大家熟悉的书《精通正则表达式》,的确是好书,不过要不是工作原因,我才不会一个月内看了两遍呢!实在是枯燥的很啊!不过最近发现的确对自己很有帮助,还可以时常在工作中要到,有时候也能帮群里解决一些问题。我这里算是把那本书精简了一下的版本吧!没书上的详细,不过结合了网上一些资料加上自己的试验。
1、历史起源
正则表达式的“鼻祖”或许可一直追溯到科学家对人类神经系统工作原理的早期研究。美国新泽西州的Warren McCulloch和出生在美国底特律的Walter Pitts这两位神经生理方面的科学家,研究出了一种用数学方式来描述神经网络的新方法,他们创新地将神经系统中的神经元描述成了小而简单的自动控制元,从而作出了一项伟大的工作革新。
在1956 年,美国的一位名叫Stephen Kleene的数学科学家,他在Warren McCulloch和Walter Pitts早期工作的基础之上,发表了一篇题目是《神经网事件的表示法》的论文,利用称之为正则集合的数学符号来描述此模型,引入了正则表达式的概念。正则表达式被作为用来描述其称之为“正则集的代数”的一种表达式,因而采用了“正则表达式”这个术语。
之后一段时间,人们发现可以将这一工作成果应用于其他方面。Ken Thompson就把这一成果应用于计算搜索算法的一些早期研究,Ken Thompson是 Unix的主要发明人,也就是大名鼎鼎的Unix之父。Unix之父将此符号系统引入编辑器QED,然后是Unix上的编辑器ed,并最终引入grep。
Unix的grep家族包括grep、egrep和fgrep。egrep和fgrep的命令只跟grep有很小不同。egrep是grep的扩展,支持更多的元字符,fgrep就是把所有的字母都看作单词,也就是说,正则表达式中的元字符表示回其自身的字面意义,不再特殊。
众多UNIX工具支持正则表达式,近二十年来,在WINDOW的阵营下,正则表达式的思想和应用在大部分 Windows 开发者工具包中得到支持和嵌入应用!目前主流的开发语言(PHP、C#、Java、C++、VB、Javascript、Ruby以及python等)、数以亿万计的各种应用软件中,都可以看到正则表达式优美的舞姿。
2、Egrep元字符
文本检索式正则表达式最简单的应用之一——许多文本编辑器和文字处理软件都提供了正则表达式检索的功能。最简单也是最流行的就是egrep。几乎所有流派(在这里我把不同的语言或处理软件称作流派)支持的元字符都是根据egrep进行的扩展或者修改。
2.1Egrep元字符^$
脱字符号‘^’和美元符号‘$’分别代表了一行的开始和结束。
例如:`^cat`代表需要匹配以cat开头的一行。希望大家按照字符来理解正则表达式的习惯:不要将`^cat`理解为“匹配以cat开头的行”,而是理解为“匹配的是以c作为一行的第一个字符,紧接一个a,紧接一个t的文本。
(注:后面的我都会把正则表达式用``括起来)
如上两种理解并无差异,但按照字符来解读更易于明白正则表达式的内部逻辑。脱字符号和美元符号的特别之处在于,它们匹配的是一个位置,而不是具体的文本。egrep会如何解释`^cat$`、`^$`和单个的`^`呢?
`^cat$` 文字意义:匹配的条件是,行开头(显然,每一行都有开头 ),然后是字母cat,然后是行末尾。
应用意义:只包含cat的行——没有多余的单词、空白字符……只有cat。
`^$` 文字意义:匹配的条件是,行开头,然后就是行末尾。
应用意义:空行(没有任何字符,包括空白字符)。
`^` 文字意义:匹配条件是行的开头。
应用意义:无意义!因为每一行都有开头,所以每一行都能匹配——空行也不例外。
例子1:
var reg=/^[abc][abc][0123456789]/; var result1=reg.exec("ca4at"); var result2=reg.exec("1catg"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/cat$/; var result1=reg.exec("cat"); var result2=reg.exec("catg"); document.write(result1+" "+result2);
结果:
例子3:
var reg=/^cat$/; var result1=reg.exec("cat"); var result2=reg.exec("catcat"); document.write(result1+" "+result2);
结果:
2.2Egrep元字符[]-
如果我们需要搜索的单词是“grey”,同时又不能确定它是否写作“gray”,就可以使用正则表达式结构体‘[]’,也称作字符组。所以我们可以写作`gr[ae]y`,这里需要注意:字符组只要满足一个字符就可以匹配成功;字符组里面的顺序一般情况下可以随意。
如果我们想匹配1到6中的任意数字,我们可以写作`[123456]`,我们也可以通过字符组元字符‘-’(连字符)表示,也就是`[1-6]`,同样的效果。`[0-9]`和`[a-z]`是常用的匹配数字和小写字母的简便方式。多重范围也是允许的,如`[0-9a-zA-F]`。我们还可以随心所欲的把字符范围与普通文本结合起来:`[0-9A-Z_!.?]`能够匹配一个数字、大写字母、下划线、惊叹号、点号或者是问号。
请注意,只有在字符组内部,连字符才可能是元字符——否则它就只能匹配普通的连字符。这里用可能是因为如果连字符出现在字符组的开头,它代表的就只是匹配一个普通连字符,而不是一个范围。同样的道理,问号和点号通常被当做元字符处理,但在字符组中则不是如此。如`[-a-z]`就是匹配一个连字符或者小写字母。
例子1:
var reg=/gr[ae]y/; var result1=reg.exec("grygrey"); var result2=reg.exec("grygraey"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/^[abc][abc][0123456789]/; var result1=reg.exec("ca4at"); var result2=reg.exec("1catg"); document.write(result1+" "+result2);
结果:
例子3:
var reg=/^[a-z]/; var result1=reg.exec("c24at"); var result2=reg.exec("1catg"); document.write(result1+" "+result2);
结果:
例子4:
var reg=/^[0-9a-z_]$/; var result1=reg.exec("_"); var result2=reg.exec("catcat"); document.write(result1+" "+result2);
结果:
例子5:
var reg=/^[-!][0-9][a-z]/; var result1=reg.exec("-7fff"); var result2=reg.exec("!75fff"); document.write(result1+" "+result2);
结果:
2.3Egrep元字符[^]
用“[^]”取代“[]”,这个字符组就会匹配任何未列出的字符,我们称作排除型字符组。我们注意到这里的‘^’和我们表示首行的脱字符是一样的,字符确实相同,但意义截然不同,在字符组内部必须是紧接在字符组的第一个方括号之后,它才表示一个元字符。
如果我们用`q[^u]`来匹配字符串伊拉克“Iraq”是否会成功?这里需要强调一下一个字符组,即使是排除型字符组,也需要匹配一个字符。上面的匹配伊拉克字符串会失败,因为q后面没有字符,而`[^u]`必须匹配一个字符,无论是空格、换行符或者其他单词,都必须至少有一个。
例子1:
var reg1=/[^a-z]/; var result1=reg1.exec("a24at"); var result2=reg1.exec("catg"); document.write(result1+" "+result2);
结果:
例子2:
var reg1=/^[0-9a-z_^]$/; var reg2=/^[^^0-9]/; var result1=reg1.exec("^"); var result2=reg1.exec("catcat"); var result3=reg2.exec("-7fff"); var result4=reg2.exec("^75fff"); document.write(result1+" "+result2+" "+result3+" "+result4);
结果:
例子3:
var reg=/q[^0-9]/; var result1=reg.exec("a2qat"); var result2=reg.exec("Iraq"); document.write(result1+" "+result2);
结果:
2.4Egrep元字符.
元字符‘.’是用来匹配任意字符(除换行以外)的字符组的简便写法。如果我们需要在表达式中使用一个“匹配任何字符”的占位符,用点号就很方便。例如:我们需要搜索2013/01/04、2013-01-04或者2013.01.04,我们可以使用`2013.01.04`或者`2013[-./]01[-./]04[-./]`都可以,但是哪种好呢?
在`2013[-./]01[-./]04[-./]`中的点号不是元字符,因为它在字符组内部(记住在字符组里面和外面,元字符的定义和意义是不一样的)。这里的连字符同样也不是字符,因为它们都紧接在[或者[^之后。点号是元字符时能够匹配任意字符,所以也可以匹配如“2013301504”的字符串,所以`2013[-./]01[-./]04[-./]`更加精确,但是更难读,也更难写。 `2013.01.04`更容易理解,但是不够细致。
例子:
var reg=/2013.01.04/; var result1=reg.exec("reg2013-01-04"); var result2=reg.exec("2013-1-04"); document.write(result1+" "+result2);
结果:
2.5Egrep元字符| ()
‘|’是一个非常简捷的元字符,它的意思是“或”。回头来看`gr[ea]y`的例子,我们还可以写作`grey|gray`或者是`gr(e|a)y`。后者用括号来划定多选结构的范围(正常情况下括号也是元字符),对于表达式`gr(e|a)y`来说括号是必须的,不然就变成了`gre|ay`,而这个代表匹配gre或者ay。
`Jeffrey|Jeffery`、`Jeff(rey|ery)`和`Jeff(re|er)y`三个表达式是等价的。gr[ea]y`与`gr(e|a)y`的例子可能会让人觉得多选结构与字符组没太大区别,但是请不要混淆这两个的概念。一个字符组只能匹配目标文本中的单个字符,而每个多选结构自身可能是完整的正则表达式,都可以匹配任意长度的文本。
例子1:
var reg=/grey|gray/; var result1=reg.exec("reggray"); var result2=reg.exec("greay"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/(ca)t/; var result1=reg.exec("ttcatty"); var result2=reg.exec("ctat"); document.write(result1+" "+result2);
结果:
例子3:
var reg=/gr(e|a)y/; var result1=reg.exec("sagrey"); var result2=reg.exec("greay"); document.write(result1+" "+result2);
结果:
2.6Egrep元字符?+*
现在来看color和colour的匹配。他们区别在后面的单词比前面的多一个u,我们可以使用`colou?r`来解决这个问题。元字符‘?’代表可选项。再来看`four(th)?`,这里‘?’作用的元素是整个括号,括号内的表达式可以任意复杂,但是“从括号外来看”它们是一个整体。
‘+’(加号)和‘*’(星号)的作用于问号类似。元字符‘+’表示“之前紧邻的元素出现一次或多次”,而‘*’表示“之前紧邻的元素出现任意多次,或者不出现”。问号、加号和星号这三个元字符统称为量词,因为它们限定了所作用元素的重现次数。
接下来看类似<HR·SIZE=14>这样的HTML tag,它表示一条高度为14像素的穿越屏幕的水平线。在最后的尖括号之前可能出现任意多个空格,此外在等号两边也容易出现任意多个空格,最后在HR和SIZE之间必须至少一个空格。所以我们得到`<HR·+SIZE·*=·*14·*>`。如果我们找的tag的14这个数字希望是任意的,那么可以使用`<HR·+SIZE·*=·*[0-9]+·*>`。
(注:后面都使用’·’代表一个空格)
如果我们希望SIZE也是可有可无的呢?改成`<HR(·+SIZE·*=·*[0-9]+)?·*>`就可以达到效果。总结:问号、星号和加号:
例子1:
var reg=/colou?r/; var result1=reg.exec("color"); var result2=reg.exec("colouur"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/four(th)?/; var result1=reg.exec("fourth"); var result2=reg.exec("fourt"); var result3=reg.exec("fuorth"); document.write(result1+" "+result2+""+ result3);
结果:
例子3:
var reg=/ab+/; var result1=reg.exec("abbbc"); var result2=reg.exec("abdb"); document.write(result1+" "+result2);
结果:
例子4:
var reg=/(abc*)/; var result1=reg.exec("abcccdd"); var result2=reg.exec("abdb"); var result3=reg.exec("acc") document.write(result1+" "+result2+" "+result3);
结果:
例子5:
var reg=/(ab *f)/; var result1=reg.exec("ab fdd"); var result2=reg.exec("abfdb"); var result3=reg.exec("acc") document.write(result1+" "+result2+" "+result3);
结果:
2.7Egrep元字符
某些版本的egrep支持使用元字符序列来自定义重现次数的区间“{min,max}”这称为“区间量词”。有人就使用`[a-zA-Z]{1,5}`来匹配美国的股票代码(1到5个字母),问号对应的区间量词是“{0,1}”。JavaScript也支持这种形式。
例子1:
var reg=/abc{3}/; var result1=reg.exec("abcccccdd"); var result2=reg.exec("abccdb"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/abc{1,}/; var result1=reg.exec("abcccdd"); var result2=reg.exec("abdb"); document.write(result1+" "+result2);
结果:
例子3:
var reg=/abc{0,1}/; var result1=reg.exec("abcccdd"); var result2=reg.exec("abdb"); var result3=reg.exec("acc") document.write(result1+" "+result2+" "+result3);
结果:
2.8Egrep元字符\1
目前为止我们已经见过括号可以限制多选的范围、将若干字符组合为一个单元。括号还有一个很有用途的功能——记忆文本。括号记下来的文本都是存在\n里面的,这里的\n不是换行符,n是一个具体的数字,如:`([a-z])([0-9])\1\2`中\1记录的就是一个小写字母,\2记录的就是一个一位的数字。
例子:
var reg=/a(b)c\1/; var result1=reg.exec("abcbccccdd"); var result2=reg.exec("accdb"); document.write(result1+" "+result2);
结果:
2.9Egrep元字符(?:)
此为非捕获型小括号,只用于分隔字符,组成单元,不用于记录,由于`()`会记录内部匹配的信息,所以需要占用部分内存,当用户不需要记录此匹配的信息时可以使用`(?:)`,例如`ab(?:c)d(ef)g\1`需要匹配的是字符串…abcdefgef…,而不是…abcdefgc…。
例子:
var reg=/ab(?:c)d(ef)g\1/; var result1=reg.exec("11abcdefgef22"); var result2=reg.exec("abcdefgc"); document.write(result1+" "+result2);
结果:
2.10Egrep元字符\
如果我们需要匹配的某个字符本身就是元字符,我们就需要使用‘\’。比如我们之前提到过的匹配2013.01.04就需要写作`2013\.01\.04`。如果反斜线后紧跟的不是元字符,那么就需要依流派、版本来定了,例如在某些egrep的版本里面使用`\<`和`\>`来单词的左右边界符,在javascript中使用的是`\b`,这两种流派同样都支持`\1`这种形式。
例子:
var reg=/\([a-zA-Z]+\)/; var result1=reg.exec("tt(cat)ty"); var result2=reg.exec("c(tat"); document.write(result1+" "+result2);
结果:
到此基本介绍了常用的简单的元字符,不过在每一种语言里面元字符都不一样,需要读者自己注意。
其实这些都是最基础的正则表达式的使用,只能算是入门,主要是给刚接触正则表达式的朋友了解的。后面再细讲正则表达式的核心——正则引擎。最终用来提高我们的表达式的效率。