30分钟入门正则表达式

正则表达式既简单又博大精深。半个小时肯定是不能精通,不过入门应该是够了,接下来赶紧把它用到实际编程中,熟能生巧,多用就是走向精通的捷径。

什么是正则表达式?

简单来说就是按照某种规则去匹配符合条件的字符串,正则表达式描述的就是这个规则

相信大家都用过编辑器(如sublime)里的查找功能。你在搜索框输入几个字符,编辑器就会帮你找到含有这些字符的字符串。这里你输入那几个字符其实就是一种规则,编辑器找到的就是符合这个规则的字符串。只不过这样的查找规则过于简单,很多时候不能找出我们想要的东西。正则表达式的出现正是用来解决这个痛点的。通过他我们能够描述出精确得多的规则,也就更容易找到我们想要的东西。

正则表达式就像JSON一样,并不是某个语言独有的。 大部分编程语言都支持正则表达式,本文重点讲讲Javascript中的正则表达式。

在线可视化工具

正则表达式的规则虽然简单,不过一长串字符还是很容易让人看晕的,debug也不方便。这里我推荐一个在线的可视化工具,他能够很形象地描述正则表达式。下面我简单说一下怎么看它给出的结果。

我随便写了一个正则表达式\b\d{3}.?\w+\d*\b。什么意思先不用管,我们直接看结果。
image

第一个word boundary就表示说这是一个单词的边界,digit就表示数字,这些都用英文写的很清楚了我就不细说了(也说不完)。重点看后面那些循环和分支。第一个digit那里可以看到有一个圈,他代表循环,2times表明循环两次,加上原本那个digit就是有三个digit。明显这里就是三个digit的简写。后面any character上面有一条空的分支,表明这里可以有一个任意字符,也可以没有。再后面一个digit有一个空分支,也有一个表示循环的圈,圈上没有写明次数,这里的意思是可以有一个digit,可以有多个(无上限),也可以一个也没有。

有没有觉得很形象?没有这感觉的话可能是因为你还没学正则的那些规则,没关系,学了规则之后再回来看,到时候你会发现这正是学习和debug的利器。

RegExp对象

JS通过内置的RegExp对象支持正则表达式,跟其他JS对象一样,实例化RegExp对象也有两种方法:

  • 字面量:var reg = /\bis\b/g;(g是修饰符,表示进行全文搜索匹配)

  • 构造函数:var reg = new RegExp ('\\bis\\b' , 'g' );(字符串中的\是有含义的字符,所以我们需要转义)(其他在后面被介绍的,有特殊含义的字符,当我们需要匹配他们的时候也需要用\转义)

元字符

正则表达式由两种基本字符类型组成:

  • 原义文本字符,例如’a’就是指字母’a’
  • 元字符,类似我们之前用过的\b。下面列举了一些元字符
字符 含义
\t 水平制表符
\v 垂直制表符
\n 换行
\r 回车
\0 空字符
\f 换页
\cX 与X对应的控制字符(ctrl+X)

还有一些具有特殊含义的标点符号也是元字符:* + ? $ ^ . | \ ( ) { } [ ]

元字符当然不止这些,下面我们将结合他们的用法慢慢介绍。

字符类

一般情况下正则表达式的一个字符对应字符串的一个字符,例如ab\t的含义就会一个字母a一个字母b再加一个水平制表符

有时候我们不是想匹配某个字符而是某一类字符,例如不只是a,而是a,b,c都行。这种情况下我们可以使用元字符[]来构建一个简单的类。所谓类是指符合某些特性的对象,一个泛指,而不是特指某个字符。
例如:表达式[abc]把字符a或b或c归为一类,意思是’a’,’b’,’c’都行。

取反

有时候我们不是要匹配某些字符而是躲开某些字符。这时候我们可以用元字符^创建反向类。意思是不属于某类的内容,如[^abc]表示不是字符a或b或c的内容

范围类

有时候我们需要匹配的那一类字符太多,比如说我们想匹配一个任意小写字母,傻乎乎地写上[abcdefg......]既麻烦又不简洁。这时候我们可以使用范围内。就像这样:[a-z],这表明我们要匹配任意一个从a到z的字母。

有几点我们要注意的。

  • 这是一个双闭区间,包含了a和z。
  • 类的内部是可以连写的,例如可以这样:[a-zA-Z],意思是匹配任意一个a到z或者A到Z的字母。
  • 两个字符之间的短横线会被认为是在表示范围,如果我们就是想匹配短横线,我们可以把它加在后面,例如这样:[a-z-],意思是任意一个a到z的字母或者短横线。

预定义类

为了进一步简化我们的正则表达式,我们可以使用下面这些预定义类。

字符 等价类 含义
. [^\r\n] 除了回车符和换行符以外的所有字符
\d [0-9] 数字字符
\D [^0-9] 非数字字符
\s [\t\n\x)b\f\r] 任何空白字符,包括空格、制表符、换页符等等
\S [^\t\n\x)b\f\r] 非空白符
\w [a-zA-z_0-9] 包括下划线的任何单词字符
\W [^a-zA-z_0-9] 非单词字符

修饰符

前面的例子用到一个修饰符g,除了g以外还有其他修饰符,他们的意思如下:

修饰符 含义
g 全局搜索,而不是搜索到第一个就停止
i 忽略大小写
m 多行搜索 (把换行符后的当新的一行)

使用这些修饰符的话就像上面说过的例子一样,如果是通过字面量实例化,就在最后的/后面加上修饰符,如果是通过构造函数,就作为第二个参数传递给构造函数。

定位符

定位符能够使你的正则表达式匹配特定位置的字符。

字符 含义
^ 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 ‘\n’ 或 ‘\r’ 之后的位置。(不是在中括号中就是边界的意思)
$ 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 ‘\n’ 或 ‘\r’ 之前的位置。
\b 单词边界。例如,’er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。
\B 非单词边界。与上面相反,’er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。

举个例子,有输入字符串"12A1A",我们只想匹配开头的数字,不想匹配中间的。这时候我们可以用这样的正则表达式:^\d

注意: 在用^匹配开头的时候应将^放在表达式开头,如^\d,而在用$匹配结尾的时候则需要放在表达式后面。

量词

有时候我们想匹配多个相同的字符,例如匹配3个数字。我们当然不会这么写:\d\d\d,这时候我们就用到了量词。

字符 含义
? 出现零次或一次(最多出现一次)
+ 出现一次或多次(至少出现一次)
* 出现零次或多次(任意次)
{n} 出现n次
{n,m} 出现n到m次
{n,} 至少出现n次

贪婪模式与非贪婪模式

大家有没有想过,如果我们在用上面提到的量词时给出的量词时一个范围,例如\d{3,6}时,究竟会匹配到3到6次的哪一个呢?

其实在这种情况下,正则表达式选择次数的时候会有两种模式:

  • 贪婪模式
    • 这是默认模式。
    • 尽可能多的匹配。例如12345678.replace(\d{3-6},'X')的结果为X78。虽然3,4,5次都符合,不过最终匹配的是6次。
  • 非贪婪模式
    • 让正则表达式尽可能少的匹配,也就是说一旦匹配成功不再继续尝试。
    • 只需要在量词后加上?
    • 同样举上面的例子12345678.replace(\d{3-6}?,'X')此时的结果为XX78。123和456都被匹配到了。

分组与反向引用

首先思考一个例子,假如我们想把形如2017-8-3这样的日期修改成8/3/2017这样的日期,那么需要怎么做呢?本节讲到的分组与反向引用可以帮组你。

使用( ) 可以达到分组的功能,使量词作用于分组。例如(hello){3}表示hello重复三遍。

使用|,可以达到或的效果。他会把表达式分成前后两部分,两部分任意一个匹配成功都行。有时候我们并不想把整个表达式分成两部分,只想其中一段分成两部分。这时候我们可以利用分组。WEf(on|ca)serr 意思是中间是on或者ca都行。

反向引用。使用 $ 能够捕获分组的内容(类似变量的概念)。$1表示的就是第一个分组,以此类推可以表示第n个分组。举个例子:

1
2
'2016-2-14'.replace(/(\d{4})-(\d{2})-(\d{2})/g, '$2/$3/$1')
// 结果被替换为2/14/2016

忽略分组。有时候我们不想捕获某些分组,只需要在这个分组内加上?:就可以了。例如(?:hello).(world)这样hello这个分组就不会被捕获,此时world算第一个分组。

前瞻与后顾

首先说明一下什么叫『前』什么『后』。

正则表达式从文本头部向尾部开始解析,因此文本尾部的方向就叫做『前』。 前瞻就是正则表达式在匹配到规则时,向前检查是否符合断言后顾就是把反向换过来。

这里所说的断言其实用来限定匹配字符但又不属于匹配字符的东西。举个例子,我们要匹配一个跟在一个字母后面的数字,这里这个『跟在一个字母后面』就是断言

JS不支持后顾
符合和不符合断言称为肯定(正向)匹配和否定(负向)匹配。

名称 正则 备注
正向前瞻 exp(?=assert) exp表示规则,assert表示断言。后同
负向前瞻 exp(?!assert)
正向后顾 exp(?<=assert) JS不支持
负向后顾 exp(?<!assert) JS不支持

举个例子:

1
2
'a2*34vv'.replace(/\w(?=\d)/g, 'X')
// 结果为X2*X4VV 表明只有a和3符合要求,注意正则表达式中,括号内的部分仅为断言,所以不会被替换。

JS正则表达式对象(RegExp)

前面说过虽然很多编程语言都支持正则表达式,不过这里主要讲的是Javascript下的。因此我们在这节讲讲JS内建的RegExp对象。

对象属性

属性 类型 含义 默认值
global 布尔值 是否全文搜索 false
ignoreCase 布尔值 是否忽略大小写 false
multiline 布尔值 是否多好检索 false
source 字符串 正则表达式的文本
lastIndex 整数 若匹配模式中含有g,这个属性储存整个字符串中下一次检索的开始位置 0

注意这些属性时只读的。

对象方法

  • RegExp.prototype.test(str)
    • 测试一下在给定字符串中能否匹配到,能就返回true。
    • 如果正则表达式有修饰符g,即全局搜索。那么每一次调用test方法,都会从上一次的lastIndex开始搜索,直到匹配不到返回false才从头开始。例如有字符串’12cd’,正则表达式是/\d/。第一次调用test方法返回true,lastIndex为1,第二次调用返回true,lastIndex为2,第三次调用返回false,lastIndex重置为0,第四次调用返回true,lastIndex又变回1。
    • 鉴于有这样的特性,我们建议在使用这个方法时要回归他的本意,即测试某字符串能否匹配到你想要的东西,知道有没有就行了,不关心有多少个在哪里等信息(一定要这些信息的话就改用下面说到的方法)。
    • 因此使用这个方法时不要给正则表达式加上修饰符g。
  • RegExp.prototype.exec(str)
    • 使用正则表达式对字符串执行搜索,并将跟新全局RegExp对象的属性以反映匹配结果。
    • 如果没有匹配的文本就返回null,否则返回一个结果数组。
      • index声明匹配文本的第一个字符的位置。
      • input存放被检索的字符串string。
    • 非全局调用情况
      • 代用非全局RegExp的exec()时,返回数组。
      • 第一个元素是与正则表达式相匹配的文本。
      • 第二个元素是与RegExpObject的第一个子表达式相匹配的文本(如果有)。
      • 第三个元素是与RegExp对象的第二个子表达式相匹配的文本(如果有),以此类推。
    • 看例子:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      var reg3 = /\d(\w)(\w)\d/;
      var reg4 = /\d(\w)(\w)\d/g; // 全局
      var ts = '$1az2bb3cy4dd5ee'; // 测试用字符串
      var ret3 = reg3.exec(ts);
      console.log(reg3.lastIndex + '\t' + ret.index + 't' + ret.toString());
      console.log(reg3.lastIndex + '\t' + ret.index + 't' + ret.toString()); // 执行两次
      // 结果为:
      // '0 1 1az2,a,z'
      // '0 1 1az2,a,z'
      // 两次结果一样
      while(ret4 = reg4.exec(ts)){
      console.log(reg4.lastIndex + '\t' + ret.index + '\t' + ret.toString());
      }
      // 结果为:
      // '5 1 1az2,a,z'
      // '11 7 3cy4,c,y'
      // 两次结果不一样

字符串对象有关正则表达式的方法

JS中的字符串对象其实也支持正则表达式,下面是这个对象有关正则表达式的一些方法。

  • String.prototype.search(reg)
    • search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。
    • 返回第一个匹配结果的index,找不到返回-1.
    • search()方法不执行全局匹配,会忽略修饰符g,并且总从字符串的开始进行检索
  • String.prototype.match(reg)

    • match()方法将检索字符串,以找到一个或多个与regexp匹配的文本
    • regexp有无修饰符g对结果影响很大。
    • 非全局调用

      • 返回数组的第一个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本
      • 除了常规的数组元素以外,返回的数组还含有2个对象属性
        • index声明匹配文本的原始字符正字字符串中的位置
        • input声明对stringObject的引用
      • 看一个例子
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        var reg3 = /\d(\w)\d/;
        var reg4 = /\d(\w)\d/g; // 全局
        var ts = '$1a2b3c4d5e'; // 测试用字符串
        var ret = ts.match(reg3);
        console.log(ret);
        console.log(ret.index + '\t' + reg3.lastIndex);
        // ["1a2", "a"]
        // "1 0"
        // 表明不会影响正则表达式的lastIndex属性
    • 全局调用

      • 如果regexp具有g,则match()方法将执行全局搜索,找到字符串中的所有匹配子字符串
        • 如果没有找到,返回null
        • 如果找到一个多多个,则返回一个数组
      • 数组元素中存放的是字符串中所有的匹配子串,而且也没有index属性或者input属性
      • 看一个例子
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        var reg3 = /\d(\w)\d/;
        var reg4 = /\d(\w)\d/g; // 全局
        var ts = '$1a2b3c4d5e'; // 测试用字符串
        ret = ts.match(reg4);
        console.log(ret);
        console.log(ret.index + '\t' + reg4.lastIndex);
        // ["1a2", "3c4"]
        // "undefined 0"
  • String.prototype.split(reg)

    • 一般用法'a,b,c,d'.split(','); // ["a", "b", "c", "d"]
    • 除此之外我们还可以传入正则表达式实现复杂点的分割:'a1b1c2d'.split(/\d/); // ["a", "b", "c", "d"]
  • String.prototype.replace
    • String.prototype.replace(str,replaceStr)
    • String.prototype.replace(reg,replaceStr)
    • String.prototype.replace(reg,function)
      • function会在每次匹配替换时调用,有四个参数:
        • 匹配字符串
        • 正则表达式分组内容(如果没有分组则没有这个参数)
        • 匹配项在字符串中的index
        • 原字符串

Reference

http://blog.guowenfh.com/2015/12/01/Regexp-basis/

http://www.runoob.com/regexp/regexp-tutorial.html

http://www.jb51.net/tools/zhengze.html

http://www.imooc.com/learn/706