1、第六章 正则表达式正则表达式是对字符串的结构进行的形式化描述,非常简洁优美,而且功能十分强大。很多的语言都不同程度的支持正则表达式,而在很多的文本编辑器如 Emacs,vim,UE 中,都支持正则表达式来进行字符串的搜索替换工作。UNIX 下的很多命令行程序,如 awk,grep,find 更是对正则表达式有良好的支持。JavaScript 同样也对正则表达式有很好的支持,RegExp 是 JavaScript 中的内置“类”,通过使用 RegExp,用户可以自己定义模式来对字符串进行匹配。而 JavaScript 中的 String 对象的 replace 方法也支持使用正则表达式对串进行匹
2、配,一旦匹配,还可以通过调用预设的回调函数来进行替换。正则表达式的用途十分广泛,比如在客户端的 JavaScript 环境中的用户输入验证,判断用户输入的身份证号码是否合法,邮件地址是否合法等。另外,正则表达式可用于查找替换工作,首先应该关注的是正则表达式的基本概念。关于正则表达式的完整内容完全是另外一个主题了,事实上,已经有很多本专著来解释这个主题,限于篇幅,我们在这里只关注 JavaScript 中的正则表达式对象。6.1 正则表达式基础概念本节讨论正则表达式中的基本概念,这些基本概念在很多的正则表达式实现中是一致的,当然,细节方面可能会有所不同,毕竟正则表达式是来源于数学定义的,而不是程
3、序员。JavaScriipt 的正则表达式对象实现了 perl 正则表达式规范的一个子集,如果你对 perl 比较熟悉的话,可以跳过这个小节。脚本语言 perl 的正则表达式规范是目前广泛采用的一个规范,Java 中的 regex 包就是一个很好的例子,另外,如 vim 这样的应用程序中,也采用了该规范。6.1.1 元字符与特殊字符元字符,是一些数学符号,在正则表达式中有特定的含义,而不仅仅表示其“字面”上的含义,比如星号(*),表示一个集合的零到多次重复,而问号(?)表示零次或一次。如果你需要使用元字符的字面意义,则需要转义。下面是一张元字符的表:元字符 含义 串的开始$ 串的结束* 零到多
4、次匹配+ 一到多次匹配? 零或一次匹配b 单词边界特殊字符,主要是指注入空格,制表符,其他进制(十进制之外的编码方式)等,它们的特点是以转义字符()为前导。如果需要引用这些特殊字符的字面意义,同样需要转义。下面为转移字符的一张表:字符 含义字符本身 匹配字符本身r 匹配回车n 匹配换行t 制表符f 换页x# 匹配十六进制数cX 匹配控制字符6.1.2 范围及重复我们经常会遇到要描述一个范围的例子,比如,从 0 到 3 的数字,所有的英文字母,包含数字,英文字母以及下划线等等,正则表达式规定了如何表示范围:标志符 含义 在集合中的任一个字符 不在集合中的任一个字符. 出n 之外的任一个字符w 所
5、有的单字,包括字母,数字及下划线W 不包括所有的单字,w 的补集s 所有的空白字符,包括空格,制表符S 所有的非空白字符d 所有的数字D 所有的非数字b 退格字符结合元字符和范围,我们可以定义出很强大的模式来,比如,一个简化版的匹配 Email 的正则表达是为:Js 代码 1. var emailval = /w-+(.w-+)*w-+(.w-+)+$/; 2. 3. emailval.test(““);/true 4. emailval.test(“john.abruzzipl.kunming.china“);/true 5. emailval.test(““);/false,不合法 w-表
6、示所有的字符,数字,下划线及减号,w-+表示这个集合最少重复一次,然后紧接着的这个括号表示一个分组(分组的概念参看下一节),这个分组的修饰符为星号(*),表示重复零或多次。这样就可以匹配任意字母,数字,下划线及中划线的集合,且至少重复一次。而符号之后的部分与前半部分唯一不同的是,后边的一个分组的修饰符为(+),表示至少重复一次,那就意味着后半部分至少会有一个点号(.),而且点号之后至少有一个字符。这个修饰主要是用来限制输入串中必须包含域名。最后,脱字符()和美元符号($)限制,以开始,且以结束。这样,整个表达式的意义就很明显了。再来看一个例子:在 C/Java 中,变量命名的规则为:以字母或下
7、划线开头,变量中可以包含数字,字母以及下划线(有可能还会规定长度,我们在下一节讨论)。这个规则描述成正则表达式即为下列的定义:Js 代码 1. var variable = /a-zA-Z_a-zA-Z0-9_*/; 2. 3. print(variable.test(“hello“); 4. print(variable.test(“world“); 5. print(variable.test(“_main_“); 6. print(variable.test(“0871“); 将会打印:truetruetruefalse前三个测试字符均为合法,而最后一个是数字开头,因此为非法。通过加推导
8、(+),星推导(*),以及谓词,我们可以灵活的对范围进行重复,但是我们仍然需要一种机制来提供诸如 4 位数字,最多 10 个字符等这样的精确的重复方式。这就需要用到下表中的标记:标记 含义n 重复 n 次n, 重复 n 或更多次n,m 重复至少 n 次,至多 m 次有了精确的重复方式,我们就可以来表达如身份证号码,电话号码这样的表达式,而不用担心出做,比如:Js 代码 1. var pid = /d15|d18/;/身份证 2. var mphone = /d11/;/手机号码 3. var phone = /d3,4-d7,8/;/电话号码 4. 5. mphone.test(“138939
9、39392“);/true 6. phone.test(“010-99392333“);/true 7. phone.test(“0771-3993923“);/true 6.1.3 分组与引用在正则表达式中,括号是一个比较特殊的操作符,它可以有三中作用,这三种都是比较常见的:第一种情况,括号用来将子表达式标记起来,以区别于其他表达式,比如很多的命令行程序都提供帮助命令,键入 h 和键入 help 的意义是一样的,那么就会有这样的表达式:h(elp)?/字符 h 之后的 elp 可有可无这里的括号仅仅为了将 elp 自表达式与整个表达是隔离(因为 h 是必选的)。第二种情况,括号用来分组,当正
10、则表达式执行完成之后,与之匹配的文本将会按照规则填入各个分组,比如,某个数据库的主键是这样的格式:四个字符表示省份,然后是四个数字表示区号,然后是两位字符表示区县,如yunn0871cg 表示云南省昆明市呈贡县(当然,看起来的确很怪,只是举个例子),我们关心的是区号和区县的两位字符代码,怎么分离出来呢?Js 代码 1. var pattern = /w4(d4)(w2)/; 2. var result = pattern.exec(“yunn0871cg“); 3. print(“city code = “+result1+“, county code = “+result2); 4. res
11、ult = pattern.exec(“shax0917cc“); 5. print(“city code = “+result1+“, county code = “+result2);正则表达式的 exec 方法会返回一个数组(如果匹配成功的话),数组的第一个元素(下标为 0)表示整个串,第一个元素为第一个分组,第二个元素为第二个分组,以此类推。因此上例的执行结果即为:写道city code = 0871, county code = cgcity code = 0917, county code = cc第三种情况,括号用来对引用起辅助作用,即在同一个表达式中,后边的式子可以引用前边匹配
12、的文本,我们来看一个非常常见的例子:我们在设计一个新的语言,这个语言中有字符串类型的数据,与其他的程序设计语言并无二致,比如:Js 代码 1. var str = “hello, world“; 2. var str = fair enough; 均为合法字符,我们可能会设计出这样的表达式来匹配该声明:Js 代码 1. var pattern = /“*“/; 看来没有什么问题,但是如果用户输入:Js 代码 1. var str = hello, world“; 2. var str = “hello, world; 我们的正则表达式还是可以匹配,注意这两个字符串两侧的引号不匹配!我们需要的是
13、,前边是单引号,则后边同样是单引号,反之亦然。因此,我们需要知道 前边匹配的到底是“单”还是“双”。这里就需要用到引用,JavaScript中的引用使用斜杠加数字来表示,如1 表示第一个分组(括号中的规则匹配的文本),2 表示第二个分组,以此类推。因此我们就设计出了这样的表达式:Js 代码 1. var pattern = /(“)“*1/; 在我们新设计的这个语言中,为了某种原因,在单引号中我们不允许出现双引号,同样,在双引号中也不允许出现单引号,我们可以稍作修改即可完成:Js 代码 1. var pattern = /(“)1*1/; 这样,我们的语言中对于字符串的处理就完善了。6.2 使
14、用正则表达式创建一个正则表达式有两种方式,一种是借助 RegExp 对象来创建,另一种方式是使用正则表达式字面量来创建。在 JavaScript 内部的其他对象中,也有对正则表达式的支持,比如 String 对象的 replace,match 等。我们可以分别来看:6.2.1 创建正则表达式使用字面量:Js 代码 1. var regex = /pattern/; 使用 RegExp 对象:Js 代码 1. var regex = new RegExp(“pattern“, switchs); 而正则表达式的一般形式描述为:Js 代码 1. var regex = /pattern/switc
15、hs; 这里的开关(switchs)有以下三种:修饰符 描述i 忽略大小写开关g 全局搜索开关m 多行搜索开关(重定义与$的意义)比如,/java/i 就可以匹配 java/Java/JAVA,而/java/则不可。而 g 开关用来匹配整个串中所有出现的子模式,如/java/g 匹配”javascript 2. print(pattern.test(“javanjavascript“);/false 3. pattern = /javascript/m; 4. print(pattern.test(“javanjavascript“);/true RegExp 对象的方法:方法名 描述test
16、() 测试模式是否匹配exec() 对串进行匹配compile() 编译正则表达式RegExp 对象的 test 方法用于检测字符串中是否具有匹配的模式,而不关心匹配的结果,通常用于测试,如上边提到的例子:Js 代码 1. var variable = /a-zA-Z_a-zA-Z0-9_*/; 2. 3. print(variable.test(“hello“);/true 4. print(variable.test(“world“);/true 5. print(variable.test(“_main_“);/true 6. print(variable.test(“0871“);/f
17、alse 而 exec 则通过匹配,返回需要分组的信息,在分组及引用小节中我们已经做过讨论,而 compile 方法用来改变表达式的模式,这个过程与重新声明一个正则表达式对象的作用相同,在此不作深入讨论。6.2.2 String 中的正则表达式除了正则表达式对象及字面量外,String 对象中也有多个方法支持正则表达式操作,我们来通过例子讨论这些方法:方法 作用match 匹配正则表达式,返回匹配数组replace 替换split 分割search 查找,返回首次发现的位置Js 代码 1. var str = “life is very much like a mirror.“; 2. var
18、 result = str.match(/is|a/g); 3. print(result);/返回“is”, “a” 这个例子通过 String 的 match 来匹配 str 对象,得到返回值为“is”, “a”的一个数组。Js 代码 1. var str = “Welcome, John“; 2. var result = str.replace(/span/g, “div“); 3. print(str); 4. print(result); 得到结果:Html 代码 1. Welcome, John 2. Welcome, John 也就是说,replace 方法不会影响原始字符串,
19、而将新的串作为返回值。如果我们在替换过程中,需要对匹配的组进行引用(正如之前的1,2 方式那样),需要怎么做呢?还是上边这个例子,我们要在替换的过程中,将 Welcome 和John 两个单词调换顺序,编程 John, Welcome:Js 代码 1. var result = str.replace(/(w+),s(w+)/g, “$2, $1“); 2. print(result); 可以得到这样的结果:Html 代码 1. John, Welcome 因此,我们可以通过$n 来对第 n 个分组进行引用。Js 代码 1. var str = “john : tomorrow :remove
20、:file“; 2. var result = str.split(/s*:s*/); 3. print(str); 4. print(result); 得到结果:john : tomorrow :remove:filejohn,tomorrow,remove,file注意此处 split 方法的返回值 result 是一个数组。其中包含了 4 个元素。Js 代码 1. var str = “Tomorrow is another day“; 2. var index = str.search(/another/); 3. print(index);/12 search 方法会返回查找到的文本
21、在模式中的位置,如果查找不到,返回-1。6.3 实例:JSFilter本小节提供一个实例,用以展示在实际应用中正则表达式的用途,当然,一个例子不可能涵盖所有的内容,只是一个最常见的场景。考虑这样一种情况,我们在 UI 上为用户提供一种快速搜索的能力,使得随着用户的键入,结果集不断的减少,直到用户找到自己需要的关键字对应的栏目。在这个过程中,用户可以选择是否区分大小写,是否全词匹配,以及高亮一个记录中的所有匹配。显然,正则表达式可以满足这个需求,我们在这个例子中忽略掉诸如高亮,刷新结果集等部分,来看看正则表达式在实际中的应用:图 1 在列表中使用 JSFilter(结果集随用户输入而变化)来看一
22、个代码片段:Js 代码 1. this.content.each(function() 2. var text = $(this).text(); 3. var pattern = new RegExp(keyword, reopts); 4. if(pattern.test(text) 5. var item = text.replace(pattern, function(t) 6. return “+t+“; 7. ); 8. $(this).html(item).show(); 9. else/clear previous search result 10. $(this).find(
23、“span.“+filterOptions.highlight).each(function() 11. $(this).replaceWith($(this).text(); 12. ); 13. 14.); 其中,content 是结果集,是一个集合,其中的每一个项目都可能包含用户输入的关键字,keyword 是用户输入的关键字序列,而 reopts 为正则表达式的选项,可能为(i,g,m),each 是 jQuery 中的遍历集合的方式,非常方便。程序的流程是这样的:进入循环,取得结果集中的一个值作为当前值使用正则表达式对象的 test 方法进行测试如果测试通过,则高亮标注记录中的关键字否则跳过,进行下一条的检测遍历完所有的结果集,生成了一个新的,高亮标注的结果集,然后将其呈现给用户。而且可以很好的适应用户的需求,比如是否忽略大小写检查,是否高亮所有,是否全词匹配,如果自行编写程序进行分析,则需要耗费极大的时间和精力。图 2 在表格中使用 JSFilter(不减少结果集)这个例子来源于一个实际的项目,我对其进行了适度的简化,完整的代码可以参考附件。