Skip to content

正则表达式

3160字约11分钟

javascript

2018-11-26

本文正则表达式基于javascript,不同的计算机语言对正则表达式的支持情况以及实现,语法不尽相同,不一定适用于其他语言。

简介

正则表达式,是一种文本模式(Regular Expression),是对字符串的一种匹配查找规则。 可以方便的在某一文本字符串中,查找、定位、替换符合某种规则的字符串。

比如说,我们想要找出一段文本中的手机号码,文本内容如下:

name:Mark  tel:13800138000
name:Jhon  tel:13800138888

很明显,在这段文本中,手机号码是以 tel:开头,这符合一定的规则,这样我们可以通过正则表达式来书写这个规则, 然后去查找匹配:

var text = `name:Mark  tel:13800138000
name:Jhon  tel:13800138888`

var result = text.match(/tel:(1\d{10})/)
// ["tel:13800138000", "13800138000", index: 0, input: "tel:13800138000", groups: undefined]
var tel = result[1]
// 13800138000

/tel:(1\d{10})/ 便是所说的正则表达式。

RegExp 与字面量

javascript中,我们可以使用构造函数RegExp 创建正则表达式。

new RegExp(pattern[, flags])

var regExp = new RegExp('\\d', 'g')

也可以通过 字面量的方式:

var regExp = /\d/g

两种创建正则表达式适用的场景有些细微的不同,一般使用new RegExp()来创建动态的正则表达式,使用字面量创建静态的正则表达式。

正则表达式字面量是提供了对正则表达式的编译,当正则表达式保持不变时,用字面量的方式创建正则表达式可以获得更好的性能。

以下讨论以正则表达式字面量来创建正则表达式:

正则表达式一般由元字符和普通字符组成。

元字符

元字符也叫特殊字符,是正则表达式规定的,对符合特定的单一的规则的字符的描述。

字符含义
\在非特殊字符的前面加反斜杠,表示这个字符是特殊的,不能从字面上解释。比如在\d描述的不是一个普通的字符d,而是正则表达式中的数值0-9
如果在特殊字符前面加反斜杠,这表示将这个字符转义为普通字符,比如?在正则中有其特殊含义,前面加反斜杠?,这可以将其转为普通的?
^匹配文本开始的位置,如果开启了多行标志,也会匹配换行符后紧跟的位置。
比如^a会匹配abc,但不会匹配到bac
$匹配文本结束的位置,如果开启了多行标志,也会匹配换行符前紧跟的位置。
比如b$会匹配acb,但不会匹配到abc
*匹配前一个表达式0次到多次。
比如,ab*会匹配到abbbbbbc中的abbbbbb,以及acbbbbb中的a
+匹配前一个表达式1次到多次。
比如,ab+会匹配到abbbbbbc中的abbbbbb,但不会匹配acbbbbb
?匹配前一个表达式0次到1次。
比如,ab*会匹配到abbbbbbc中的ab,以及acbbbbb中的a
.匹配除换行符之外的任何单个字符。
x|y匹配 x或者y。
[xyz]表示一个字符的集合。匹配集合中的任意一个字符。可以使用破折号-来指定一个字符范围。
比如,[0-4][01234],都可以匹配4567中的4
[^xyz]表示一个方向字符集合。匹配任意一个不包括在集合中的字符。可以使用破折号-来指定一个字符范围。
比如,[0-4][01234],都可以匹配2345中的5
{n}n为一个整数,表示匹配前一个匹配项n次。
比如a{2}不会匹配abc中的a,但会匹配aaaabc中的aa
{m,n}m,n都是一个整数,匹配前一个匹配项至少发生了m次,最多发生了n次。
当m,n值为0时,这个值被忽略,当n值不写,如{1,}表示1次到多次。当m值不写时,如{,1}表示0次到1次。
(x)匹配x并且捕获该匹配项。称为捕获括号,括号中的匹配项也称作子表达式。
(?:x)匹配x但不捕获该匹配项。称为非捕获括号。
x(?=y)匹配x且当x后面跟着y。称为正向肯定查找(正向前瞻)。
x(?!y)匹配x且当x后面不跟着y。称为正向否定查找(负向前瞻)。
[\b]匹配一个退格(U+0008)。
\b匹配一个词的边界。匹配的值的边界并不包含在匹配的内容中。
\B匹配一个非单词的边界。
\d匹配一个数字。等价于[0-9]
\D匹配一个非数字。等价于[^0-9]
\n匹配一个换行符 (U+000A)。
\r匹配一个回车符 (U+000D)。
\s匹配一个空白字符,包括空格、制表符、换页符和换行符。
等价于[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]
\S匹配一个非空白字符。
\t匹配一个水平制表符 (U+0009)。
\w匹配一个单字字符(字母、数字或者下划线)。
等价于[A-Za-z0-9_]
\W匹配一个非单字字符。
\xhh与代码 hh 匹配字符(两个十六进制数字)
\uhhhh与代码 hhhh 匹配字符(四个十六进制数字)。

上表在多数文章都会提及,但有一些注意的细节,下面我单独拎出来说说。

  1. [xyz] 匹配集合中的任意一个字符
    这个字符集的元素,可以是普通字符,也可以是特殊字符,也可以用破折号-规定一个字符集范围。
    以匹配数字为例,可以写成[0123456789] ,也可以写成[\d],也可以写成[0-9]
    类似于()等特殊字符,在[]中有其作用,都特殊字符的作用一致,不能直接当做普通字符来使用,所以我们需要使用反斜杠\将其转义为普通字符,值得注意的是,上表的特殊字符中,星号*、小数点.[]中并没有特殊用途,所以不需要做转义处理,当然,即使做了转义,也不会出现问题;而破折号-[]中有其特殊作用,所以作为普通字符使用时,需要转义。

  2. ?:匹配前一个表达式0次到1次。
    其实这里准确描述来说,匹配前一个表达式,且该表达式 非任何量词 *+?{} ,匹配前一个表达式0次到1次。
    如果紧跟在 非任何量词 *+?{} 的后面,将会使量词变为非贪婪的(匹配尽量少的字符)
    贪婪与非贪婪匹配,我们在下文细说。

等价字符

正则表达式中,有不少特殊字符的写法,是等价的,也可以说是简写形式,下表的左右两边,都是等价的。

regExpregExp
*{0,}
+{1,}
?{0,1}
\d[0-9]
\D[^0-9]
\w[a-zA-Z*]
\W[^a-zA-Z*]
\s[\f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]

贪婪模式与非贪婪

什么是贪婪模式?

贪婪是指正则表达式匹配时,是贪心的,会尽可能的匹配多的字符,主要体现在量词特殊字符

// 匹配一个到多个数字
var r = /\d+/
var t1 = '12a'
var t2 = '1234a'
var t3 = 'a12b345'
console.log(t1.match(r)[0]) // 12
console.log(t2.match(r)[0]) // 1234
console.log(t3.match(r)[0]) // 12

非贪婪,即是让正则表达式匹配尽量少的字符。那么如何改变正则表达式的贪婪模式?

在量词特殊字符后面紧跟使用?

我们说说的量词包括*, +, ?, {m,n}。那么紧跟了?,会有什么不同的表现呢?

我们从例子来分析:

var r1 = /<div>.*<\/div>/
var r2 = /<div>.*?<\/div>/
var str = '<div>aaa</div>bbb<div></div>ccc'

变量r1是贪婪匹配,得到的结果会是什么呢?

console.log(str.match(r1)[0])
// <div>aaa</div>bbb<div></div>

在这段字符串中,有两个</div>的匹配字符串,正则表达式在遇到第一个</div>匹配字符项时,同时满足了/.*//<\/div>/的匹配条件,优先作为/.*/的匹配值,在遇到第二个时,同样还是优先作为/.*/的匹配值,直到匹配的字符串str的结束,没有满足条件的匹配字符串,再把第二个</div>作为/<\/div>/的匹配值。最终得到了<div>aaa</div>bbb<div></div>的匹配结果。

变量r2这是非贪婪匹配,得到的结果又会有所不同:

console.log(str.match(r1)[0])
// <div>aaa</div>

同样,两个</div>的匹配字符串,但实际非贪婪匹配模式,在匹配到第一个</div>,就不会再继续向下匹配字符串了。

也就是说,贪婪匹配是,在满足规则下,尽可能多的匹配更多的字符串,直到字符串结束或没有满足规则的字符串了;非贪婪匹配是,在满足规则下,尽可能少的匹配最少的字符串,一旦得到满足规则的字符串,就不再向下匹配。

  1. x*?:尽可能少的匹配x,匹配的结果可以是0个x
  2. x+?:尽可能少的匹配x,但匹配的结果至少有1个x
  3. x??:尽可能少的匹配x, 匹配的结果可以是0个x,但最多可以有一个x
  4. x{m,n}?:尽可能少的匹配x,但匹配的结果至少有m个x,最多可以有n个x

可能从字面来说,不好理解 x??, x{m,n}? ,来看一个例子就可以明白了:

var s1 = '<div>aa</div>'
var s2 = '<div>a</div>'
var s3 = '<div></div>'
var r1 = /<div>a??<\/div>/

console.log(r1.test(s1)) // false
console.log(r1.test(s2)) // true
console.log(r1.test(s3)) // true
var s1 = '<div>aaa</div>'
var s2 = '<div>aa</div>'
var s3 = '<div>aaaa</div>'
var r1 = /<div>a{2,3}?<\/div>/
var r2 = /<div>a{2,3}?/

console.log(r1.test(s1)) // true
console.log(r1.test(s2)) // true
console.log(r1.test(s3)) // false

console.log(s1.match(r2)[0]) // <div>aa
console.log(s2.match(r2)[0]) // <div>aa
console.log(s3.match(r2)[0]) // <div>aa

正则表达式标志

标志描述
g全局搜索
i不区分大小写搜索
m多行搜索
y执行“粘性”搜索,匹配从目标字符串的当前位置开始
uUnicode模式。用来正确处理大于 \uFFFF 的Unicode字符

m

使用m标志时,会改变开始(^)和结束字符($)的工作模式,变为在多行上匹配,分别匹配每一行的开始和结束,即\n\r 分割。

y

使用y标志时,匹配是从RegExp.lastIndex指定的位置开始匹配,匹配为真时,会修改 lastIndex的值到当前匹配字符串后的位置,下次匹配从这个位置开始匹配,如果匹配为假时,不会修改lastIndex的值。

let reg = /o/y
let str = 'foo'

// lastIndex 为 0,从字符 f 开始匹配
reg.test(str) // false
// 由于结果为 false, lastIndex 还是为 0
reg.test(str) // false

let str2 = 'oof'
// lastIndex 为 0 ,从字符 o 开始匹配
reg.test(str2) // true
//  lastIndex 此时修改为 1, 从第二个 o 开始匹配
reg.test(str2) // true
// lastIndex 此时修改为 2
reg.test(str2) // false   此时开始匹配的字符是 f
// lastIndex没有被修改
reg.test(str2) // false

正则表达式中的捕获—— \1,\2,\3... 以及 $1,$2,$3

在上文中我们介绍了 (x) 是匹配 x 并捕获,那么有了捕获就必然可以去使用捕获到的结果, \1,\2,\3... 以及$1,$2,$3... 便是指捕获的结果。

\1, \2, \3, \4, \5, \6, \7, \8, \9 在正则表达式中使用,捕获结果为正则表达式的源模式.

在这个正则表达式中(bc)被捕获并标记为\1, (ef)被捕获并标记为\2

let reg = /a(bc)d(ef)/

也可以使用来简化正则表达式

let reg = /a(bc)dbc/
let reg2 = /a(bc)d\1/

let str = 'abcdbc'

reg.test(str) // true
reg2.test(str) // true

$1, $2, $3, $4, $5, $6, $7, $8, $9RegExp的包含括号子表达式的正则表达式静态的只读属性。

let reg = /a(bc)d/
let str = 'abcd'

reg.test(str)
console.log(RegExp.$1) // bc

String.replace() 中使用:

let reg = /(\w+)\s(\w+)/
let str = 'apple pear'
str.replace(reg, '$2 $1') // pear apple
RegExp.$1 // apple
RegExp.$2 // pear