「译」JavaScript正则表达式应用(Eloquent JavaScript 第九章)
此文翻译《Eloquent Javascript》中第九章,Regular Expressions,侵删。
该书有中文译本出版。此译文仅作交流学习之用。
有人说,当我遇到一个问题的时候,我会说,我知道用正则表达式解决。那么他们就有两个问题了。
—— Jamie Zawinski
Yuan-Ma说,当你收获粮食时,需要更多的力量。当你获得一个问题的答案时,需要更多的代码。
—— Master Yuan-Ma, The Book of Programming
编程工具和技术成长之路一直是混乱的,革命性的。它并不总是最优秀最聪明的那个获胜,而是那些能保证功能运行正常的最后才能活下来。举个例子,那些能够很好的和别的技术结合的编程工具。
在这章里,我会讲符合上述的一个工具,即正则表达式。正则表达式是一种描述字符串规律的方法。这是一个小且独立的语言,是JavaScript还有很多别的语言的一部分, 是个好用的小工具。
正则表达式很奇怪,但同时非常好用。它语法很神奇,JavaScript提供的接口也很笨拙。但不妨碍它是一个功能强大的字符串检索和处理工具。正确的理解正则表达式能让你成为一个更高效的程序员。
创建一个正则表达式
一个正则表达式就一个对象,可以通过RegExp
构造函数创建,或者是作为一个斜杠/
包围的文字值来写入。
1 | var re1 = new RegExp("abc"); |
这两个正则表达式的对象模式
相同:字母a后面跟着b,b后面跟着c。
使用RegExp
构造函数时,模式
被写为正常的字符串,所以对反斜杠也适用。
第二行中把表达式写在两个斜杠中间,对反斜杠的处理会有所不同。首先,因为斜杠代表一个模式
的结束,所以就需要在模式
内部的斜杠前加一个反斜杠。另外,如果斜杠不是某个特殊符号的一部分,它也会被保留下来,并且会改变模式
的含义。比如\n
。还有一些字符比如问号和加号,他们在正则表达式中有特殊的含义。如果这些符号需要作为字符串的一部分,也需要在这些字符前加反斜杠。
1 | var eighteenPlus = /eighteen\+/; |
书写正确的正则表达式反斜杠转义,就需要知道正则表达式中每一个特殊符号的含义。但就目前来看还不太现实,所以在任何非字母,数字,或者空格的字符前加上反斜杠。
匹配测试
正则表达式对象拥有自己一系列的方法。最简单的就是test
。给它传递一个字符串,它会返回一个布尔值,用于表示这个字符串是否包含符合表达式的模式
。
1 | console.log(/abc/.test("abcde")); |
一个只由非特殊符号组成的表达式,只用于表示这个字符串序列。如果abc
出现在字符串的任何位置,test
都会返回true
。
匹配一组字符串
测试一个字符串是否包含abc
和调用indexOf
的功能类似。正则表达式的功能更为强大,能够检测会更复杂的模式
。
假设我们想要匹配任何数字。在正则表达式中,把字符放在方括号内,就表示会匹配方括号内的任何字符。
下面的两个式子都表示匹配任意数字:
1 | console.log(/[0123456789]/.test("in 1992")); |
在方括号内,两个字符之间加-
表示一定范围内的字符,顺序等同于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 | var dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/; |
这个表达式包含很多反斜杠,制造了太多背景噪音,让人很难去理解这个表达式多传递的模式
。我们稍后会讲一个稍微升级一点的版本。
反斜杠也可以用在方括号中间。举个例子,[\d.]
表示任何数字或者句点字符。但要注意句点符号,当它出现在方括号内部时,就失去了它的特殊含义。其他的特殊含义字符也类似,比如+
。
为了反向验证字符串,就是待验证的字符串中不能出现正则表达式的模式
,可以在中括号中添加一个插入符。
1 | var notBinary = /[^01]/; |
重复部分模式
现在你已经知道了如何匹配单个数字,那么一个数字字符串(多余一个数字的数)该如何匹配呢?
当你在正则表达式后面加一个加号+
时,表示这个元素可能重复出现。所以,/\d+/
匹配的是一个或多于一个数字的字符串。
1 | console.log(/'\d+'/.test("'123'")); |
星号*
的含义与之类似,但也匹配出现0次的模式
。任何结尾出现星号的表达式永远会匹配,如果字符湖惨重没有出现匹配的模式
,就是0匹配实例。
问号的含义是模式
的一部分为可选,即可以出现0次或一次。在下面的例子中,字符u
可以出现,但它没有出现的时候同样匹配。
1 | var neighbor = /neighbou?r/; |
如果要指定一个模式
的出现次数,需要用到花括号。在元素后面加上{4}
,就表示该元素必须要出现4次。当然也可以指定出现次数的范围,{2,4}
表示该元素出现至少2次至多4次。
下面的例子就是时间模式
的另一种表示方法,可以匹配单个或两个数字的日期,月份还有时间。并且增加了可读性。
1 | var dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/; |
你还可以指定一个开放的范围,在花括号中,省略逗号后面的数字。{5,}
表示出现超过5次。
分组表达式
如果需要一次使用多个操作符,如*
或者+
,可使用括号。只要后续的操作符是相关联的,正则表达式中被括号括起来的部分就可以算作一个单独的元素。
1 | var cartoonCrying = /boo+(hoo+)+/i; |
第一个和第二个+
只表示boo
和hoo
中的第二个o
,第三个+
表示的是hoo+
这个整体,检测的是一个或多个整体。
在表达式最后的i
表示该表达式大小写不敏感,虽然表达式中都是小写,但还是可以检测到字符串中的B
。
匹配和分组
test
方法绝对是正则表达式匹配最简单的方式,它只告诉你是否匹配,没有更多的信息。正则表达式还有一个exec
方法,如果没有找到匹配项会返回null
,反之返回找到的匹配项。
1 | var match = /\d+/.exec("one two 100"); |
exec
返回的对象有index
属性,可以告诉我们匹配项的位置。除了这个,这个对象同时也是一个字符串,第一个元素就是我们想要找的匹配项。
字符串对象也有一个功能相似的match
方法。
1 | console.log("one two 100".match(/\d+/)); |
当正则表达式中包含有圆括号括起来的子表达式时,所有匹配的项会作为数组返回。全部匹配的项会做为数组的第一个元素。第二个元素部分匹配,以此类推。
1 | var quotedText = /'([^']*)'/; |
当一个字符串中完全没有可匹配的项时,那么输出值会是undefined
。类似地,当某项多次匹配时,数组的最后一个元素是最后一个匹配项。
1 | console.log(/bad(ly)?/.exec("bad")); |
分组在提取部分字符串的时候非常有用。如果我们不只是想要验证字符串时候包含一个日期,而是同时想提取这个日期并且作为对象返回。我们可以用圆括号包含数字模式,使用exec
返回日期数值。
我们绕了一圈终于要讨论一下JavaScript中日期和时间的存储方式。
时间类型
JavaScript中有一个标准的日期对象,或者说是时间点,叫做Data
。如果你new
一个日期对象,将会得到当前日期和时间。
1 | console.log(new Date()); |
你也可以创建一个特定的时间对象。
1 | console.log(new Date(2009, 11, 9)); |
JavaScript按照惯例月份从0开始(所以12月是11),但是日期是从1开始。这一点很让人混淆,需要注意。
最后四个参数(小时,分钟,秒钟,还有毫秒)是可选项,默认值为0。
时间戳是一个毫秒为单位的数字,从1970年开始算起,1970年之前是负值(遵循Unix time约定,大概是1970年规定的)。getTime
方法可以返回特定时间的时间戳数字。你可以想象,这个数字非常大。
1 | console.log(new Date(2013, 11, 19).getTime()); |
如果给Date
构造函数传递一个参数,这个参数就会被当作这样的毫秒数值。通过创建一个新的Date
对象并调用getTime
,或者是Date.now
,就可以得到当前的毫秒数。Date
对象提供getFullYear
,getMonth
,getDate
,getHours
, getMinutes
和getSeconds
方法,用于提取相应的值。还有getYear
会返回相对不那么有用的两位数的年份,例如94或者14。
将我们感兴趣的子表达式用小括号括起来,我们就可以很容易的从一个字符串里提取日期对象了。
1 | function findDate(string) { |
字和字符串的边界
不幸的是,findDate
也会从字符串”100-1-30000”提取出无意义的日期00-1-3000。这样的匹配可能会发正在字符串的任一位置,在这种情况下,它会从第二个字符开始,在倒数第二个字符结束。
如果我们想要强制扫面整个字符串,我们可以加上标签^
和<div class="post-content"。插入符会从输入的开端开始匹配,美元符号表示匹配结尾。所以
/^\d+$/匹配连续的一个或者一串数字,
/^!/匹配任意带有感叹号开端的字符串,
/x^/不能匹配任何字符串(因为字符串开始之前不可能有x)。
如果相反,我们只想要确保一个词语的开始和结束字符,我们可以使用
\b`。字符边界可以是开头或者结尾。
1 | console.log(/cat/.test("concatenate")); |
注意边界符并不代表一个实际的字符。它只是强制正则表达式去匹配相应的模式。
选择模式
如果我们想要找到一段文字不仅包含一个数字,还跟着例如pig
,cow
或者chicken
,或者它们的复数形式。
我们可以写三个正则表达式,然后一个一个测试,但有更简洁的办法。管道字符|
表示它两侧的表达式都可以作为备选。如下:
1 | var animalCount = /\b\d+ (pig|cow|chicken)s?\b/; |
圆括号可以用来限制模式应用的部分,你也可以在两个模式之间接连使用多个选择表达式。
匹配机制
正则表达式可以看作流程图。以下例子是前面的家畜表达式:
如果我们的表达式能顺利从左到右找到一个通路,就可以找到一个匹配的字符串。我们保存字符串里的一个当前位置,通过没个检验盒的时候,我们只验证当前位置之后的部分是否与标准匹配。
所以当我们试图用正则表达式验证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 | console.log("papa".replace("p", "m")); |
其中第一个变量也可以是正则表达式,符合正则表达式的字符将会被替换。增则表达式中的g
选项,指定的是所有符合正则表达式的字符串将会被替换,不仅仅是第一个。
1 | console.log("Borobudur".replace(/[ou]/, "a")); |
通过给replace
添加参数,或者使用replaceAll
方法替换所有匹配项,都是可行的。但还有一些情况,只能用合适的正则表达式来计算。
正则表达式真正的强大的地方,在于我们可以使用分组去替换字符串。举个例子,我们有一个包含名字的很长的字符串,每个名字占一行,格式是Lastname,Firstname
。如果我们想要去掉中间的逗号变成Firstname Lastname
这样的格式,我们可以这样做:
1 | console.log( |
表达式中的的$1
和$2
表示圆括号的匹配项。$1
表示第一个匹配项,$2
表示第二个,以此类推。全部的匹配项可表示为><
。
除了字符串之外,还可以给replace
传递一个函数作为第二个参数。对于每一个替换项,每一个匹配项都会作为参数调用这个函数,返回值会作为一个新的字符串。
下面是一个简单的例子:
1 | var s = "the cia and fbi"; |
下面是一个更有意思的例子:
1 | var stock = "1 lemon, 2 cabbages, and 101 eggs"; |
这个函数传入一个字符串作为参数,找到所有的数次和量词一起出现的组合,返回一个把数量减一的字符串。
(\d+)分组返回的是amount
变量,(\w+)分组返回的是unit
变量。函数中转换amount
为数字变量,这个转换的前提是它作为\d+
的匹配项,函数中还为0和1的情况设置了不同的处理机制。
贪婪
用replace
方法写一个能够去掉JavaScript代码中注释的函数是很难的。下面是一个简单的尝试:
1 | function stripComments(code) { |
在|
算子之前的部分,匹配的是非新行的双斜杠后面的任意字符。需要注意的是多行注释。我们使用[^]
(任意非空字符串)去匹配任意字符。这里不能只用一个.
,因为块级注释可以跨行,而.
不能跨行检索。
但是之前的代码还是出错了,为什么呢?
就像之前回溯
中讲过的,表达式中的[^]*
会尽可能多的匹配。如果下一部分不能匹配模式,那么匹配器会回退一个字符并且重新开始检索匹配项。在本例中,匹配器先识图匹配整个字符串,然后回溯。在回溯四个字符之后,*/
找到了匹配项。这并不是我们想要的结果,原本的意图是找到简单的注释,而不是在整个代码中找到注释的结束符。
由于这样的机制,我们可以说重复算子(+,,?和{})具有贪婪属性,意味着这样的算子会尽可能多的匹配,并且回溯。如果你在这些算子后面接了?
(+?, ?, ??, {}?),这些算子就不再贪婪,而是尽可能少的去匹配,只有在不符合最小匹配的时候才去查找更多的匹配项。
这就是我们在本例中想要的结果。星号需要查找的是最小的*/
匹配项,只需要找到一个注释块。
1 | function stripComments(code) { |
正则表达式的很多bug就是,在该使用非贪婪算子的时候使用了贪婪算子。使用重复算子的时候,优先考虑非贪婪算子。
动态创建正则表达式对象
当你在写代码的时候,总有一些时候,你会不知道确切的要匹配的模式。比如说你想要在一个字符串中找出用户名,并用下划线标示。因为只有在程序跑起来之后才能知道用户名,所以你不可能用结合斜杠的查找方法。
但是你可以用一个正则表达式构造函数去做这件事,这里给出一个例子:
1 | var name = "harry"; |
在这里我们使用双斜杠去表示\b
边界符,因为这里我们写的是一个普通字符串,而不是一个斜杠包含的正则表达式。RegExp
的第二个参数gi
的意思是全局查找,而且大小写敏感。
但如果有个中二少年的名字是dea+hl[]rd
,这就需要一个不敏感的正则表达式才能匹配,本例中检测不到这样的名字。
为了正常运行,我们在任何不确定的字符前都加上反斜杠。但是在任一字母前面加反斜杠,并不是一个好的习惯,比如\b
和\n
都有它们的特殊含义。但转义所有非字母或者空格都是安全的。
1 | var name = "dea+hl[]rd"; |
查找
正则表达式没有内置字符串的indexOf
方法,但是它有别的方法,search
,并且该方法只能被正则表达式调用。类似于indexOf
,search
返回的是表达式找到的匹配项的第一个位置,-1表示没有匹配项。
1 | console.log(" word".search(/\S/)); |
不友好的是,search
方法不能指定偏移(indexOf
的第二个参数是偏移量)。
LastIndex
属性
exec
方法同样也有没有提供一个方便的给定开始检索的属性,但提供了一个不太方便的办法。
正则表达式对象也有自己的属性。其中的一个属性就是source
,它包含了原始字符串。另一个属性是lastIndex
,在某些情况下,它能够控制下一个检索开始的位置。
如果要使用这些属性,必须开启全局选项,而且exec
方法必须能找到匹配项。再说一句,一个更合理的方法是给exec
多传递一个参数,但是JavaScript没有给正则表达式提供这样的接口。
1 | var pattern = /y/g; |
如果匹配成功,exec
自动更新lastIndex
属性的值为匹配的后面一个位置。如果没有匹配项,lastIndex
重新设置为0,同时新建的正则表达式这个属性也是0。
当使用全局正则表达式多次调用exec
,自动更新lastIndex
导致问题出现。你的正则表达式会从上一次调用之后的位置之后开始。
1 | var digit = /\d/g; |
另一个有意思的全局反应就是,它改变了match
方法对字符串的作用。当调用一个全局正则表达式,它不是像exec
一样返回一个数组,match
会返回所有匹配的字符串,并组成一个数组。
1 | console.log("Banana".match(/an/g)); |
所以要小心使用全局表达式。只在适当的时候使用,调用replace
还有调用lastIndex
的时候使用,通常也只有这两个地方会用到。
循环匹配
一种常见的模式就是用lastIndex
和exec
在循环体内扫描,找出匹配的字符串。
1 | var input = "A string with 3 numbers in it... 42 and 88."; |
这里使用了赋值表达式(=)。使用match = number.exec(input)
,作为while
成立的条件,每次迭代的时候执行匹配,把结果保存进变量,当没有匹配项的时候循环结束。
解析INI文件
做为本章的总结,我们来看一个使用正则表达式的实际问题。想象我们正在编写一个可以自动从往上收集敌人信息的程序。(我们并不是真的在这里写这个程序,只是读取配置文件的部分,不好意思让你失望了)配置文件如下:
1 | searchengine=http://www.google.com/search?q=$1 |
具体的格式化规则(被广泛的使用,称做INI文件)如下:
- 忽略空行和以分号开头的行
- 独立出[and]连接的句子
- 包含文数字后面加
=
的行,在当前部分增加一个设置 - 其他任何都是无效的
我们的任务就是像上述要求把字符串转变成数组对象,每一项都有一个name
属性,还有一个设置数组。每一个部分我们都需要这样一个对象,并且在开头还需要一个全局设置。
因为格式化需要逐行执行,开始就是需要把文件按行分割。在第六章中我们讲过用string.split("\n")
实现。但是在某些操作系统中,不仅使用换行符分割行,而是使用回车加换行符(”\r\n”)。正则表达式也可以作为split
方法的参数,我们可以用正则表达式/\r?\n/
去正确的分割\n
或者\r\n
换行的文件。
1 | function parseINI(string) { |
这行代码会逐行去运行,运行的时候不断更新“当前部分”。首先,它用表达式/^\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
构造函数可以用作从字符串创建一个正则表达式值。
正则表达式非常强大但是不容易使用。它很大程度上简化了计算,但是也会由于应用的复杂导致失控。对正则表达式的初步了解是不够的,而一旦掌握它,你就会疯狂的爱上它并且想用它表示一切东西。