指尖上的正则表达式–入门篇
1) 历史和起源
正则表达式的“鼻祖”或许可一直追溯到科学家对人类神经系统工作原理的早期研究。美国新泽西州的Warren McCulloch和出生在美国底特律的Walter Pitts这两位神经生理方面的科学家,研究出了一种用数学方式来描述神经网络的新方法,他们创新地将神经系统中的神经元描述成了小而简单的自动控制元,从而作出了一项伟大的工作革新。
在1956年,出生在被马克·吐温(Mark Twain)称为“美国最美丽的城市之一的”哈特福德市的一位名叫Stephen Kleene的数学科学家,他在Warren McCulloch和Walter Pitts早期工作的基础之上,发表了一篇题目是《神经网事件的表示法》的论文,利用称之为正则集合的数学符号来描述此模型,引入了正则表达式的概念。正则表达式被作为用来描述其称之为“正则集的代数”的一种表达式,因而采用了“正则表达式”这个术语。
之后一段时间,人们发现可以将这一工作成果应用于其他方面。Ken Thompson就把这一成果应用于计算搜索算法的一些早期研究,Ken Thompson是Unix的主要发明人,也就是大名鼎鼎的Unix之父。Unix之父将此符号系统引入编辑器QED,然后是Unix上的编辑器ed,并最终引入grep。Jeffrey Friedl在其著作“Mastering Regular Expressions (2nd edition)”中对此作了进一步阐述讲解,如果你希望更多了解正则表达式理论和历史,推荐你看看这本书。
自此以后,正则表达式被广泛地应用到各种UNIX或类似于UNIX的工具中,如大家熟知的Perl。Perl的正则表达式源自于Henry Spencer编写的regex,之后已演化成了pcre(Perl兼容正则表达式Perl Compatible Regular Expressions),pcre是一个由Philip Hazel开发的、为很多现代工具所使用的库。正则表达式的第一个实用应用程序即为Unix中的qed编辑器。
然后,正则表达式在各种计算机语言或各种应用领域得到了广大的应用和发展,演变成为目前计算机技术森林中的一只形神美丽且声音动听的百灵鸟。
以上是关于正则表达式的起源和发展的历史描述,到目前正则表达式在基于文本的编辑器和搜索工具中依然占据这一个非常重要的地位。
在最近的六十年中,正则表达式逐渐从模糊而深奥的数学概念,发展成为在计算机各类工具和软件包应用中的主要功能。不仅仅众多UNIX工具支持正则表达式,近二十年来,在 Windows 的阵营下,正则表达式的思想和应用在大部分 Windows 开发者工具包中得到支持和嵌入应用!从正则式在 Microsoft Visual Basic 6 或 Microsoft VBScript 到 .NET Framework 中的探索和发展,Windows系列产品对正则表达式的支持发展到无与伦比的高度,目前几乎所有 Microsoft 开发者和所有.NET语言都可以使用正则表达式。如果你是一位接触计算机语言的工作者,那么你会在主流操作系统(*nix[Linux, Unix等]、Windows、HP、BeOS等)、目前主流的开发语言(PHP、C#、Java、C++、VB、Javascript、Ruby以及Python等)、数以亿万计的各种应用软件中,都可以看到正则表达式优美的舞姿。(摘自《百度百科–正则表达式》)
2) 正则表达式的定义
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
给定一个正则表达式和另一个字符串,我们可以达到如下的目的:1. 给定的字符串是否符合正则表达式的过滤逻辑(称作“匹配”);2. 可以通过正则表达式,从字符串中获取我们想要的特定部分。
正则表达式的特点是:1. 灵活性、逻辑性和功能性非常的强;2. 可以迅速地用极简单的方式达到字符串的复杂控制。3. 对于刚接触的人来说,比较晦涩难懂。
由于正则表达式主要应用对象是文本,因此它在各种文本编辑器场合都有应用,小到著名编辑器EditPlus,大到Microsoft Word、Visual Studio等大型编辑器,都可以使用正则表达式来处理文本内容。
3) 正则表达式的语法
正则表达式由一些普通字符和一些元字符组成,普通字符就是我们平时常见的字符串、数字之类的,当然也包括一些常见的符号,等等。而元字符则可以理解为正则表达式引擎的保留字符,就像很多计算机语言中的保留字符一样,他们在正则引擎中有特殊的意义,下面将介绍JavaScript中和正则相关的元字符以及API。(不同的计算机语言,对正则引擎的实现不是完全一致的,所以,有些元字符和组合方式在JavaScript中不存在)
我们将按照下面的分类将元字符一一列出:
3. 1 字面量字符( Literal Characters )
字符 | 描述 | 描述 |
---|---|---|
f | 换页符 | (u000C) |
n | 换行符 | (u000A) |
r | 回车 | (u000D) |
o | NUL字符 | (u0000) |
t | 制表符 | (u0009) |
v | 垂直制表符 | (u000B) |
xnn | 由十六进制数nn指定的拉丁字符 | x0A等价于n |
uxxxx | 由十六进行xxxx指定的Unicode字符 | u0009等价与t |
cX | 控制字符(X的值必须是A-Z或a-z) | cJ等价于换行符n |
3. 2 字符类( Character Classes )
字符 | 描述 | 示例 |
---|---|---|
[xyz] | 匹配位于括号内的任意字符 | [abc]匹配’plain’中的a |
[^xyz] | 匹配不在括号之中的任意字符 | [^abc]匹配’plain’中的p |
w | 等价于[a-zA-Z0-9_] | w匹配’sina’中的s |
W | 等价于[^a-zA-Z0-9_] | w不能匹配’sina’ |
s | 任何Unicode空白符 | [ fnrtv] |
S | 任何非空白字符 | [^ fnrtv] |
d | 等价于[0-9] | d匹配’sina123′中的1 |
D | 等价于[^0-9] | D不能匹配’sina1′中的1 |
[b] | 退格直接量(特例) | |
除换行符和其它Unicode换行符之外的任意字符,[sS]匹配任意字符 a.c 匹配 “abc”, “a1c”, and “a-c”. |
3. 3 重复( Repetition )
字符 | 描述 | 示例 |
{n,m} | 匹配至少n次,但不超过m次,n和m必须是非负整数,且n<=m
? 等价于 {0,1} |
[abc]匹配’plain’中的a |
{n,} | 匹配至少n次 | o{2,} 不匹配’Bob’中的’o',但匹配’food’中的 o. |
{n} | 恰好匹配n次 | o{2} 不匹配’Bob’中的’o',但匹配’food’中的o. |
? | 匹配0次或1次,等价于{0,1} | zo? 匹配 "z" and "zo", 但不匹配"zoo". |
+ | 匹配1次或多次,等价于{1,} | zo+ 匹配 "zo" and "zoo", 但不匹配 "z". |
* | 匹配0次或多次,等价于{0,} | zo* 匹配 "z" 和 "zoo". |
3. 4 非贪婪的重复(non-greedy)
字符 | 描述 | 示例 | |
*?
+? ?? {n}? {n,}? {n,m}? |
在重复字符后加上问号,匹配模式就是非贪婪的匹配. 这种模式会尽可能少的对目标字符串进行匹配 |
o+? 匹配"oooo"中的一个"o", o+ 则匹配所有的 "o".
/a+?b/.exec(‘aaab’) |
3. 5 选择、分组和引用
字符 | 描述 | 示例 |
---|---|---|
(选择)匹配该符号左边或右边的子表达式 |
(zf)ood匹配zood或food | |
(pattern) |
(分组)将几个项目组合成一个单元,这个单元可由*,+,?和等符号使用,而且还可记住和这个组合匹配的字符以供后面的引用使用 |
(AB) [1-9] 匹配 "A5", 字母A被保存,可通过n或RegExp的$1-$9引用该值 |
(?:pattern) | 只组合,不记忆 |
ai(?:rR) 等价于airaiR |
n | 和第n个分组第一次匹配的字符串匹配 |
3. 6 指定匹配位置
字符 | 描述 | 示例 |
^ |
匹配字符串的开头,在多行检索中,匹配一行的开头 |
^food&匹配以f开头,d结尾的food,而不会匹配’eatfood222′中的’food’ |
$ |
匹配字符串的结尾,在多行检索中,匹配一行的结尾 |
同上 |
b | 匹配一个单词的边界 |
erb 匹配’never’中的’er’,但不匹配’verb’中的’er’. |
B | 匹配非单词边界 | |
(?=pattern) | 正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用 | “Windows(?=9598NT2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始 |
(?!pattern) | 正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用 | “Windows(?!9598NT2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始 |
3. 7 标志
字符 | 描述 |
i | 忽略大小写 |
g | 执行全局匹配,即找到所有的匹配,而不是在找到第一个匹配后停止 |
m |
多行模式,^匹配一行的开头和字符串的开头,$匹配一行的结尾或字符串的结尾 |
4) JavaScript中的正则API
上一部分,我们了解了正则表达式的语法,那么,如何在JavaScript中构造一个正则表达式的对象呢?有以下两种方法:
1.字面量表示法,例如:var reg = /d+/i; 就表示一个正则表达式的实例,这个写法需要注意的地方是:正则表达式的主体部分,也就是示例中的 d+ ,必须位于“ / / ”之间,而标志位则需要跟在结束 “/” 的后面。
2.实例化RegExp对象,例如: var reg = new RegExp( “d+”, “igm” ); 第一个参数是表达式的主体,而第二个参数则是标志位,此参数可选。
上面两种方法可以得到正则表达式的实例,通常,我们都使用第一种方式,即字面量的方式,这种方式比较直观,第二种方式通常用来组合表达式,比如,通过用户输入的某个值,来构造表达式,例如:var input = ‘this is a test’; var reg = new RegExp( “dw” + input,); 通过这种方式,我们可以组合成新的表达式,来达到我们的目的。在使用这种方式时,需要注意对特殊字符的转义和过滤,以免遭受恶意攻击。
JavaScript中,除了正则表达式对象外,字符串也和正则表达式息息相关。可以说,RegExp和String是密不可分的,缺一不可。所以,和正则相关的API就是RegExp对象和String对象的一些API。
首先是RegExp对象,该对象的实例中,有三个方法和正则表达式相关,分别是test( ),exec( )和compile( )。而String对象中和正则相关的方法则包括:match( ), replace( ), search( ), split( ), 接下来会对这些方法做详细介绍。
4. 1 exec( )
exec( ) 方法检索字符串中的指定值。返回值是被找到的值。如果没有发现匹配,则返回 null。
例子1:
var patt1 = new RegExp("e"); // var patt1 = /e/; document.write( patt1.exec("The best things in life are free") ); //output : e
从上面的例子中可以看出,exec( )方法接收一个参数,类型为字符串,可以简单的理解为:我是一个正则表达式实例,请给我一个字符串来测测吧,于是,我们满足了它的愿望,把目标字符串丢给它。
如果匹配成功,该方法的返回结果是一个数组[array],该数组的第一个元素则是匹配的字符串,后面的元素则是表达式分组所捕获到的值,分组为1,则是该数组中的第一个元素,依次类推。若没有分组,该数组只包含一个元素。当然,该数组还有另外3个属性,input,index和lastIndex。若没有匹配成功,则返回null。
input属性表示目标字符串。
index属性表示匹配到的位置。
lastIndex属性表示上一次匹配后的位置。
另外,需要注意的是,若表达式实例中包含了全局(g)标志位,则会从 lastIndex 所标记的位置开始查找,而不会从字符串的开始位置进行。所以,我们可以多次调用exec( )方法,比如:
function RegExpTest() { var src = "The quick brown fox jumps over the lazy dog."; // Create regular expression pattern with a global flag. var re = /w+/g; // Get the next word, starting at the position of lastindex. var arr; while ((arr = re.exec(src)) != null) { // New line: document.write ("<br />"); document.write (arr.index + "-" + arr.lastIndex + " "); document.write (arr[0]); } }
详细描述请参考:
http://msdn.microsoft.com/en-us/library/z908hy33(v=vs.94).aspx
4. 2 test( )
test( ) 方法比 exec( ) 方法简单,该方法只会返回一个Boolean值,如果匹配成功,则返回true,否则,返回false。可参考下面的示例:
function TestDemo(re, teststring) { // Test string for existence of regular expression. var found = re.test(teststring) // Format the output. var s = ""; s += "'" + teststring + "'" if (found) s += " contains "; else s += " does not contain "; s += "'" + re.source + "'" return(s); }
详细描述请参考:
http://msdn.microsoft.com/en-us/library/a55e5s6b(v=vs.94).aspx
4. 3 match( )
需要注意的是,该方法的参数,也就是正则表达式添加了全局( g )标志位时的变化。如果没有添加全局标志位,返回结果和exec( )方法的返回值一样,但不包含所有的属性。
详细描述请参考:
http://msdn.microsoft.com/en-us/library/7df7sf4x(v=vs.94).aspx
4. 4 replace( )
该方法是字符串中非常强有力的方法,它接收两个参数,第一个参数为表达式,也可以直接传入字符串,该方法在内部会自动将字符串转换为正则表达式;第二个参数是想要替换成的字符串,该参数可以为一个函数,但是该函数的返回值必须为字符串。具体用法请参考
http://msdn.microsoft.com/en-us/library/t0kbytzc(v=vs.94).aspx
4. 5 split( )
该方法可以按照传递的参数对字符串进行分割,会返回一个数组,里面包含了分割后的元素。一般用法是直接传入字符串进行分割,当然,也可以传入正则表达式,按表达式分割,详细信息请参考:
http://msdn.microsoft.com/en-us/library/t5az126b(v=vs.94).aspx
4. 6其它
compile( ),search( )等方法,在日常使用中较少使用,大致了解一下即可。
5) 实例解析
大家可以参考【正则表达式经典实例】,请参考附件。
小结
以上主要讲解了正则表达式的基本语法和常用API,对于如何构造复杂的表达式和表达式的匹配原理,本文并未涉及。说实话,正则表达式本身确实晦涩难懂,比较拗口,大家需在平时,多多温习这些基础的语法和API。
另外,可以用一些工具来辅助我们写出严谨的正则表达式,比如:RegexBuddy或者一些在线的应用。当然,还有浏览器的控制台,可以在里面很方便的进行测试。
在日常的开发中,千万不要在正则上钻牛角尖,因为我们可以利用其它API,达到同样的目的。而且,太过复杂的表达式,效率可能较低,而且,对于以后的维护人员来说,也是非常不方便维护的。
但千万不要浅尝辄止,应该继续深挖正则中的思想,正是这些基础的思想,才能帮我我们在编程上更上层楼。
参考信息
http://msdn.microsoft.com/en-us/library/z908hy33(v=vs.94).aspx
exec()
http://msdn.microsoft.com/en-us/library/a55e5s6b(v=vs.94).aspx
test()
http://msdn.microsoft.com/en-us/library/7df7sf4x(v=vs.94).aspx
match()
http://msdn.microsoft.com/en-us/library/t0kbytzc(v=vs.94).aspx
replace()