写个正则有多难
为什么要学正则表达式
虽然很多web开发者在忽视正则表达式后,还可以顺利工作,但在javascript中还存在一些问题,如果不用正则表达式,是没办法进行很好的解决的.
当然,也许还有其他的方式能够解决相同的问题.但通常,用一句正确的正则表达式很有可能就可以省略半屏幕的代码.
正则看起来很复杂,但是真的很难吗?
为什么正则表达式很牛
假设我们要验证一个字符串是否为格式正确的手机号码.手机号码开头都是1,然后后面都是数字,然后长度都为11位
让我们创建一个函数,对一个输入内容进行验证
function isPhoneNumber(candidate){
if(typeof candidate !== "string" || candidate.length!=11){
return false; //过滤明显不符合条件的输入内容
}
if(isNaN(candidate)){
return false; //过滤非纯数字的输入内容
}
return true;
}
虽然这段代码实现的很合理,检查了输入内容的类型和长度,但是对于一个简单的手机号码检查,看起来依然有太多的代码
现在考虑这样一种方式
function isPhoneNumber(candidate){
return /^1[0-9]{10}$/.test(candidate)
}
除了函数体内的一些深奥的语法以外,这种方式看起来更简洁,更优雅,不是吗?
这就是正则表达式的威力,而且这只是它冰山的一角.如果语法看起来像键盘上爬行的蜥蜴的话,也不要担心.
正则表达式进阶
让我们开始对正则表达式进行深挖,要学习正则表达式,就先了解正则表达式出现的由来
正则表达式解释
"正则表达式(regular expression)" 这个词源于中世纪的数学,当时,一个名叫Stephen Kleene的数学家,使用了一个名为"正则集合"的数学符号描述自动计算模式.但这不会帮助我们了解任何关于正则表达式的内容,那么让我们把它简单化,正则表达式通常被称为一个模式(pattern),是一个用简单方式描述或者匹配一系列符合某个语法规则的字符串.表达式本身包含了允许定义这些模式的术语和操作符.我们很快就会看到这些术语和操作符.
在 javascript 中,与大多数其他对象类型一样,有两种方式可以创建正则表达式:通过正则表达式字面量,或者通过构造 RegExp 对象的实例.
例如,如果要创建一个正则表达式 (或简称为正则(regex)),用于精确匹配字符串 "test" ,可以使用正则字面量:
var pattern = /test/;
正斜杠可能看起来有点奇怪,但是就像字符串使用引号进行界定的一样,正则字面量是用正斜杠进行界定的.
或者,我们可以构造一个 RegExp 实例,将正则作为字符串传入:
var pattern = new RexExp('test');
这两种格式在pattern变量中创建的正则表达式都是一样的.
在开发过程中,如果正则是已知的,则优先选择字面量语法,而构造器方式则是用于在运行时,通过动态创建字符串来构建正则表达式.
字面量语法优先于用字符串构建正则表达式的其中一个原因是反斜杠字符在正则表达式中发挥重要的作用(很快就能看到).但由于反斜杠字符在普通字符串中也是一个转义字符,所以,如果要在字符串内表示反斜杠,我们就要使用 \\
(两个反斜杠).这回让本来语法就很神秘的正则表达式变得更加怪异了.
除了表达本身,还有三个标志可以与正则表达式进行关联.
-
i
让正则表达式不区分大小写,所以/test/i
不仅可以匹配 "test" ,还可以匹配 "Test" "TEST" "tEsT" 等.
-
g
匹配模式中的所有实例,而不是默认的只匹配第一次出现的结果
-
m
允许匹配多个行,比如可以匹配文本区元素(textarea)钟德志这些标志将附加到字面量尾部(例如:
/test/ig
) 或者作为 RegExp 构造器的第二个字符参数 (new RegExp("test","ig")
) .
RegExp 对象方法
方法 | 描述 |
---|---|
compile | 编译正则表达式。 |
exec | 检索字符串中指定的值。返回找到的值,并确定其位置。 |
test | 检索字符串中指定的值。返回 true 或 false。 |
例子:
需要注意的是String对象也有个match方法,也可以检索目标字符串,用法和RegExp的exec方法类似,区别是
-
exec是正则表达式的方法,而不是字符串的方法,它的参数才是字符串
-
exec和match返回的都是数组,但是match是返回所有匹配的字符串合成的数组,但是正则表达式必须指定全局g属性才能返回所有匹配,不指定g属性则会返回一个只有一个元素的数组。exec永远返回与第一个匹配相关的信息,其返回数组包括第一个匹配的字串,所有分组的反向引用。
参考资料:http://www.jb51.net/article/46374.htm
术语与操作符
正则表达式,就像我们熟悉的大多数其他表达式一样,由术语和验证这些术语的操作符组成.在接下来的小节中,我们将了解这些术语和操作符,看看它们是如何用于表达模式的.
精确匹配
如果一个字符不是特殊字符或操作符(后续会进行介绍),则表示该字符必须在表达式中出现.例如,在/test/
正则中,有4个术语,它们表示这些字符必须在一个字符串中出现,才能匹配该模式.
一个接一个的字符,隐式表达了"后面跟着(followed by)"这样一个操作.所以,/test/
的意思是说, "t" 后面跟着 "e","e" 后面跟着 "s","s" 后面又跟着 "t".
匹配一类字符
很多时候,我们并不像匹配一个特定的字符,而是想匹配一个有限字符集中的某一个字符.我们可以通过将字符集放到中括号内,来指定该字符集操作符(也被称为字符类(character class)操作符): [abc]
上述示例,是说我们要匹配 "a" "b" "c" 中的任何一个字符.
有的时候,我们想要匹配一组有限字符集以外的字符.可以通过在中括号第一个开括号后面加一个插入符(^)来实现,比如:
[^abc]
其意义将改变为:除了 "a" "b" 或 "c" 以外的任意字符.
在字符集操作方面,还有一个更加重要的变异操作: 指定一个范围. 例如,如果要匹配 "a" 和 "m" 之间的任何一个小写字母,我们可以这样写: [abcdefghijklm]
但可以写成更加简洁的 [a-m]
中横线表示从 "a" 到 "m" 之间的所有字符 (包含 a 和 m,按字典顺序)都在该字符集内.
转义
并不是所有的字符和其字符字面量都是等价的.当然,所有的字母和十进制数字字符都能代表自己,但是,我们很快就会发现.像$ 和点(.) 这样的特殊字符,表示的是他们自身以外的东西,或者表示为验证术语的操作符. 事实上,我们已经看到了如何用[、]、-、^
字符表示它们自身以外的东西
如果我们需要匹配[、]、$、^或其他这样的特殊字符,该怎么办?在正则里,使用反斜杠可以对任意字符进行转义,让被转义字符作为字符本身进行匹配.所以,\[
表示要匹配[
字符.两个反斜杠(\\
)则匹配一个反斜杠
匹配开始于匹配结束
我们可能经常需要确保模式匹配一个字符串的开始,或者一个字符串的结束.插入符号(^),如果作为正则表达式的第一个字符,则表示要从字符串的开头进行匹配,这样/^test/
就只能匹配以 "test" 开头的字符串了.(注意,这只是&字符的一个重载,它还可以用于否定一个字符类集,比如 /[^abc]/
用于选择除了除了 "a" "b" 或 "c" 以外的任意字符)
类似的,美元符号$表示该模式必须出现在字符串的结尾:/test$/
.
同时使用^和$则表明指定的模式必须包含整个候选字符串.
重复出现
如果要匹配连续的4个 "a" 字符,可以用/aaaa/
来表示,但如果我们想匹配任意数量的相同字符串呢?
在重复选项上,正则表达式提供了很多方式.
-
在一个字符后面加一个问号( ? ),可以定义为该字符是可选的(也就是,可以出现一次,或根本不出现)
-
如果一个字符要出现一次或多次,可以使用加号(+),例如
/t+est/
可以匹配"test","ttest","tttest",而不能匹配"est" -
如果一个字符要出现零次或多次,可以使用星号( * ),例如
/t*est/
可以匹配"test","ttest","tttest",以及"est" -
也可以在字符后面的花括号里指定一个数字来表示重复次数,例如,
/a{4}/
表示匹配含有连续四个"a"字符的字符串 -
也可以在字符后面的花括号里指定两个数字(用逗号隔开)来表示重复次数区间,例如,
/a{4,10}/
表示匹配任何含有连续4个至10个 "a" 字符的字符串. -
次数区间的第二个值是可选的(但是要保留逗号),其表示一个开区间.例如,
/a{4,}/
表示匹配任何含有连续4个或多于4个 "a" 字符的字符串.这些重复操作符可以是贪婪的(greedy)或非贪婪的(nongreedy).默认情况下,它们是贪婪的:它们匹配所有的字符组合.在操作符后面加一个问号?字符(?操作符的一个重载),如 a+?,可以让该表达式变成非贪婪的:进行最小限度的匹配
举个例子,如果我们对字符串 "aaa" 进行匹配,正则表达式
/a+/
将匹配所有这三个字符,而非贪婪表达式/a+?/
则只匹配一个 "a" 字符,因为一个a字符就可以符合a+术语.
预定义字符类
有一些我们想匹配的字符,是不可能用字面量字符来表示的(如像回车的控制字符),还有一些我们可能经常想匹配的字符类,比如小数位数或一组空白字符.正则表达式语法提供了很多表示这些字符或常用类的预定义术语,这样在正则表达式中,我们就可以使用这些控制字符进行匹配了,因此,我们不需要再去依靠常用的字符集.
下面列出了这些术语以及它们表示的字符或字符集.
预定义术语 | 匹配内容 |
---|---|
\t |
水平制表符 |
\b |
空格 |
\v |
垂直制表符 |
\f |
换页符 |
\r |
回车 |
\n |
换行符 |
\cA: \cZ |
控制符,例如\cM 匹配一个Control-M |
\x0000:\xFFFF |
十六进制Unicode码 |
\x00:\xFF |
十六进制ASCII码 |
. |
匹配除了新行(\n )之外的任意字符 |
\d |
匹配任意数字,等价于[0-9]
|
\D |
匹配任意数字,等价于[^0-9]
|
\w |
匹配包括下划线的任意单词字符,等价于[A-Za-z0-0]
|
\W |
匹配任何非单词字符,等价于[^A-Za-z0-9]
|
\s |
匹配任何空白字符,包括空格,制表符,换页符 |
\S |
匹配任何非空白字符 |
\b |
匹配单词边界 |
\B |
匹配非单词边界 |
这些预定义集,让正则表达式看起来不那么过于神秘了.
分组
到目前为止,我们看到的操作符(如+和*)只能影响前面的术语.如果将操作符应用于一组术语,可以像数学表达式一样在该组上使用小括号.例如,/(ab)+/
匹配一个或多个连续出现的字字符串 "ab".
当正则表达式有一部分使用括号进行分组时,它具有双重责任,同时也创建所谓的捕获(capture).正则表达式有很多捕获,我们将在下面对其进行更深入的讨论.
或操作符(OR)
可以用竖线(|)字符表示或者的关系.例如:/a|b/
匹配 "a" 或 "b" 字符,/(ab)+|(cd)+/
则匹配出现一次或多次的 "ab" 或 "cd".
反向引用
正则表达式中最复杂的术语是在正则中所定义的捕获(captures)的反向引用,我们将在后面几节花很大篇幅来介绍捕获方面的内容,但现在就将捕获作为正则表达式中能够成功匹配术语时的候选字符串.
这种术语表示法是在反斜杠后面加一个要引用的捕获数量,该数字从1开始,如\1
,\2
等
举例来说,/^([dtn])a\1/
表示可以任意一个以 "d" "t" "n" 开头,且后面跟着一个 a 字符,并且后面跟着的是和第一个捕获相同字符的字符串.后面这一点很重要! 它和/^[dtn]a[dtn]/
不一样,因为a后面跟着的虽然也是"d" "t" "n" 中的一个,但可能和开头字符不同,例如:在全�局模式下,有两个字符串"dad" 和 "dat" /^([dtn])a\1/
会选中 "dad",而不会选中 "dat",
/^[dtn]a[dtn]/
既选中 "dad",也会选中 "dat"
要匹配像 " <strong>whatever</strong>" 这样的元素,不适用反向引用,是无法做到的,因为我们无法知道关闭标签和开始标签是否匹配.
例如:/<(\w+).+<\/\1>/g
就可以选中正常闭合的简单标签
在上面的代码中,我们使用\1
引用了表达式中的第一个捕获,在本例中,该捕获是标签名称
还有一个方法可以获取捕捉的引用,那就是通过调用String对象的replace()方法替换字符串.在这里,我们使用$1
$2
$3
语法表示每个捕获的数字.
例子:
var pattern=/([A-Z])/g;
var str="fontFamily";
console.log(str.replace(pattern,"-$1").toLowerCase()); //font-family
在上面的代码中,首先获取的捕获值(在本例中是大写字母F),在替换字符串中进行了引用(通过$1).这种方式允许我们制定一个替换字符串,即便是在运行之前还不知道它的值.这是个强大的武器.
这种可以引用正则表达式捕捉结果的能力,让很多本来很复杂困难的代码变得相当简单.其富有表现力的特性,将本来可能很迟钝,复杂且冗长的代码,最终变成了一些简短的语句.
分组与捕获
有时候我们需要在一个字符串里找到特定的数据,比如需要在爬虫爬到的数据里找到特定的数据,比如天气温度,用户id,特定标签里面的内容.拿在特定标签里找里面的内容来举例,用正则匹配的话我们通常会先找到这个标签,比如<(strong).+<\/\1>
,这样就能匹配到整个strong标签和strong标签里面的内容了,
我们找到了strong标签和它里面的内容,那么又怎么找到strong标签里面的test呢?
其实,只需要给''.+''加个括号就行
f于是我们看到匹配内容多了个test
由于返回的匹配是个数组,所以我们只需要用数组下标就能取到test这个值
g为什么加个括号就能取到test了呢 ?
我们前面已经知道了小括号具有双重责任,不仅可以用来分组,还会创建所谓的 "捕获" .
因此,使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。
既然小括号既有分组的作用,又有捕获的作用,如果我们只想分组,又不想捕获怎么办呢?
思考一下如下的正则表达式:
var pattern=/((good-)+)man/;
在单词 "man" 前面,允许前缀 "good" 出现一次或多次,并且希望捕获整个前缀.这个正则表达式需要两层括号
- 定义捕获(man 之前的所有字符串)的小括号
- 针对+操作符,对 "good-" 文本进行分组的小括号
一切运行正常,但由于括号分组的功能,不仅是单一目标捕获
要让一组括号不进行结果捕获,正则表达式的语法允许我们在开始括号后加一个 ?:
标记.这就是所谓的 被动表达式
将我们的正则表达式修改成如下这样:
var pattern=/((?:good-)+)man/;
该表达式只会为外层的括号创建捕获.内层括号被转换为一个被动子表达式
利用函数进行替换
String的replace()方法是一个强大且灵活的方法,将正则表达式作为replace()方法的第一个参数时,导致在该模式的匹配元素(全局匹配的话,就是多个匹配元素)上进行替换,而不是在固定字符串上进行替换.
举个例子,假如需要让所有的大写字符串都替换成"X",我们可以这样编写:
"ABCEDfg".replace(/[A-Z]/g,"X")
其结果为 "XXXXXfg". 很不错.
不过,获取replace()最强大的特性是可以接受一个函数作为替换值,而不是一个固定的字符串.
当替换值(第二个参数)是一个函数时,每个匹配都会调用该函数(记住,全局搜索会在源字符串中匹配所有的模式实例)并带有一串参数列表.
-
匹配的完整文本.
-
匹配到捕获,一个捕获对应一个参数
-
匹配字符在源字符串中的索引
-
源字符串
函数的返回值是即将要替换掉值.
这给了我们很多思考的余地,让我们在运行时确定应该替换的字符串,并掌握大量与匹配特性有关的信息.
例如,在下面的代码中,我们使用一个函数,动态地将中横线分割的字符转换成等价的驼峰拼写字符.
function upper(all,letter){
return letter.toUpperCase();
}
var pattern=/-(\w)(\w)/g;
var str="border-bottom-width";
console.log(str.replace(pattern,upper)); //borderBttomWdth
在这里,我们提供了一个正则表达式,用于匹配中横线字符后的任意一个字符.全局正则中的捕获结果就是该匹配的字符(不包括之中横线).函数在每次被调用的时候(本例中是两次),传入匹配的完整的字符串作为第一个参数,捕获结果(本例只有一个)作为第二个参数.我们对其他参数不感兴趣,所以没有指定他们.
函数在第一次被调用的时候,传入了 "-b" 和 "b" ,第二次被调用的时候传入的是 "-w" 和 "w",在本例中,将捕获字符转换成大写,并作为替换值返回.最终,我们将 "-b" 替换成了 "B",将 "-w" 替换成了 "W"
利用正则表达式解决常见问题
修剪字符串
将字符串前后多余的空格进行删除是一种常见的需求,但String对象却没有这种功能(最近才有).对于没有String.trim()方法的旧版浏览器,几乎所有的JavaScript库都提供了一种实现.
最常用的实现,类似如下代码:
function trim(str){
return (str||"").replace(/^\s+|\s+$/g,"");
}
console.log(trim(" #id div.class ")==="#id div.class"); //true