正则基础学习

正则表达式是一个威力巨大的处理字符串的工具,能够高效、神奇得完成对字符串的操作。相比较简单的字符串比较、查找、替换,正则表达式提供了更加强大的处理能力。正则表达式的价值就在于,不用正则来解决问题会让人疯掉,但是用了之后“纠结”的问题已不再是问题了。而且因为正则表达式用的不是一个固化的、具体的字符串来匹配字符串,而是抽象的模式的,所以只要正则写的规则没问题,一般都都能高效的完成任务。

虽然正则表达式看起来确实很像外星文,就像变魔术一样,魔术本身也不神奇,只是的观众不解其中奥妙。学会了其中的规则,我们再去使用,肯定会发出感慨:神奇、复杂、好用。

正则到底强在哪里呢?我们举个简单的例子:在一串包含数字以及英文字母的字符串中中找出数字并保存在数组中。代码如下:

不使用正则:
遍历字符串,利用字符串charAt()的方法将字符串中的数字检索出来,再push数组中,然后继续检索再push到数组中直到结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
    var str = '12 javascript 34 html5 33 php 77 css';
var arr = [];
var figure = '';
for(var i=0;i='0' && str.charAt(i)<='9'){
figure += str.charAt(i);
}else{
if(figure){
arr.push(tmp);
figure ='';
}
}
}
console.log(arr)//[ "12" , "34" , "5" , "33" , "77" ]

而使用正则只需要如下短短的一行代码

1
var arr = str.match(/\d+/g);

基础知识

书写风格

javascript中的正则是Perl5的正则表达式语法的大子集,所以在javascript中创建正则有js风格和petl风格两种。

  1. JS 风格: new RegExp(‘patten’,’ig’)
  2. perl风格:/patten/ig

JS风格其实就是通过RegExp对象来表示,而perl风格更普遍的叫法是RegExp直接量。这两个语法都是一样的,只是转义字符的写法不同。

什么是正则

正则表达式的结构与数学表达式很类似。

一个数学表达式由若干个“项”组成,“项”与“项”之间用加号或减号相连

为了方便理解,让我们先来看看大家一个典型的的数学表达式 (x+3)*2+y

这个数学表达式中(x+3)*2y分别是两个项。每个项又由若干个因子组成,因子之间用乘号或除号相连。这里第一个项有两个因子(x+3)2,而第二个项只有一个因子“y”。每个因子可以是一个简单的数,一个代数变量,也可以是放在括号里面的一个表达式,括号中的表达式称为子表达式。这里x+3就是一个子表达式。

与数学表达式的因子相对应,构成正则表达式的部件称为单位则与正则表达式的子表达式相对应。而从逻辑上讲,子表达式之间是串接的关系,一个字符串必须与每个子表达式依次相匹配,才算与这个表达式相匹配。

术语

为了更好的学习正则,我们再来学习下正则表达式的一些术语

  1. 匹配(matching)
    一个正则表达式“匹配”一个字符串,其实是指这个正则表达式能在字符串中找到匹配文本。

  2. 元字符(metacharacter)
    只有在字符组外部并且是在未转义之前的情况下,才是一个元字符。

  3. 子表达式(subexpression)
    子表达式指的一般是整个正则表达式中的一部分,通常是括号内的表达式,或者有|分隔的多选分支。子表达式由不可分割的单位组成。与多选分支不同的是,量词作用的对象是他们之前紧邻的子表达式。而如果量词之前紧邻的是一个括号保卫的自表达式,则不管其多么复杂都被视为一个单元。

匹配模式

上例中斜杠后面的 ig是匹配模式,可选的值有3个:i,g,m。其含义如下:

  • i:为 ignore case,即 忽略大小写。
  • g:为 global search,即全局搜索。
  • m:为 moltiline search,即多行搜索。

所以,一个完整正则表达式是由一个个子表达式组成的,而子表达式则是由各种符号组成,这些符号按照功能可以分成以下类:转义字符、预定义特殊字符、字符类、量词、贪婪模式和非贪婪模式、匹配位置、分组、非捕获性分组、前瞻(零宽断言)。

元字符组成部分

转义字符

什么是转义字符?在\后面加字符就可以转义为特殊字符。

例如: \n匹配一个换行符, \\匹配“\”。

预定义特殊字符

  1. \o:Nol字符。
  2. \t:水平制表符。
  3. \v:垂直制表符。
  4. \n:换行符。
  5. \r:回车符。
  6. \b:退格符。 只有出现在字符中才有效,即[](中括号)中。

字符类

  1. [ ]:表示范围,一个字符的集合,匹配该集合中的任意一个字符,例如 [abc]就可以匹配”css”中的c;
    如果上例前面加 ^元字符,形如[^asd],则表示匹配除了asd的其他字符;

如果觉得匹配的字符太多,而且类型相似,则可以用-元字符表示,那么上例就可以这么写[a-c]这么写,所以上例也可以这么写 [^a-d]

  1. \w\W:\w表示匹配任何ASCII字符组成的单词,等价于[a-zA-Z0-9];\W表示匹配不是ASCII字符组成的单词等价于[^a-zA-Z0-9]
  2. \s\S:\s匹配空白符,等价于[\t\n\x0B\f\r]\S则匹配非空白字符,等价于[^\t\n\x0B\f\r]
  3. \d\D:\d匹配数字字符,等价于[0-9]\D匹配数字字符,等价于[^0-9]
  4. .:javascript有点特殊,由于浏览器的解析引擎不同,.的匹配范围也有所不同。

    1. IE8以下:
      .匹配所有除了换行符/n换行符之外的任意字符。等同于[^\n\r]

    2. IE9以上以及其他浏览器
      .匹配所有除了换行符/n换行符和回车符\r之外的任意字符。等同于[^\n\r]

1
2
3
document.write(/./.test("\r") + "");
document.write(/./.test("\n") + "");
/*IE8以下输出true false;IE9以上及其他浏览器输出 false false*/

量词

首先我们得了解匹配量词都是匹配优先的,简单说就是匹配量词的结果总是尝试匹配尽可能多的字符,直到匹配上限为止,然后为了让整个表达式匹配成功,就需要“释放”之前优先匹配的字符,所以也被称为贪婪模式。

而既然有贪婪模式,则一定也有非贪婪模式。

对于贪婪模式和非贪婪模式影响的是被量词修饰的子表达式的匹配行为,既在贪婪模式下,在整个表达式匹配成功的前提下,尽可能多的匹配,而非贪婪模式在在整个表达式匹配成功的前提下,尽可能少的匹配。而且允许允许接下来的正则继续匹配。

贪婪模式的量词,也叫简单量词,如下:

{n}:n是一个正整数,表示前一个子表达式匹配n次。例如: /o{2}/匹配两次o,它可以匹配”footer“,但是不能匹配hot中的o。
{n,}:n是一个正整数,表示前一个子表达式至少匹配n次。例如:/o{2,}/,它可以匹配“footer”,也可以匹配“fooooooooooter”。
{n,m}:n、m都是正整数,表示至少匹配n次,至多m次。
?:等价于{0,1}
+:等价于{1,}
*:等价于{0,}
而在贪婪模式后加上 ?就变成了非贪婪模式。

贪婪模式和非贪婪模式

在上面提到的一个前提条件就是在整个表达式匹配成功,为什么要强调这个前提条件呢,看如下例子:

1
2
3
4
5
var pattern = 'aAaAaAb';
console.log(/a+/i.exec(pattern)); //aAaAaA
console.log(/a+?/i.exec(pattern)); //a
console.log(/a+b/i.exec(pattern)); //aAaAaAb
console.log(/a+?b/i.exec(pattern)); //aAaAaAb

全部是在忽略大小写的模式下:

  1. 第一个匹配结果解释:采用贪婪模式,在匹配第一个“a”时,整个表达式匹配成功了,由于采用了贪婪模式,所以仍然向右匹配,向右再也没有可以成功匹配的子字串,匹配结束,最终匹配结果为“aAaAaA”
  2. 第二个匹配结果解释:采用非贪婪模式,在匹配第一个“a”时,整个表达式匹配成功了,由于采用了非贪婪模式,所以结束匹配,最终匹配结果为“a。”
  3. 第三个匹配结果解释:采用贪婪模式,所以a+仍然可以匹配到“aAaAaA”,但是由于后面的 b无法匹配成功,所以为了让整个表达式匹配成功,a+必须让出前面的匹配内容,所以最终匹配结果为“aAaAaAb”。
  4. 第四个匹配结果解释:采用非贪婪模式,所以a+任然可以匹配到“a”,但是由于后面的 b无法匹配成功,所以为了让整个表达式匹配成功,a+必须继续匹配后面的直到“b”,所以最终匹配结果跟采用贪婪模式的匹配结果一样,也为“aAaAaAb”。
  5. 所以,不管是贪婪模式还是非贪婪模式,都只有在整个表达式匹配成功的前提下量词才能影响字表达式的匹配行为。贪婪跟非贪婪模式主要功能是提高匹配效率,贪婪模式下可能会越过后面的正则,从而会导致匹配的回溯问题。所以在前面的正则坑会会越过后面的正则的情况下,请使用非贪婪模式。

匹配位置

前面说的量词是修饰子字符串的重复次数,而匹配位置则是来表示子字符串的出现位置,匹配的只是一个位置,所以是零宽度的。

  1. ^:匹配文字的开头。如果正则表达式的匹配模式设置为’,’m’则也匹配每个换行符或者回车符之后的位置。
  2. $:匹配文字的开头。如果正则表达式的匹配模式设置为’,’m’则也匹配每个换行符或者回车符之前的位置。
  3. /b:匹配单词边界,不匹配任何字符。

所谓的“单词”,就是位于\w(ASCII单词)和\W(非ASCII单词)之间的边界,或者位于ASCII单词与字符串开始或者结尾的合法位置。所以\/bjava/b\不匹配“javascript is more than java”中的javascript中java而只匹配之后的单词“java”。

而因为javascript只支持ASCII字符不支持Unicode的,所以在javascript这门语言中\w就可以等价于[a-zA-Z0-9],也因为于此,javascript中\w是不包括中文已经其他Unicode码特殊符号的,如下例子:

1
2
3
var str = "html5_css3中文_h5$c3&汉字%";
console.log(str.match(/\w+/g)); //"html5_css3" , "_h5" , "c3"
console.log(str.match(/.\b./g));//"3中" , "文_" , "5$" , "3&"

第一个例子中\w+匹配了”html5_css3” , “h5” , “c3”三个字符串,而其他的因为javascript只能匹配ASCII码的字符,所以除了字母、数字、““以及”$“的字符就都成单词的边界;而当使用.\b.(除了换行符之外的任意字符,.匹配了那些\w无法识别的Unicode码字符)匹配时,我们又得到”3中” , “文“ , “5&” ,说明这个字符串中有4个分界点5个子字符串,分别在”3中” , “文“ , “5&”之间,而四个子字符串分别是”html5_css3”,”中文,”_h5”,”$c3”,”&汉字%”。

所以,在处理一些字符串时,如果要使用\b得先确认是否还有ASCII码的字符。

注意:\b在[]中表示退格。

分组

学习完以上的,应该会知道中括号用来限定字符类的范围,大括号则用来指定重复的次数,而小括号除了限制多选项的范围以及将若干字符组合为一个单位让量词能影响这个单元。还有一个用途就是,小括号能”记住“它们匹配成功的文本,在正则表达式的后面引用前面“记住”的匹配文本,通过 \后加以为或者多位数字来实现,也就是“反向引用”。

看实际例子吧:

1
2
3
4
5
6
7
//1分组+量词
console.log(/(js){2}/.test("jsjs"));//true
//2分组+范围
console.log(/[JL]script/.test("Lscript"));//true
//3反向引用
console.log(/([jJ])s\1/.test("jsJs"));//false
console.log(/([jJ])s\1/.test("jsjs"));//true

例1和例2将括号内的若干字符组合为一个单位。而例3因为\1引用的是之前匹配成功的字符串,所以例三中\1就只能匹配”js“而不能匹配”Js“。

然后介绍第二个分组的符号|。

与小括号不同,小括号内的是一个整体(独立的子表达式),而|分割开的各分支是多选分支,即你可以选择|前面的也可以选择|后面的,如果有多个|隔开则是多选几。如下:

1
2
3
4
var reg = /(html5|css3|js)!!/;
console.log(reg.test("html5!!"));//true
console.log(reg.test("css3!!"));//true
console.log(reg.test("js!!"));//true

非捕获性分组

对带圆括号的子表达式的引用确实强大,但是既然能够反向引用,正则引擎肯定是保存了括号内的一些信息。所以从效率角度来看,如果只是为了分组而不反向引用的话就采取非捕获性分组的方法。要创建一个非捕获性分组,只要在捕获性分组的左括号的后面紧跟一个问号与冒号就行了。

从字面意思来看:非捕获分组能分组,但是不能捕获各个组的信息。如下:

1
2
var pattern1 = "JS,HTML5,CSS";
console.log(pattern1.replace(/(\w+),(?:\w+)/, "$2,$1"));//$2,JS,CSS

前瞻

前瞻也是属于零宽断言,说白了就是匹配位置的高级变体。前面我们说过的只是单纯的开头、结尾以及单词的边界,而前瞻的匹配则更加随意,如下:

  1. (?=p):要求之后的字符必须与p匹配
  2. (?!p):要求之后的字符必须不与p匹配

如下实例:

1
2
3
4
5
6
var reg1 = /java(?!Scrit)/;
var reg2 = /java(?=Scrit)/;
console.log(reg1.test("javaScrit")); //false
console.log(reg1.test("javaB"));//true
console.log(reg2.test("javaScrit")); //true
console.log(reg2.test("javaB"));//false

前瞻的作用就是给正则增加一个附加条件,只有满足条件,才能继续走下去,前瞻匹配的结果是不纳入结果里的,只是一个条件。

支持正则方法有支持正则的字符串方法和正则自身的方法

支持正则表达式的 String 对象的方法

字符串搜索:

search()方法用于检索字符串中指定的子字符串,返回匹配的字符的位置(0-~)。

如果没有找到匹配的字符,则返回-1;将忽略RegExp中的全局模式,只返回第一个匹配的位置。

1
2
var pattern = "hello html5 js css";
console.log(pattern.search(/Html5/i));//6

字符串匹配:

match()方法可以返回匹配结果的数组,并且依赖于regexp的全局标志g。如果没有全局标志g,则只匹配一次;如果有,则匹配多次直到结束,最后返回一个存有匹配匹配文本的数组。

match()即不提供与子表达式匹配的文本的信息,也不声明每个匹配子串的索引位置。如果您需要这些信息,可以使用 RegExp.exec()。

1
2
var pattern="2012 years 12 month 20 is the end of the world";
console.log(pattern.match(/\d+/g));//["2012","12","20"]

字符串替代:

replace()方法用于替换字符串或者正则表达式匹配的子字符串,并且也依赖于regexp的全局标志g。如果没有全局标志g,则只替换第一个匹配的子字符串;如果有,则替换所有匹配的子字符串。

replace()的第二个参数可以是字符串,也可以是函数。如果是字符串,则由每个匹配的字符串替换,其中 $ 具有特殊的含义:

  1. $n:其中n表示1-99,表示匹配的子字符串中的第n个,n就是带圆括号的子表达式的位置。
  2. $&:全部匹配的子字符串
  3. $`:匹配子串左侧的文本
  4. $':匹配子串右侧的文本
1
2
3
4
5
var pattern1 = "JS,HTML5,CSS";
var replace1 = pattern1.replace(/(\w{1,}),(\w+)/, "$2,$1");
console.log(replace1);//HTML5,JS,CSS
var replace2 = pattern1.replace(/(\w+),/g,"$1-");
console.log(replace2);//JS-HTML5-CSS

字符串分割:

split()方法用于把一个字符串分割成字符串数组。该方法有两个参数,第一个参数是指定分割的边界;第二个参数是指定返回数组的长度,如果没有则字符串之间的都会被分割。

若使用 split("") 则会将单词分割成字母

1
2
3
4
5
var pattern = "HTML5 JS CSS";
var sWord1 = pattern.split(" ");
var sWord2 = pattern.split("");
console.log(sWord1);//[ "HTML5" , "JS" , "CSS" ]
console.log(sWord2);//[ "H" , "T" , "M" , "L" , "5" , " " , "J" , "S" , " " , "C" , "S" , "S"]

RegExp 对象的方法

test:

test()方法用于检索要检测的字符串是否存在,若含有与regExp相匹配的文本,则返回true,否则返回false

exec:

exec()方法用于匹配字串,跟不是全局的match()方法很类似,但是它不仅能检索字符串中指定的值,返回找到的值,还能确定其位置。 比match()强大。如果利用 exec() 的lastIndex属性反复调用同样可以模拟match()全局检索字符串的效果。