很长一段时间没有总结写博客了,也不是因为有多忙,而是在闲暇的时间人就变懒了,各种电影、游戏打发了时间。经过一段时间的低迷,又重新燃气了学习的欲望,所以写博客这个习惯还是要保持下去。前不久为了学习 PVP 游戏开发买了一个阿里云的服务器,闲下来后觉得有一些浪费,准备干点儿什么。因为平时混迹各种平台,账号太多记不住,打算写一个简单的网页,用于管理这些账号,于是又开始了 Web 的学习。在学到 JavaScript 正则表达式的时候发现自己的基础还是不太好,以前的东西基本上忘了,所以把正则表达式好好“复习”了一边。
什么是正则表达式
正则表达式是由一个字符序列形成的搜索模式,当你在文本中搜索数据时,你可以用搜索模式来描述你要查询的内容。正则表达式可以是一个简单的字符,或一个更复杂的模式。正则表达式可用于所有文本搜索和文本替换的操作。
大部分编程语言都支持正则表达式,每种语言又有细节上的差异,本文只对 Java 和 JavaScript 的正则表达式做一个总结。
语法
在 JavaScript 中,正则表达式的形式是:
/表达式/[修饰符]
其中表达式是主体,修饰符可以省略。
简单模式
先来一个最简单的正则表达式:
1 | var regex = /boy/; |
match() 函数返回的是 RegExpMatchArray 对象,里面记录了这一次匹配的结果,关于 match() 函数还有其他和正则表达式有关的函数后面再详细介绍。
执行上面的代码(本人使用 node.js 命令行执行),控制台会出现如下信息:
1 | [ 'boy', index: 17, input: 'I am a beautiful boy.' ] |
结果的含义很明显,’boy’ 表示满足表达式匹配到的字符串,index 表示匹配到第一个字符的位置,从 0 开始,input 是输入的要匹配的字符串。
这是正则表达式最简单的模式,直接匹配指定的字符串,也是用的较多的一种模式。
修饰符
修饰符用于增加正则表达式的匹配能力,有三个修饰符可以选择:
- i:忽略大小写
- g:全局匹配
- m:多行匹配
修饰符 i
忽略大小写,这个很好理解,就拿上一节的例子来说,稍微改动一下:
1 | var regex = /boy/; |
这里有一个新的函数 test(),用于测试一个字符串是否能匹配某个正则表达式,返回 true 或 false。
执行代码,返回 false,如果加上 i 修饰符:
1 | var regex = /boy/i; |
此时结果返回 true,这样匹配过程忽略了大小写。
修饰符 g
正则表达式在默认情况下搜索到第一个匹配的字符串就会停止,加上 g 修饰符便会进行全局匹配,直到字符串被搜索完,来一个例子:
1 | var regex = /a/g; |
上面的代码如果不加修饰符 g,结果只有一个 ‘a’,加上 g 之后会匹配到三个 ‘a’,这便是全局匹配。
修饰符 m
使用这个修饰符之前要先了解下另外两个元字符:^ 和 $,这两个元字符分别表示要匹配的字符串必须以 ^ 后面的表达式开始,以 $ 前面的表达式结束,拿 ^ 举例来说:
1 | var regex = /^a/; |
上面的代码如果不加元字符 ^,结果返回 true。但加上之后表示要匹配的字符串必须以 ‘a’ 为第一个字符,所以返回 false。$ 的用法类似。
元字符是正则表达式的特殊字符,用于指定各种匹配规则,关于这部分后面再详细介绍。
有什么办法让其匹配成功呢?m 修饰符可以上场了,m 修饰符的作用是修改 ^ 和 $ 在正则表达式中的作用,让它们分别表示行首和行尾。意思就是加上 m 修饰符,如果目标字符串中有换行符 \n,则 ^ 和 $ 可以对每一行进行匹配,修改下上面的例子:
1 | var regex = /^a/m; |
执行代码会返回 true,’\na’ 表示第二行,加上 m 修饰符后便是多行匹配,只要行首是 ‘a’,则匹配成功。
混合使用
修饰符可以混合使用,再修改下上面的例子:
1 | var regex = /^a/gm; |
执行上面的代码,控制台输出结果:
1 | [ 'a', 'a' ] |
很好理解,不做解释。
RexExp 对象
RexExp 是 JavaScript 中用来控制正则表达式的对象,其构造方式有以下两种:
1 | var regex = /表达式/[修饰符]; |
1 | var regex = new RegExp("表达式"[, "修饰符"]); // 注意表达式和修饰符这时是一个字符串,要加单引号或双引号 |
这两种构造方式构造出来的正则表达式没有任何区别,但在不需要改变对象的情况下建议使用第一种,性能会略微好一点。
属性
RegExp 对象有几个内置属性:
- global:该对象是否开启全局匹配模式,布尔值
- ignoreCase:该对象是否开启忽略大小写模式,布尔值
- multiline:该对象是否开启多行模式,布尔值
- lastIndex:该对象下一次开始匹配的位置,即上一次匹配结束的位置(匹配到的字符串的末尾位置),正整数
- source:该对象的源文本,不包含修饰符,字符串
函数
RegExp 对象有几个函数:
test()
上面使用过这个函数,用于测试一个字符串是否能匹配某个正则表达式,参数为字符串,返回 true 或 false。该函数将忽略 g 修饰符,只要搜索到符合正则表达式的字符串即匹配成功。如果正则表达式含有 g 修饰符,那么每次使用 test() 将从 lastIndex 位置开始匹配,匹配成功会更新 lastIndex 属性值。若没有 g 修饰符,test() 将从字符串起点开始匹配,也不会更新 lastIndex 属性值。
1 | var regex = /am/; |
执行上面的代码,控制台输出如下信息:
1 | true, 0 |
给正则表达式加上 g 修饰符:
1 | var regex = /am/g; |
再次执行代码,控制台输出如下:
1 | true, 4 |
exec()
搜索一个字符串中的符合正则表达式的匹配,参数为字符串,返回 RegExpExecArray 对象,记录了匹配的结果。该函数同样忽略 g 修饰符,若正则表达式中没有 g 修饰符,exec() 将从字符串的起始位置搜索,只要搜索到符合正则表达式的字符串就停止,也不更新 lastIndex 属性。如果有 g 修饰符,会从 lastIndex 位置开始搜索,搜到到符合正则表达式的字符串便停止,而且更新 lastIndex 属性。这个原理和 test() 函数其实是一样的,不再举例。
RegExpExecArray 对象中有一个 index 属性,这个属性是记录匹配到的字符串的开始位置在原始字符串中的索引,而 RegExp 对象中的 lastIndex 属性是记录匹配到的字符串的末尾位置在原始字符串中的索引,这个上面有提及到。
字符串对象
正则表达式服务的就是字符串,所以要使用好正则表达式,需要对字符串有一定的了解。本文只对字符串中几个和正则表达式有关的函数进行介绍。
search()
搜索一个字符串中的符合正则表达式的匹配,参数可以是正则表达式,也可以是字符串,返回匹配到的字符串的开始位置在原始字符串中的索引。该函数会忽略正则表达式中的 g 修饰符和 RegExp 对象中的 lastIndex 属性,即总是会从字符串的起始位置搜索,也不更新 lastIndex 属性,如果未搜索到返回 -1。
1 | var regex = /am/; |
执行上面代码,返回结果为 2。
match()
搜索一个字符串中的符合正则表达式的一个或多个匹配,参数可以是正则表达式,也可以是字符串,返回 RegExpMatchArray 对象,记录了匹配的结果。该函数忽略 RegExp 对象中的 lastIndex 属性,总是会从字符串的起始位置搜索,不更新 lastIndex 属性。如果正则表达式中没有 g 修饰符,只搜索到第一个匹配的字符串就会停止。如果有 g 修饰符,那么会匹配结果会是一个数组,包含所有能匹配到的字符串。
1 | var regex = /a/g; |
执行上面的代码,控制台输出如下:
1 | [ 'a', 'a', 'a' ] |
RegExpMatchArray 对象中也有一个 index 属性,记录匹配到的字符串的开始位置在原始字符串中的索引。
字符串对象的 match() 函数和 RegExp 对象的 exec() 函数功能很相似,区别在于 match() 函数不需要关心上一次匹配后记录的位置索引,而且在某些情况下搜索到一个匹配结果后还能继续搜索匹配。
split()
字符串分割,按照正则表达式将一个字符串分割成若干个,参数有两个,第一个可以是正则表达式,也可以是字符串,第二个用于指定返回数组的个数,默认为最大个数,函数返回一个数组。该函数只会分割一次,没有全局分割和上一次分割的概念。
1 | var regex = /a/g; |
执行上面代码,控制输出如下信息:
1 | [ 'I ', 'm ', ' be' ] |
字符串对象还有一个 replace() 函数,该函数用法较为复杂,和很多元字符相关联,后面再详细介绍。
字符串的函数有一个共同点,就是参数可以是正则表达式,也可以是字符串,如果传入字符串,其实就是简单模式的正则表达式(没有任何元字符和修饰符)。
元字符
正则表达式中的元字符是包含特殊含义的字符,它们有一些特殊的功能,可以控制字符串匹配的方式。如果想要使元字符用作普通字符,在其前面加上反斜杠。
转义字符
转义字符只有一个:\,主要用于将元字符标记为普通字符,比如 /\d/(下节将要介绍)是匹配所有数字,如果改为 /\d/,则只会匹配 ‘\d’ 这个字符串。
单个字符
这类元字符只能匹配字符串中的一个字符:
元字符 | 匹配规则 |
---|---|
. | 匹配除换行符以外的所有字符 |
或 | 匹配该元字符两端的任意一个字符,比如 x或yood 可以匹配 ‘xood’ 或者 ‘yood’ |
[] | 匹配中括号里的任意字符,里面的内容可以是字符,也可以是一个范围,用破折号 - 表示区间,比如 [0-9a-z] 表示匹配所有的数字和小写字母的字符。中括号内的某些元字符是不起作用的,当作普通字符匹配 |
[^] | 功能同上,含义相反,比如 [^0-9a-z] 表示匹配所有非数字、非小写字母的字符 |
\d | 匹配所有数字,等价于 [0-9] |
\D | 匹配所有非数字,等价于 [^0-9] |
\w | 匹配所有数字、字母和下划线 _ |
\W | 匹配所有非数字、非字母、非下划线 _ |
或代表 | 元字符,在 markdown 的表格中出现的 | 会被翻译。
根据上面的元字符已经可以做很多事了,现在来写一个简单的功能,提取一个字符串中所有的手机号码:
1 | var regex = /1[3458]\d\d\d\d\d\d\d\d\d/g; |
执行代码,控制台输出如下信息:
1 | [ '15921453264', '14735675687' ] |
这段代码较简单,第三组数字的第二位不是 3、4、5、8 中的一个,第四组数字只有 10 位,第五组数字的第一位不是 1,所以只有前两组数字被提取出来。
空白字符
这类元字符可以匹配字符串中的各种空白字符:
元字符 | 匹配规则 |
---|---|
\0 | 匹配 null 字符 |
\f | 匹配进制字符 |
\n | 匹配换行符 |
\r | 匹配回车字符 |
\t | 匹配水平制表符 |
\v | 匹配垂直制表符 |
\s | 匹配空白字符、空格、制表符和换行符 |
\S | 匹配非空白字符 |
这类元字符较简单,一般结合其他元字符进行使用。
锚字符
这类元字符用来控制要匹配字符串的位置:
元字符 | 匹配规则 |
---|---|
^ | 该元字符后面的表达式必须位于目标字符串的行首位置,这个元字符前面一般不能有表达式 |
$ | 该元字符前面的表达式必须位于目标字符串的行尾位置,这个元字符后面一般不能有表达式 |
\b | 匹配单词边界,在该元字符前面或后面的表达式必须是是在单词边界,也就是说表达式前面或后面必须有空格,这个元字符放在 [] 中失效,表示匹配一个退格符 |
\B | 匹配非单词边界,功能同上,含义相反 |
这类元字符还是较常用的,软件产品中经常会有这样的需求,输入框只能输入手机号码,那么到现在就可以写出这么一个功能:
1 | var regex = /^1[34578]\d\d\d\d\d\d\d\d\d$/; |
执行代码,返回 true,这个正则表达式就是把开始位置和结束位置都限定死了,可以用来检测一个字符串是否为手机号码。
重复字符
这类元字符用来控制要匹配字符串的重复次数:
元字符 | 匹配规则 |
---|---|
{n} | n 是一个非负整数,其前面的表达式必须连续出现 n 次 |
{n,} | n 是一个非负整数,其前面的表达式必须至少出现 n 次 |
{n,m} | n 和 m 都是一个非负整数,其前面的表达式必须至少出现 n 次,至多出现 m 次,注意 n 和 m 之间不能有空格 |
* | 匹配前面的表达式零次或多次,等价于 {0,} |
+ | 匹配前面的表达式一次或多次,等价于 {1,} |
? | 匹配前面的表达式零次或一次,等价于 {0,1} |
有了这类元字符,之前那个检测手机号码的功能可以简化一下代码:
1 | var regex = /^1[34578]\d{9}$/; |
执行代码,返回 true,这样,一些需要匹配的重复出现的规则相同的字符,写起来就简单多了。
分组字符
接下来到了个人认为正则表达式中最难理解的元字符了,这类元字符用来进行分组匹配。
(x)
这种分组叫做捕获性分组,匹配到的所有分组内容都返回。先来看一个列子:
1 | var regex = /ab(cd)ef/; |
执行上面代码,控制输出如下信息:
1 | [ 'abcdef', 'cd', index: 0, input: 'abcdefgh' ] |
通过上面的结果可以发现,加上括号后返回结果多了 ‘cd’,这便是这个元字符的作用,它的过程是先按照全部的正则表达式匹配,如果搜索到匹配结果再将括号内的内容从头匹配。如果没有搜到全部正则表达式的匹配,则不会匹配括号中的内容,比如上面的例子稍微改一下:
1 | var regex = /ab(cd)ef/; |
执行代码,返回结果为 null。
使用这个元字符还可以使用替代元字符,分为两种:\ 和 $。这两个元字符后面跟一个正整数,代表替代分组中的第几组匹配结果,例如:
1 | var regex = /ab(cd)ef\1/; |
执行代码,控制台输出如下信息:
1 | [ 'abcdefcd', 'cd', index: 0, input: 'abcdefcdgh' ] |
因为匹配的分组结果第一组为 ‘cd’,所以全部的正则表达式匹配的时候匹配内容就是 ‘abcdefcd’。
之前在介绍字符串对象函数的时候有一个 replace() 函数没有介绍,现在是时候登场了。这个函数是用来将一个字符串替换成指定字符串的,参数有两个,第一个参数可以是正则表达式,也可以是字符串,第二个参数是将要替换字符串的内容,先来一个简单的用法:
1 | var regex = /abcdef/; |
执行代码,返回结果是 ‘123gh’,可以看出 ‘abcdef’ 被替换成了 ‘123’,这个很好理解。现在加入捕获性分组元字符,代码如下:
1 | var regex = /ab(cd)ef/; |
执行代码,返回结果是 ‘123cdgh’,将要替换的字符串内容多了 ‘$1’,它代表了分组匹配中的第一组内容。
(?:x)
这种分组叫非捕获性分组,和捕获性分组的区别就是返回结果不包含分组匹配的内容。那这样就有疑问了,这样和不加这个元字符有什么区别?先来看一个例子:
1 | var regex = /zoo+/; |
执行上面的代码,控制台输出如下:
1 | [ 'zoo', index: 0, input: 'zoozoo' ] |
上面的正则表达式只能匹配 ‘zo’ 后面跟一个或多个 ‘o’,但很多实际情况想要把整个 ‘zoo’ 当成一个表达式来匹配,这时候如果使用捕获性分组又不想返回分组匹配的内容,使用非捕获性分组是最优选择,改动代码如下:
1 | var regex = /(?:zoo)+/; |
执行代码,控制台输出如下:
1 | [ 'zoozoo', index: 0, input: 'zoozoo' ] |
这样的结果就是我们预期所需要的。
其他分组字符
除了上面详细的介绍的两个分组字符,还有一些分组字符也较为常用:
元字符 | 匹配规则 |
---|---|
(?=x) | 正向肯定预查,是一个非捕获性分组,该元字符前面的表达式后面必须跟能够正确匹配 x 的表达式,比如 Window(?=2000) 可以正确匹配 ‘Window2000’,但无法匹配 ‘Window 98’ |
(?!x) | 正向否定预查,是一个非捕获性分组,和上面的元字符含义正好相反,该元字符前面的表达式后面一定不能跟能够正确匹配 x 的表达式 |
(?<=x) | 反向肯定预查,是一个非捕获性分组,和正向肯定预查的区别是作用于该元字符前面的表达式,JavaScript 不支持这个元字符 |
(?<!x) | 反向否定预查,是一个非捕获性分组,和正向否定预查的区别是作用于该元字符前面的表达式,JavaScript 不支持这个元字符 |
惰性字符
正则表达式中匹配模式默认为贪婪的,即尽可能匹配多的字符,比如下面的代码:
1 | var regex = /fo*/; |
执行上面代码,匹配结果为 ‘fooooo’,这个表示贪婪匹配,如果想要改为惰性匹配,在其后面加上元字符 ? 即可:
1 | var regex = /fo*?/; |
执行代码,匹配结果为 ‘f’,尽可能匹配少的字符,这就是惰性字符。
惰性字符只能跟在 *、+、?、{} 这几个元字符后面,如果跟在其他元字符后面表示匹配前面的表达式零次或一次,这个前面有介绍过。
总结
至此,关于 JavaScript 中有关正则表达式的用法就全部介绍完了,正则表达式在程序开发中是比不可少的,了解并深入学习也是很有必要的。