第⑧章正则表达式
在前面几个章节中,我们都不得不在字符串值中寻找一些模式。第3章中,我们通过编写能够找到日期中各个数字的准确位置的代码,从字符串里提取了日期值;第5章中,我们看到了一些特别不美观的代码,这些代码用于在字符串里查找特定类型的字符,例如,在HTML输出的时候必须要转义的字符。
正则表达式是一种在字符串数据里表示模式的语言。它们组成了一种小型且独立的语言,嵌在 JavaScript里(其他编程语言也是如此)。这种语言非常简洁,尽管其可读性不是很强——大型正则表达式往往就像卡通人物的咒语一样。总之,它们是一种很强大的工具,能够真正简化字符串处理程序。
8.1语法
与将字符串写在引号里的方式相同,正则表达式写在斜杠(/)之间。字符串的search方法 与indexOf类似,indexOf返回找到参数的位置,但search的参数是一个正则表达式而非字符串;
"doubledare" .search (/le/);
→4
由于斜杠通常表明了正则表达式的结束,表达式里的斜杠则必须通过反斜杠进行转义(但不包括引号)。例如,我们定义一个正则表达式,它只包含一个斜杠;
var slash = /\//;
"AC/DC", search (slash);
→2
正则表达式指定的模式可以完成一些字符串不能完成的工作。例如’它们允许一些元素匹配 多个单字符。
8.1.1匹配字符集
在第5章中,当从文档里提取标记时,我们需要在字符串里査找第一个星号或者左大括号可以使用正则表达式来完成这一工作,如下:
var asteriskOrBrace = /[\{\*]/;
var story = "We noticed the *giant sloth*, hanging from a giant branch.";
story. search (asteriskOrBrace);
→15
在正则表达式里,中括号“[”和“]”有着特殊的意义。它包含一个字符列表,只要某一个 个字符被找到即可被匹配上。大多数标点字符在正则表达式里都有特殊的意义,因此,它们来表示实际字符的话,最好用反斜杠(在这种情况下,反斜杠并不是十分必要,因为[]里的字符应用了不同的规划。而现在,仅对其进行转义变得更加简单,因此不需要考虑太多。)对其转义。
有些字符在正则表达式里指的是整个字符集。点(.)用来表示“任何不是换行的字符”;
转义d (\d)表示“任意数字”;转义w (\w)匹配任何单词(也就是任何英文、数字和下划线组 成的字符)。转义s(\s)匹配任何空字符(例如Tab、换行和空格)。
var digitSurroundedBySpace = /\s\d\s/;
"la 2 3d". search(digitSurroundedBySpace);
→3
可以用大写字符替换相应的\d、\w、和\s来反转正则表达式的意义。例如,\S匹配任何不是空字符串的字符。使用中括号[和]时,反转模式可以以^字符开始:
var notABC = /[^ABC]/;
"ABCBACCBBAOABC".search ( notABC );
→10
根据我们现在所知道的,可以编写一个匹配XX/XX/XXXX格式日期的正则表达(这里的X是数字),第3章中介绍过这样的字符:born 15/11/2003 (mother Spot): White Fang
var datePattern = /\d\d\/\d\d\/\d\d\d\d/;
"born 15/11/2003 (mother Spot): White Fang". search (datePattern);
→5
大量的反斜杠让表达式变得很难读懂。它表示:数字、数字、斜线、数字、数字、斜线、数字、数字、数字、数字。我们马上就能明白如何使用类似的表达式来真正从字符串里提取日期。
8.1.2匹配单词和字符边界
有时需要确认一个模式以特定字符开始或者以特定字符结束。可以使用特殊字符^和$完成,^字符匹配字符的开头,而$字符匹配字符的结尾:
/a/.test("blah");
→ true
/^a$/.test("blah");
→false
第一个正则表达式匹配任何包含字符a的字符串,而第二个只能匹配字符串“s”, 需要注意 正则表达式是对象,并且拥有方法。它的test方法返回布尔值,表示给定的字符串是否匹配表达式
转义字符\b)可以匹配“单词边界” ——它可以是标点符号、空格、字符的开头或结尾:/cat/.test("concatenate");
→true
/\bcat\b/.test("concatenate");
→false
8.1.3重复模式
在正则表达式里,重复表示子模式也是可以的。在元素后面跟一个星号(*)表示可以重复匹配任意次数(包括零次);元素后面跟一个加号(+)要求至少要匹配一次;元素后面跟一个问 号(?)表示该元素可选(它可能出现零次或一次)。
var parenthethicText = /\(.*\)/;
"Its (the sloth's) claws were gigantic!".search(parenthethicText);
→4
在必要的时候,可以使用花括号来指定一个元素可能发生的次数。花括号里只有1个数字 (例如{4})确定了元素必须出现的次数。花括号之间有两个数字,并且两个数字中间有逗号 ({3,10})时,表示元素至少要出现第一个数字的次数(这里是3次),而且最多只能出现第二 个数字的次数(这里是10次);类似地,{2,}表示要出现两次或两次以上,而{,4}则表示出现次数要少于四次。
下面是一个更灵活的匹配日期的模式:
var datePattern = /\d{l,2}\/\d\d?\/\d{4}/;
"born 15/11/2003 (mother Spot): White Fang".search(datePattern);
→5
表达式/\d{1,2}/和/\d\d?/是同一件事的两种表示方式:“一个或两个数字”。
8.1.4子表达式分组
通常需要在同一个表达式里,对多个字符使用像*或+这样的特殊字符。可以使用小括号对表达式的一部分进行分组,然后用整个分组进行匹配。例如:
var cartoonCrying = /boo(hoo+)+/i;
cartoonCrying. test ("Boohoooohoohooo");
→true
模式hoo+允许h后面加两个或多个0字符。(hoo+)+则允许该模式整体被重复一次或多次。 需要注意正则表达式结尾的i字符。在关闭斜杠后面,可以添加可选项。这里的i表示该表达式不区分大小写,允许小写字母b匹配字符串里的大写字符B,在本章的后面,我们将了解另外一个可选项,作为“全局”的g。
8.1.5多选一
对于更髙级的“分支”模式,可以使用竖线(丨)表示允许檳式在多个元素中选择一个。示例如下:
var holyCow = /\b(sacred|holy) (cow|bovine|bull|taurus)\b/i;
holyCow. test ("Sacred bovine!")
→true
该模式将匹配任何单词,包含后面跟着cow、bovine、bull或taurus四个单词中的任何一个字符的scared或holy。这里需要使用括号,否则只会在sacred、holycow、bovine等单词之间进行选择。
8.2匹配与替换
通常,寻找一个模式只是从字符串中提取内容的第一步,通过调用字符串的indexOf方法和slice方法实现的。现在了解了正则表达式,可以完成得更好。
8.2.1匹配方法
字符串有-个match方法,接收正则表达式作为参数。如果匹配失败就返回null;如果匹配成功则返回匹配的字符串组成的数组。通过下面的示例来了解这一实现过程:
"No".match(/yes/i);
→null
"... yes".match(/yes/i);
→["yes"]
"Giant Ape".match(/giant (\w+)/i);
→["Giant Ape", "Ape"]
返回数组的第一个元素总是匹配整个模式的字符串。第二个示例说明,模式里有括号时,括 号匹配的部分将添加到该数组。通常,这使提取字符串变得非常容易。
现在,可以重写第3章编写的extractDate函数了。如果传入一个字符串,该函数会查找日期格式的内容。如果它能够找到这样的日期,就将该值存放在Date对象中;否则,就抛出一个异常。
function extractDate(string) {
var found = string.match(/\b(\d\d?)\/(\d\d?)\/(\d{4})\b/);
if (found == null)
throw new Error ("No date found in '" + string + "'.");
return new Date (Number (found [ 3 ]), Number(found[2]) - 1, Number (found [l]));
}
该函数不再是以前的版本了,实际上,它能够检查输入是否匹配了其期望值,如果是无意义 的输入则拋出异常。如果没有正则表达式就会很难理解——需要多次调用indexOf函数,来找出 该数字是否包含一个或两个数字、斜杠是否在期望的位置。
8.2.2正则表达式和替换方法
字符串值的replace方法可以接收一个正则表达式作为其第一个参数:
"Borobudur".replace(/[ou]/g,"a");
→ "Barabadar"
请注意,正则表达式后面的字符g代表“全局”,这意味着匹配该模式的字符的每个部分都应该被替换掉。当省略g时,只有第一个字符o被替换掉,这是一种常见的错误。
有时候我们需要保留替换掉的部分。例如,如果一个大字符串包含很多人名,每行有一个人 格式是:名,姓。如果我们想把中间的逗号去掉,并进行名和姓的换位,来得到简单的姓名格式 ,可以使用如下代码:
var names = "Picasso, Pablo\nCauguin, Paul\nVan Gogh, Vincent";
names.replace(/([\w ]+), ([\w ]+)/g, "$2 $1");
→ "Pablo Picasso\nPaul Cauguin\nVincent Van Gogh"
$1和$2是模式里括号部分的替换。$1被匹配第一对括号的内容替换掉了,$2则是被第二对括号替换掉了,以此类推,一直到$9。
如果模式里的括号超过9个,这一技术将不再适用。但是,使用正则表达式还有另外一个更 灵活的替换字符串的方式。当传给replace函数的第二个参数是函数而不是字符串时,每次找到匹配值,该函数就会被调用一次,匹配的文本就会被函数的返回值替换掉。传递给该函数的参数是成功匹配的元素,它们与match返回的数组里的值类似:第一个参数是整体匹配,后面是每个括号匹配的部分。
下面是一个简单的示例:
"the cia and fbi".replace(/\b(fbi|cia)\b/g, function(str) {
return str.toUpperCase();
});
→ "the CIA and FBI"
还有一个更好的示例:
var stock = "1 lemon, 2 cabbages, and 101 eggs";
function minusOne(match, amount, unit) {
amount = Number(amount) - 1;
if (amount = l) //如果只剩1个,删除后面的s
unit = unit.slice(0, unit.length - l);
else if (amount == 0)
amount = "no";
return amount + " " + unit;
}
stock.replace(/(\d+) (\w+)/g, minusOne);
→ "no lemon, l cabbage, and 100 eggs"
它接收一个字符串,找出所有出现的数字接字母数字单词,以字符中的形式将每次出现的数字递减返回。
(\d+)分组表示函数里的amount参数(\w+)分组表示匹配的unit.涵数将amount转化成数字( 因为它匹配了\d+,所以一直都有效),如果只剩一个或零个的话再做一些调整。
传一个函数给replace的技巧可以HTML转义器(第5章中)变得更加有效。它的代玛是这样的:
function escapeHTML(text) {
var replacements = [["&", "&tamp;"], ["\"", """],
["<", "<"], [">"", ">"]];
forEach(replacements, function(replace) {
text = text.replace(replace[0], replace[l]); ));
})aa
return text;
}
现在,可以编写新版本的escapeHTML 了,它可以完成同样的操作,伹只调用一次replace。
function escapeHTML(text) {
var replacements ={"<":"<", ">":">",
"&":"&","\"": """};
return text.replace(/[<>&"]/g, function(character) {
return replacements[character];
}
replacements对象是将特殊字符和对应转义字符进行关联的一条捷径。可以使用第6章里介 绍的Dictionary对象,该对象用于将一些值映射到其他值上。但一个简单的对象同样也是安全的,因为我们知道哪些字符将被用作属性并且不需要使用contains方法(该方法检査一个名称是否存在于一个对象中)。
8.2.3动态创建RegExp对象
在某些倩况下编写代码时,可能不知道需要的匹配模式。比如要编写一个(非常简单的)留言板淫秽词语过滤器,我们只想让消息不包含淫秽的单词。
检査文本中的淫秽词语最有效的方式就是使用一个正则表达式。由于无法亊先得知要检测哪
些词语,因此必须在代码中创建正则表达式。为此,需要使用正则表达式构造函数。
var badWords = ["ape", "monkey", "simian", "gorilla", "evolution"];
var pattern = new RegExp(badWords.join("|"), "i");
function isAcceptable(text) {
return !pattern.test(text);
}
isAcceptable("The quick brown fox...");
→true
isAcceptable("Cut that monkeybuslness out.");
→false
isAcceptable("Mmmm, grapes.");
→false
RegExp构造函数的第一个参数包含表达式模式的字符串,第二个参数(也可以省略)用于添加忽略大小写或非全局匹配。
另外,在词语周围添加\b模式,以避免(例如)“grapes”被识别成不可接受字符。但是,这样也会让“monkeybusiness”变成可接受字符,这也许是不正确的。正如我们所看到的,淫秽词语过滤器很难恢复正常(通常会令人恼火)。
使用字符串建立正则表达式模式时,必须注意反斜杠的用法:通常,在解释器解释的时候反斜杠是被删除掉的,因此在正则表达式里任何反斜杠都要用反斜杠自身再转义一次:
var digits.new RegExp("\\d+");
8.3解析.ini文件
现在,看一个正则表达式调用的真正问题。想象我们正在编写一个从互联网上自动获取敌人相关信息 的程序。实际上,我们并不能编写出这样的程序,而只能编写读取配置文件的那部分。 文件是这样的:
searchengine=http: //www. google. cora/search ?q=$l
spitefulness=9.7
;comments are preceded by a semicolon...
;these are sections, concerning individual enemies
[larry]
fullname=Larry Doe type=kindergarten bully
website=http://www.geocities. com/CapeCanaveral/11451
[gargamel]
fullname=Gargainel
type=evil sorcerer
outputdir=/home/marijn/enemies/gargamel
下面是这种格式的准确规则(实际上是一种广泛使用的格式,通常称为.ini文件):
•忽略空行以及以分号开头的行;
•以中括号[ 和 ]包住的行是新的章节;
•如果行中包含字母数字标识符,并且紧接着是 = 的话,将内容设置到当前章节;
•其他行都是无效的。
我们的任务是将类似的字符串转化成一个对象数组,每个对象都要有一个名字和name/value 对的数组。我们需要一个这样的对象保存章节,还需要一个对象保存章节以外的设置。
因为这种格式要逐行进行处理,所以,首先要将这些内容分隔成多行,到目前为止,总是使 用string.split("\n")来分隔行,伹有些搡作系统不仅使用一个换行符来分隔,还在回车符后面跟着一个换行符("\r\n")。
字符串的split方法也可以接收一个正则表达式作为参数,下面的函数是将字符串分隔成字符行的数组,"\n"和"\r\n"均可置于行与行之间。
function splitLines(string) {
return string.split(/\r?\n/);
}
下面是所有编写.ini文件解析函数所需的代码:
function parselNI(string) {
var lines = splitLines(string);
var categories =[];
function newCategory(name) {
var cat = {name: name, fields: []};
categories.push(cat);
return cat;
}
var currentCategory = newCategory("TOP");
forEach(lines, function(line) {
var match;
if (/^\s*(;.*)?$/.test(line))
return;
else if (match = line.match(/^\[(.*)\]$/))
currentCategory = newCategory(match[1]);
else if (match = line.match(/^(\w+)=(.*)$/))
currentCategory. fields. push ({name: match[l], value: match[2]});
else
throw new Error("Line'" + line + "' is invalid.");
});
return categories;
}
简而言之,代码遍历文件里的每一行字符。它保留了一个当前分类对象,一旦找到正常指示,就将其添加到对象上。当它遇到一个开始新分类的行,就会使用新分类替换当前分类,后面 的子序列也将添加进去。最终,它返回一个包含所有遇到的分类类别的数组。
需要注意,循环使用^和$是为确保表达式匹配整个完整的行,而不仅仅是其中一部分。将它们省略是一个很常见的错误——代码虽然能正常使用,但有些输入却会很怪异。
表达式/^\s*(;.*)?$/用来测试可以被忽略的行。知道它是如何工作的吗?圆括号之间的部分 将匹配注释,其后面的?将确保它能匹配空行。
使用正则表达式时,会经常看到if (match == string.match(...))模式。通常情况下,我们不能完全确信表达式是否能够匹配成功,而且也不想让代码去执行像null[1]这样的内容,因此需要测试match返回的是否为非null值。为了不破坏这个完美的形式链,可以将结果賦值给一个if 测试判断的变量:在一个单行里进行匹配和测试。
8.4结论
至此,正则表达式中最重要的内容就是它们确实存在,并且可以使字符管理代码变得更短。
事实上,除了本章所介绍的正则表达式的内容外,还有很多内容需要学习。如果有兴趣可以搜索互联网。Javascript正则表达式使用的语法叫Perl兼容正则表试(Perl CompatibleRegular Expressions),其他编程语言里也可以看到该语法。
该语法非常含糊,以至于在前10次(或更多次)使用的时候,不得不去査看正则达式的细节。持之以恒,就可以编写出非常复杂、神秘的表达式了。
【参考】
Chapter 10: Regular Expressions[英文原文地址]