此文翻译《Eloquent Javascript》中第九章,Regular Expressions,侵删。
该书有中文译本出版。此译文仅作交流学习之用。


有人说,当我遇到一个问题的时候,我会说,我知道用正则表达式解决。那么他们就有两个问题了。
—— Jamie Zawinski
Yuan-Ma说,当你收获粮食时,需要更多的力量。当你获得一个问题的答案时,需要更多的代码。
—— Master Yuan-Ma, The Book of Programming

编程工具和技术成长之路一直是混乱的,革命性的。它并不总是最优秀最聪明的那个获胜,而是那些能保证功能运行正常的最后才能活下来。举个例子,那些能够很好的和别的技术结合的编程工具。
在这章里,我会讲符合上述的一个工具,即正则表达式。正则表达式是一种描述字符串规律的方法。这是一个小且独立的语言,是JavaScript还有很多别的语言的一部分, 是个好用的小工具。
正则表达式很奇怪,但同时非常好用。它语法很神奇,JavaScript提供的接口也很笨拙。但不妨碍它是一个功能强大的字符串检索和处理工具。正确的理解正则表达式能让你成为一个更高效的程序员。


创建一个正则表达式

一个正则表达式就一个对象,可以通过RegExp构造函数创建,或者是作为一个斜杠包围的文字值来写入。

1
2
var re1 = new RegExp("abc");
var re2 = /abc/;

这两个正则表达式的对象模式相同:字母a后面跟着b,b后面跟着c。
使用RegExp构造函数时,模式被写为正常的字符串,所以对反斜杠也适用。
第二行中把表达式写在两个斜杠中间,对反斜杠的处理会有所不同。首先,因为斜杠代表一个模式的结束,所以就需要在模式内部的斜杠前加一个反斜杠。另外,如果斜杠不是某个特殊符号的一部分,它也会被保留下来,并且会改变模式的含义。比如\n。还有一些字符比如问号和加号,他们在正则表达式中有特殊的含义。如果这些符号需要作为字符串的一部分,也需要在这些字符前加反斜杠。

1
var eighteenPlus = /eighteen\+/;

书写正确的正则表达式反斜杠转义,就需要知道正则表达式中每一个特殊符号的含义。但就目前来看还不太现实,所以在任何非字母,数字,或者空格的字符前加上反斜杠。


匹配测试

正则表达式对象拥有自己一系列的方法。最简单的就是test。给它传递一个字符串,它会返回一个布尔值,用于表示这个字符串是否包含符合表达式的模式

1
2
3
4
5
console.log(/abc/.test("abcde"));
// → true

console.log(/abc/.test("abxde"));
// → false

一个只由非特殊符号组成的表达式,只用于表示这个字符串序列。如果abc出现在字符串的任何位置,test都会返回true


匹配一组字符串

测试一个字符串是否包含abc和调用indexOf的功能类似。正则表达式的功能更为强大,能够检测会更复杂的模式
假设我们想要匹配任何数字。在正则表达式中,把字符放在方括号内,就表示会匹配方括号内的任何字符。
下面的两个式子都表示匹配任意数字:

1
2
3
4
5
console.log(/[0123456789]/.test("in 1992"));
// → true

console.log(/[0-9]/.test("in 1992"));
// → true

在方括号内,两个字符之间加-表示一定范围内的字符,顺序等同于Unicode编码。0-9在这个编码中就是以数字从小到大排列(编码48-57),所以[0-9]就表示从0到9的任意数字。
正则表达式中还有内置的简写字符串,用于表示一些常用的匹配项。

Expression Meaning
\d Any digit character
\w An alphanumeric character (“word character”)
\s Any whitespace character (space, tab, newline, and similar)
\D A character that is not a digit
\W A nonalphanumeric character
\S A nonwhitespace character
. Any character except for newline

所以我们可以用一下格式去匹配时间字符串,30-01-2003 15:20:

1
2
3
4
5
var dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;
console.log(dateTime.test("30-01-2003 15:20"));
// → true
console.log(dateTime.test("30-jan-2003 15:20"));
// → false

这个表达式包含很多反斜杠,制造了太多背景噪音,让人很难去理解这个表达式多传递的模式。我们稍后会讲一个稍微升级一点的版本。
反斜杠也可以用在方括号中间。举个例子,[\d.]表示任何数字或者句点字符。但要注意句点符号,当它出现在方括号内部时,就失去了它的特殊含义。其他的特殊含义字符也类似,比如+
为了反向验证字符串,就是待验证的字符串中不能出现正则表达式的模式,可以在中括号中添加一个插入符。

1
2
3
4
5
var notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110"));
// → false
console.log(notBinary.test("1100100010200110"));
// → true

重复部分模式

现在你已经知道了如何匹配单个数字,那么一个数字字符串(多余一个数字的数)该如何匹配呢?
当你在正则表达式后面加一个加号+时,表示这个元素可能重复出现。所以,/\d+/匹配的是一个或多于一个数字的字符串。

1
2
3
4
5
6
7
8
9
10
11
console.log(/'\d+'/.test("'123'"));
// → true

console.log(/'\d+'/.test("''"));
// → false

console.log(/'\d*'/.test("'123'"));
// → true

console.log(/'\d*'/.test("''"));
// → true

星号*的含义与之类似,但也匹配出现0次的模式。任何结尾出现星号的表达式永远会匹配,如果字符湖惨重没有出现匹配的模式,就是0匹配实例。
问号的含义是模式的一部分为可选,即可以出现0次或一次。在下面的例子中,字符u可以出现,但它没有出现的时候同样匹配。

1
2
3
4
5
var neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true

如果要指定一个模式的出现次数,需要用到花括号。在元素后面加上{4},就表示该元素必须要出现4次。当然也可以指定出现次数的范围,{2,4}表示该元素出现至少2次至多4次。
下面的例子就是时间模式的另一种表示方法,可以匹配单个或两个数字的日期,月份还有时间。并且增加了可读性。

1
2
3
var dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("30-1-2003 8:45"));
// → true

你还可以指定一个开放的范围,在花括号中,省略逗号后面的数字。{5,}表示出现超过5次。


分组表达式

如果需要一次使用多个操作符,如*或者+,可使用括号。只要后续的操作符是相关联的,正则表达式中被括号括起来的部分就可以算作一个单独的元素。

1
2
3
var cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true

第一个和第二个+只表示boohoo中的第二个o,第三个+表示的是hoo+这个整体,检测的是一个或多个整体。
在表达式最后的i表示该表达式大小写不敏感,虽然表达式中都是小写,但还是可以检测到字符串中的B


匹配和分组

test方法绝对是正则表达式匹配最简单的方式,它只告诉你是否匹配,没有更多的信息。正则表达式还有一个exec方法,如果没有找到匹配项会返回null,反之返回找到的匹配项。

1
2
3
4
5
var match = /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8

exec返回的对象有index属性,可以告诉我们匹配项的位置。除了这个,这个对象同时也是一个字符串,第一个元素就是我们想要找的匹配项。
字符串对象也有一个功能相似的match方法。

1
2
console.log("one two 100".match(/\d+/));
// → ["100"]

当正则表达式中包含有圆括号括起来的子表达式时,所有匹配的项会作为数组返回。全部匹配的项会做为数组的第一个元素。第二个元素部分匹配,以此类推。

1
2
3
var quotedText = /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]

当一个字符串中完全没有可匹配的项时,那么输出值会是undefined。类似地,当某项多次匹配时,数组的最后一个元素是最后一个匹配项。

1
2
3
4
console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]

分组在提取部分字符串的时候非常有用。如果我们不只是想要验证字符串时候包含一个日期,而是同时想提取这个日期并且作为对象返回。我们可以用圆括号包含数字模式,使用exec返回日期数值。
我们绕了一圈终于要讨论一下JavaScript中日期和时间的存储方式。


时间类型

JavaScript中有一个标准的日期对象,或者说是时间点,叫做Data。如果你new一个日期对象,将会得到当前日期和时间。

1
2
console.log(new Date());
// → Wed Dec 04 2013 14:24:57 GMT+0100 (CET)

你也可以创建一个特定的时间对象。

1
2
3
4
5
console.log(new Date(2009, 11, 9));
// → Wed Dec 09 2009 00:00:00 GMT+0100 (CET)

console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)

JavaScript按照惯例月份从0开始(所以12月是11),但是日期是从1开始。这一点很让人混淆,需要注意。
最后四个参数(小时,分钟,秒钟,还有毫秒)是可选项,默认值为0。
时间戳是一个毫秒为单位的数字,从1970年开始算起,1970年之前是负值(遵循Unix time约定,大概是1970年规定的)。getTime方法可以返回特定时间的时间戳数字。你可以想象,这个数字非常大。

1
2
3
4
console.log(new Date(2013, 11, 19).getTime());
// → 1387407600000
console.log(new Date(1387407600000));
// → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)

如果给Date构造函数传递一个参数,这个参数就会被当作这样的毫秒数值。通过创建一个新的Date对象并调用getTime,或者是Date.now,就可以得到当前的毫秒数。
Date对象提供getFullYeargetMonthgetDategetHours, getMinutesgetSeconds方法,用于提取相应的值。还有getYear会返回相对不那么有用的两位数的年份,例如94或者14。
将我们感兴趣的子表达式用小括号括起来,我们就可以很容易的从一个字符串里提取日期对象了。

1
2
3
4
5
6
7
8
9
function findDate(string) {
var dateTime = /(\d{1,2})-(\d{1,2})-(\d{4})/;
var match = dateTime.exec(string);
return new Date(Number(match[3]),
Number(match[2]) - 1,
Number(match[1]));
}
console.log(findDate("30-1-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)

字和字符串的边界

不幸的是,findDate也会从字符串”100-1-30000”提取出无意义的日期00-1-3000。这样的匹配可能会发正在字符串的任一位置,在这种情况下,它会从第二个字符开始,在倒数第二个字符结束。
如果我们想要强制扫面整个字符串,我们可以加上标签^<div class="post-content"。插入符会从输入的开端开始匹配,美元符号表示匹配结尾。所以/^\d+$/匹配连续的一个或者一串数字,/^!/匹配任意带有感叹号开端的字符串,/x^/不能匹配任何字符串(因为字符串开始之前不可能有x)。 如果相反,我们只想要确保一个词语的开始和结束字符,我们可以使用\b`。字符边界可以是开头或者结尾。

1
2
3
4
console.log(/cat/.test("concatenate"));
// → true
console.log(/\bcat\b/.test("concatenate"));
// → false

注意边界符并不代表一个实际的字符。它只是强制正则表达式去匹配相应的模式。


选择模式

如果我们想要找到一段文字不仅包含一个数字,还跟着例如pigcow或者chicken,或者它们的复数形式。
我们可以写三个正则表达式,然后一个一个测试,但有更简洁的办法。管道字符|表示它两侧的表达式都可以作为备选。如下:

1
2
3
4
5
var animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pigchickens"));
// → false

圆括号可以用来限制模式应用的部分,你也可以在两个模式之间接连使用多个选择表达式。


匹配机制

正则表达式可以看作流程图。以下例子是前面的家畜表达式:
判断机制
如果我们的表达式能顺利从左到右找到一个通路,就可以找到一个匹配的字符串。我们保存字符串里的一个当前位置,通过没个检验盒的时候,我们只验证当前位置之后的部分是否与标准匹配。
所以当我们试图用正则表达式验证the 3 pigs的时候,我们的处理流程是这样:

  • 在第四个位置,是一个词语边界,所以我们通过了第一个检验盒。
  • 仍旧是第四个位置,我们找到了一个数字,所以可以通过第二个检验盒。
  • 在第五个位置,有一条路径循环返回到第二个检验盒(数字)之前,剩下的部分继续在流程中向前走到空格位。因为字符串中含有空格,不是数字,所以我们通过了第二条路。
  • 当前位置是第六位(从pigs开始),位于流程的三通分支处。我们这里没有cow或者chicken,但是有cow,所以可以通过这一分支。
  • 在第九位置,三通分支之后,有一条路是跳过s直接指向最后的词语边界,还有一条路可以匹配s。我们这里又一个s,并不是一个词语边界,所以我们通过s这个检验盒。
  • 现在我们位于第十位(字符串终点),并且只能匹配一个词语边界。字符串末尾是一个词语边界,所以我们能够顺利通过这个检验,并匹配这个字符串。

概念上来讲,正则表达式匹配字符串的机制是这样:它从字符串头部开始寻找匹配。在以上例子中,字符串首位是一个词语边界,但其后没有数字,所以在第二个检验盒位置失败了。然后就移向字符串中的第二个字符,继续从这里查找匹配项,以此类推,直到找到一个匹配项,或者移动到字符串末尾,判断该字符串中不包含任何匹配项。


回溯

正则表达式/\b([01]+b|\d+|[\da-f]+h)\b/匹配的是一个二进制数字后面加一个b,或者没有任何附加项的任意十进制数,或者一个十六进制数后面加一个h。下面是对应的流程图:
流程图
在匹配这个表达式的时候,通常在没有包含二进制数字的时候进入最上面的分支。举个例子,当检验103的时候,只有当3进入分支后才能判断匹配不符。这个字符串是符合我们检验标准的,但不符合当前第一个检验标准。
所以匹配器需要回溯。当进入一个分支时,它记录下当前的匹配位置(在本例中,从字符串初始位置开始,只通过了第一个词语边界检验盒),如果当前检验不能通过,执行回溯并且试着去匹配下一分支。对于字符串103来说,在检验到第三个字符之后,它会去匹配十进制数字分支。这个分支是检验成功的,然后根据需求返回。
一旦找到一个全匹配项,匹配器就结束工作。这意味着如果一个字符串能够匹配多个分支,只通过第一个匹配的检验盒(顺序按照正则表达式书写的顺序)。
回溯也出现在重复运算符比如+*中。如果你用/^.*x/去匹配abcxe.*会首先试着去匹配整个字符串。然后匹配器才会知道在模式中还有x需要匹配。由于没有位于字符串末尾的x,所以星号会尝试寻找少一个字符的匹配项。但是匹配器没有在abcx之后找到x,所以它又回溯,星号的匹配项就是abc。现在找到了正确匹配的x,成功返回0-4位。
包含很多回溯的正则表达式也是可行的,当一个模式可以匹配不同的规则时使用。举个例子,当我们对于二进制表达式存疑时,我们可以写成/([01]+)+b/
流程图
如果待匹配项是一串很长的二进制字符,而且后面没有b,这个匹配器会首先在数字内部循环。然后才知道字符串中没有b,然后回溯原位,再进入外面一层的循环,再失败,继续回溯到内循环。它会通过两个循环一直寻找所有可能的匹配项。这就意味着每增加一个字符,多一倍工作量。即便只是几个字符,匹配过程会一直继续下去。


替换方法

字符串变量有内置方法replace,可以用于替换字符串中的部分字符。

1
2
console.log("papa".replace("p", "m"));
// → mapa

其中第一个变量也可以是正则表达式,符合正则表达式的字符将会被替换。增则表达式中的g选项,指定的是所有符合正则表达式的字符串将会被替换,不仅仅是第一个。

1
2
3
4
console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar

通过给replace添加参数,或者使用replaceAll方法替换所有匹配项,都是可行的。但还有一些情况,只能用合适的正则表达式来计算。
正则表达式真正的强大的地方,在于我们可以使用分组去替换字符串。举个例子,我们有一个包含名字的很长的字符串,每个名字占一行,格式是Lastname,Firstname。如果我们想要去掉中间的逗号变成Firstname Lastname这样的格式,我们可以这样做:

1
2
3
4
5
6
console.log(
"Hopper, Grace\nMcCarthy, John\nRitchie, Dennis"
.replace(/([\w ]+), ([\w ]+)/g, "$2 $1"));
// → Grace Hopper
// John McCarthy
// Dennis Ritchie

表达式中的的$1$2表示圆括号的匹配项。$1表示第一个匹配项,$2表示第二个,以此类推。全部的匹配项可表示为><
除了字符串之外,还可以给replace传递一个函数作为第二个参数。对于每一个替换项,每一个匹配项都会作为参数调用这个函数,返回值会作为一个新的字符串。
下面是一个简单的例子:

1
2
3
4
5
var s = "the cia and fbi";
console.log(s.replace(/\b(fbi|cia)\b/g, function(str) {
return str.toUpperCase();
}));
// → the CIA and FBI

下面是一个更有意思的例子:

1
2
3
4
5
6
7
8
9
10
11
var stock = "1 lemon, 2 cabbages, and 101 eggs";
function minusOne(match, amount, unit) {
amount = Number(amount) - 1;
if (amount == 1) // only one left, remove the 's'
unit = unit.slice(0, unit.length - 1);
else if (amount == 0)
amount = "no";
return amount + " " + unit;
}
console.log(stock.replace(/(\d+) (\w+)/g, minusOne));
// → no lemon, 1 cabbage, and 100 eggs

这个函数传入一个字符串作为参数,找到所有的数次和量词一起出现的组合,返回一个把数量减一的字符串。
(\d+)分组返回的是amount变量,(\w+)分组返回的是unit变量。函数中转换amount为数字变量,这个转换的前提是它作为\d+的匹配项,函数中还为0和1的情况设置了不同的处理机制。


贪婪

replace方法写一个能够去掉JavaScript代码中注释的函数是很难的。下面是一个简单的尝试:

1
2
3
4
5
6
7
8
9
function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
console.log(stripComments("1 + /* 2 */3"));
// → 1 + 3
console.log(stripComments("x = 10;// ten!"));
// → x = 10;
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 1

|算子之前的部分,匹配的是非新行的双斜杠后面的任意字符。需要注意的是多行注释。我们使用[^](任意非空字符串)去匹配任意字符。这里不能只用一个.,因为块级注释可以跨行,而.不能跨行检索。
但是之前的代码还是出错了,为什么呢?
就像之前回溯中讲过的,表达式中的[^]*会尽可能多的匹配。如果下一部分不能匹配模式,那么匹配器会回退一个字符并且重新开始检索匹配项。在本例中,匹配器先识图匹配整个字符串,然后回溯。在回溯四个字符之后,*/找到了匹配项。这并不是我们想要的结果,原本的意图是找到简单的注释,而不是在整个代码中找到注释的结束符。
由于这样的机制,我们可以说重复算子(+,,?和{})具有贪婪属性,意味着这样的算子会尽可能多的匹配,并且回溯。如果你在这些算子后面接了? (+?, ?, ??, {}?),这些算子就不再贪婪,而是尽可能少的去匹配,只有在不符合最小匹配的时候才去查找更多的匹配项。
这就是我们在本例中想要的结果。星号需要查找的是最小的*/匹配项,只需要找到一个注释块。

1
2
3
4
5
function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1

正则表达式的很多bug就是,在该使用非贪婪算子的时候使用了贪婪算子。使用重复算子的时候,优先考虑非贪婪算子。


动态创建正则表达式对象

当你在写代码的时候,总有一些时候,你会不知道确切的要匹配的模式。比如说你想要在一个字符串中找出用户名,并用下划线标示。因为只有在程序跑起来之后才能知道用户名,所以你不可能用结合斜杠的查找方法。
但是你可以用一个正则表达式构造函数去做这件事,这里给出一个例子:

1
2
3
4
5
var name = "harry";
var text = "Harry is a suspicious character.";
var regexp = new RegExp("\\b(" + name + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → _Harry_ is a suspicious character.

在这里我们使用双斜杠去表示\b边界符,因为这里我们写的是一个普通字符串,而不是一个斜杠包含的正则表达式。RegExp的第二个参数gi的意思是全局查找,而且大小写敏感。
但如果有个中二少年的名字是dea+hl[]rd,这就需要一个不敏感的正则表达式才能匹配,本例中检测不到这样的名字。
为了正常运行,我们在任何不确定的字符前都加上反斜杠。但是在任一字母前面加反斜杠,并不是一个好的习惯,比如\b\n都有它们的特殊含义。但转义所有非字母或者空格都是安全的。

1
2
3
4
5
6
var name = "dea+hl[]rd";
var text = "This dea+hl[]rd guy is super annoying.";
var escaped = name.replace(/[^\w\s]/g, "\\><");
var regexp = new RegExp("\\b(" + escaped + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → This _dea+hl[]rd_ guy is super annoying.

查找

正则表达式没有内置字符串的indexOf方法,但是它有别的方法,search,并且该方法只能被正则表达式调用。类似于indexOfsearch返回的是表达式找到的匹配项的第一个位置,-1表示没有匹配项。

1
2
3
4
console.log("  word".search(/\S/));
// → 2
console.log(" ".search(/\S/));
// → -1

不友好的是,search方法不能指定偏移(indexOf的第二个参数是偏移量)。


LastIndex属性

exec方法同样也有没有提供一个方便的给定开始检索的属性,但提供了一个不太方便的办法。
正则表达式对象也有自己的属性。其中的一个属性就是source,它包含了原始字符串。另一个属性是lastIndex,在某些情况下,它能够控制下一个检索开始的位置。
如果要使用这些属性,必须开启全局选项,而且exec方法必须能找到匹配项。再说一句,一个更合理的方法是给exec多传递一个参数,但是JavaScript没有给正则表达式提供这样的接口。

1
2
3
4
5
6
7
var pattern = /y/g;
pattern.lastIndex = 3;
var match = pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5

如果匹配成功,exec自动更新lastIndex属性的值为匹配的后面一个位置。如果没有匹配项,lastIndex重新设置为0,同时新建的正则表达式这个属性也是0。
当使用全局正则表达式多次调用exec,自动更新lastIndex导致问题出现。你的正则表达式会从上一次调用之后的位置之后开始。

1
2
3
4
5
var digit = /\d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null

另一个有意思的全局反应就是,它改变了match方法对字符串的作用。当调用一个全局正则表达式,它不是像exec一样返回一个数组,match会返回所有匹配的字符串,并组成一个数组。

1
2
console.log("Banana".match(/an/g));
// → ["an", "an"]

所以要小心使用全局表达式。只在适当的时候使用,调用replace还有调用lastIndex的时候使用,通常也只有这两个地方会用到。


循环匹配

一种常见的模式就是用lastIndexexec在循环体内扫描,找出匹配的字符串。

1
2
3
4
5
6
7
8
var input = "A string with 3 numbers in it... 42 and 88.";
var number = /\b(\d+)\b/g;
var match;
while (match = number.exec(input))
console.log("Found", match[1], "at", match.index);
// → Found 3 at 14
// Found 42 at 33
// Found 88 at 40

这里使用了赋值表达式(=)。使用match = number.exec(input),作为while成立的条件,每次迭代的时候执行匹配,把结果保存进变量,当没有匹配项的时候循环结束。


解析INI文件

做为本章的总结,我们来看一个使用正则表达式的实际问题。想象我们正在编写一个可以自动从往上收集敌人信息的程序。(我们并不是真的在这里写这个程序,只是读取配置文件的部分,不好意思让你失望了)配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
searchengine=http://www.google.com/search?q=$1
spitefulness=9.7
; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451
[gargamel]
fullname=Gargamel
type=evil sorcerer
outputdir=/home/marijn/enemies/gargamel

具体的格式化规则(被广泛的使用,称做INI文件)如下:

  • 忽略空行和以分号开头的行
  • 独立出[and]连接的句子
  • 包含文数字后面加=的行,在当前部分增加一个设置
  • 其他任何都是无效的

我们的任务就是像上述要求把字符串转变成数组对象,每一项都有一个name属性,还有一个设置数组。每一个部分我们都需要这样一个对象,并且在开头还需要一个全局设置。
因为格式化需要逐行执行,开始就是需要把文件按行分割。在第六章中我们讲过用string.split("\n")实现。但是在某些操作系统中,不仅使用换行符分割行,而是使用回车加换行符(”\r\n”)。正则表达式也可以作为split方法的参数,我们可以用正则表达式/\r?\n/去正确的分割\n或者\r\n换行的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function parseINI(string) {
// Start with an object to hold the top-level fields
var currentSection = {name: null, fields: []};
var categories = [currentSection];

string.split(/\r?\n/).forEach(function(line) {
var match;
if (/^\s*(;.*)?$/.test(line)) {
return;
} else if (match = line.match(/^\[(.*)\]$/)) {
currentSection = {name: match[1], fields: []};
categories.push(currentSection);
} else if (match = line.match(/^(\w+)=(.*)$/)) {
currentSection.fields.push({name: match[1],
value: match[2]});
} else {
throw new Error("Line '" + line + "' is invalid.");
}
});

return categories;
}

这行代码会逐行去运行,运行的时候不断更新“当前部分”。首先,它用表达式/^\s*(;.*)?$/检测该行是不是应该被忽略。你知道它怎么运行吗?在括号内部的部分匹配的是注释,?确保匹配行只包含空格。
如果该行不是注释,代码会去检测该行是不是开始了一个新的部分。如果是,它会新建一个对象,添加子变量。
最后有意思的部分就是,如果该行是常规文字,那么就添加到当前部分的对象。
如果某行不符合任一上述规则,函数抛出错误。
注意要经常使用^<div class="post-content",确保表达式匹配的是整个行,而不只是其中一部分。如果不注意这些问题在大部分代码中也可以正常运行,但是有时会表现得很奇怪,这样的bug很难追踪。if (match = string.match(…))while条件的功能类似。你经常不能确定调用是不是能够成功匹配,所以对于这个检验,结果对象只能从if条件内部获得。为了不破坏if的结构,在if`条件内,我们将匹配结果赋值给一个变量,并且立即使用。


国际字符

由于JavaScript最初设计简单,而且这一简单的设置再后来被当作标准确定下来,JavaScript中正则表达式对于非英语字符的处理很差。举个例子,对于JavaScript正则表达式,一个字母字符仅限于26个拉丁字母(包含大小写),还有下划线字符。像字符é或者β就不会匹配\w,而且会匹配\W(非字符类)。
由于一个奇怪的历史事件,\s没有这个问题,可以匹配所有标准Unicode空格字符,包括不间断空格和蒙古语元音分隔符。
在一些编程语言中,正则表达式有特殊的规则去匹配所有的Unicode字符,比如“all uppercase letters”, “all punctuation”, 或者 “control characters”。JavaScript也计划添加类似支持,但近期可能不会实现。


总结

正则表达式是代表字符串中某个模式的对象,拥有自己的语法去表达某个模式。

Expression Meaning
/abc/ A sequence of characters
/[abc]/ Any character from a set of characters
/[^abc]/ Any character not in a set of characters
/[0-9]/ Any character in a range of characters
/x+/ One or more occurrences of the pattern x
/x+?/ One or more occurrences, nongreedy
/x*/ Zero or more occurrences
/x?/ Zero or one occurrence
/x{2,4}/ Between two and four occurrences
/(abc)/ A group
/\d/ Any digit character
/\w/ An alphanumeric character (“word character”)
/\s/ Any whitespace character
/./ Any character except newlines
/\b/ A word boundary
/^/ Start of input
/$/ End of input

正则表达式含有test内置方法,可以检测给定的字符串是否匹配。还有exec方法,当检测到匹配项,返回一个数组包含所有的匹配项。这样的数组具有index属性,指向匹配项开始的位置。
相对于正则表达式,字符串对象有内置的match方法,还有search方法去检测一个匹配值,只返回第一个匹配值的位置。replace方法可以用一个字符串替换掉匹配项。另外,还可以传递一个函数作为replace的参数,会基于匹配文字和匹配组新建字符串。
正则表达式也有可选项,写在闭合斜杠后。i表示大小写不敏感,g表示全局匹配,这样就可以使replace方法不止替换第一项,而是替换所有匹配项。
RegExp构造函数可以用作从字符串创建一个正则表达式值。
正则表达式非常强大但是不容易使用。它很大程度上简化了计算,但是也会由于应用的复杂导致失控。对正则表达式的初步了解是不够的,而一旦掌握它,你就会疯狂的爱上它并且想用它表示一切东西。