瞎说系列之正则表达式入门

1,002 阅读10分钟

前言

一直以来也没有写文章的习惯,前不久在公司内部进行技术分享之后,发现写技术文章还是非常重要的,因此将之前分享过的内容整理出来,写成此文章。之后会不定期更新瞎说系列教程,敬请期待。如有不妥,欢迎大家不吝赐教。

简介

正则表达式,又称规则表达式。(在代码中常简写为regex)。正则表达式通常被用来检索,替换那些符合某个模式的文本。

正则表达式是对字符串(包括普通字符(例如:a到z之间的字母)和特殊字符(又称为元字符))操作的一种逻辑公式。用事先定义好的一些特定字符,以及这些特定字符的组合,组成一个“规则字符串”,这个规则字符串用来表达对字符串的一种过滤逻辑。正则表达式是一种文本模式,模式描述在搜索文本时要匹配的一个或多个字符串。

语法

下面详细的介绍正则表达式的语法。

字符类

元字符是拥有特殊意义的字符。

常用元字符
字符 等价 含义
. [^\n\r] 匹配任何单个字符,除了换行符和回车符。
\w [a-zA-Z_0-9] 匹配任何单词字符(数字,字母,下划线)
\W [^a-zA-Z_0-9] 匹配任何非单词字符
\d [0-9] 匹配数字。
\D [^0-9] 匹配非数字。
\s [\n\f\r\t\v\x0B] 匹配空白字符。(包括空格符,制表符,回车符,换行符,垂直换行符,换页符)。
\n \n 匹配换行符。
\f \f 匹配换页符。
\r \r 匹配回车符。
\t \t 匹配制表符。
\v \v 匹配垂直制表符。
\ \ 转义字符,转义后面字符所代表的含义(比如\*匹配的是*)
| | 表示字符匹配是或的关系(比如x|y匹配的是x或y中的一个字符)。
\0 Null 匹配null

换行符和回车符的区别可以参考阮一峰的文章

量词

量词用来表示匹配的数量。

字符 等价 含义
n? n{0,1} 匹配任何包含0个或1个的字符串(最多有一个)。
n+ n{1,} 匹配任何包含至少1个的字符串(至少一个)。
n* n{0,} 匹配任何0个或多个的字符串。
n{x} n{x} 匹配包含x个n的序列的字符串。
n{x,} n{x,} 匹配包含至少x个n的序列的字符串。
n{x,y} n{x,y} 匹配出现x次但是不超过y次的n的序列的字符串。
定位符

定位符顾名思义用来确定位置的字符。

字符 含义
^ 单独使用表示匹配表达式的开始。
$ 匹配表达式的结尾。
\b 匹配一个单词字符的边界。单词字符后面或前面不与另外的单词字符相邻,可以理解为匹配一个单词的开始或者结束。(比如:/\bx/可以匹配s x)。
\B 匹配非单词的边界。可以理解为查找不处在单词的开始或者结束的位置。
标志字符
字符 含义
g 匹配全局。
i 不区分大小写。
m 多行搜索。
范围类
字符 含义
[0-9] 匹配0到9之间的任意一个数字
[a-z] 匹配a到z之间的任意一个字符。
[A-Z] 匹配A到Z之间的任意一个字符。
[^0-9] 匹配不在0到9之间的任意一个字符。
其他规则
字符 含义
(pattern) 匹配pattern并获取这一匹配,即捕获组。
(?:pattern) 匹配pattern但不获取匹配结果,即非捕获组。
(?=pattern) 正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非捕获组匹配。例如/windows(?=95)/可以匹配windows95中的windows,但是不能匹配windows98中的windows。可以理解为匹配(?=pattern)外面且在前面的内容。
(?!pattern) 正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这也是一个非捕获组匹配。例如/windows(?!95)/可以匹配windows98中的windows,但是不能匹配windows95中的windows
(?<=pattern) 反向肯定预查,与正向肯定预查相似,只是方向相反。正向肯定预查是匹配前面的内容,而反向肯定预查是匹配后面的内容。例如(?<=95)windows能匹配95windows中的windows,但是不能匹配98windows的windows,也不能匹配windows95中的windows。
(?<!pattern) 反向否定预查,与正向否定预查相似,只是方向相反。例如(?<!95)windows能匹配98windows中的windows,但是不能匹配95windows中的windows,也不能匹配windows95中的windows。

运算符优先级

正则表达式是从左到右进行计算,并遵循优先级顺序。相同的优先级从左到右计算,不同优先级从高到低计算。下表为表达式运算符从高到低的优先级顺序。

字符 含义
\ 转义字符。
(), (?:), (?=), [] 圆括号和方括号。
*, +, ?, {n}, {n,}, {n,m} 量词等限定符。
^, $, \任何元字符、任何字符 定位点和序列(即位置和顺序)。
| ”或“操作。

贪婪模式和非贪婪模式

正则表达式的贪婪模式和非贪婪模式是相对于量词这个限定符来说的。默认情况下,正则的所有量词(限定符)都是贪婪模式,即尽可能多的去匹配字符。而在量词(限定符)后面加上?就变成了非贪婪模式,即尽可能少的去匹配字符。

举个🌰

正则表达式/a{2,5}/,可以匹配aa,aaa,aaaa,aaaaa。这是贪婪模式,即尽可能多的去匹配。

正则表达式/a{2,5}?/,只会匹配aa。这是非贪婪模式,即尽可能少的去匹配。

疑问?

如果正则表达式是<div>.+?</div>cc的话,匹配结果却是<div>test1</div><div>test2</div>cc。很多人在日常开发中会对这种情况感到困惑。这里明明是非贪婪模式,为什么匹配结果不是<div>test1</div>cc呢?这是因为无论是贪婪模式还是非贪婪模式都有一个前提条件:整个表达式必须匹配成功。当表达式匹配到<div>test1</div>时,后面的cc无法匹配成功,只有匹配到<div>test1</div><div>test2</div>时,后面的cc才会匹配成功。

分组

正则的分组主要通过小括号来实现,括号的子表达式作为一个分组,括号后面可以紧跟量词表示重复的次数。

捕获组

捕获性分组,通常由一对小括号加上子表达式组成。正则会把每个分组里面的内容保存起来,供后续调用。其中由分组捕获的串会从1开始编号,依次类推。这种引用既可以在表达式内部,也可以在表达式外部。

举个🌰
const regex = /(\d{4})-(\d{2})-(\d{2})/
const str = '2019-10-21'
console.log(RegExp.$1) // 2019
console.log(RegExp.$2) // 10
console.log(RegExp.$3) // 21

捕获组引用常用来进行替换操作。

const regex = /(\d{4})-(\d{2})-(\d{2})/
const str = '2019-10-21'
const result = str.replace(regex, '$3/$2/$1')
// => 21/10/2019
嵌套分组的捕获

在嵌套的分组中是以左括号出现的顺序进行捕获。

举个🌰
const regex = /((I) (am) (your) (father))/
const str = 'I am your father'
RegExp.$1 // I am your father
RegExp.$2 // I
RegExp.$3 // am
RegExp.$4 // your
RegExp.$5 // father
反向引用

捕获组捕获到的内容,不仅可以在正则表达式外部通过程序进行引用,也可以在正则表达式内部进行引用,在内部被反向引用的值继续参与匹配,而在表达式内部进行引用的方式被称为反向引用。其格式为\数字。反向引用通常是用来查找或限定重复,限定指定标识配对出现。

反向引用匹配原理

捕获组在匹配成功时,会将子表达式匹配到的内容,保存在一个以数字编号的组里,这时可以通过反向引用的方式,引用这个局部变量的值。一个捕获组在匹配成功之前,它的内容是不确定的,一旦匹配成功,它的内容就确定了,反向引用的内容也就是确定的了。

举个🌰
const regex = /(\w{3}) is \1/
regex.test('skr is skr') // true
regex.test('krs is krs') // true
regex.test('krs is skr') // false

在表达式匹配成功之后,\1引用了第一个被分组捕获的内容即skr或者krs,所以前两个会匹配成功。

但是,如果编号越界了,则会被当成普通的表达式:

const regex = /(\w{3}) is \\6/
regex.test('skr is skr') // false
regex.test('skr is \6') // true
非捕获组

有时我们只想要括号的原始功能,只进行分组,而不进行捕获,即既不在表达式外部引用,也不在表达式内部反向引用。此时我们可以使用非捕获分组。语法为(?:p)。

举个🌰
const regex = /(?:\d{4})-(\d{2})-(\d{2})/
const date = '2019-10-21'
RegExp.$1 // 10
RegExp.$2 // 21

在这个例子中使用了非捕获分组,因此(?:\d{4})不会捕获任何字符串,所以$1为(\d{2})捕获的内容。

RegExp对象

RegExp构造函数会创建一个正则表达式对象,用于将文本与一个模式匹配。

有两种方法来创建一个RegExp对象:一个是字面量,另一个是RegExp构造函数。要指示字符串,字面量的参数不使用引号,而构造函数的参数使用引号。

当表达式被赋值时,字面量形式提供了正则表达式的编译状态。当你在循环中使用字面量构造一个正则表达式时,表达式不会在每一次迭代中被重新编译。而通过构造函数创建的正则表达式提供了运行时编译。如果你知道表达式模式将会改变,或者你事先不知道什么模式,而是从另一个来源获取的,比如用户的输入,那么这些情况都可以使用构造函数模式。

当使用构造函数创建表达式对象时,需要常规的字符转义(即在前面加上反斜杠\)。下面两种情况是等价的:

const regexp = new RegExp('\\w+')
const regexp = /\w+/
RegExp对象属性
属性 含义
global RegExp对象是否具有标志g。
ignoreCase RegExp对象是否具有标志i。
lastIndex 一个整数,表示下一次匹配的开始位置。
multline RegExp对象是否具有标志
source 正则表达式的源文本。
RegExp对象方法
exec()

RegExp.prototype.exec():该方法用于检索字符串中正则表达式的匹配。

返回值是一个数组,其中存放匹配的结果。如果未找到匹配,将返回null。此数组的第0个元素是与正则表达式相匹配的文本。第1个元素是与RegExpObject的第1个子表达式匹配的文本,第2个元素是与RegExpObject的第2个子表达式匹配的文本,以此类推。除了数组元素和length属性外,该方法还返回两个属性。index属性表示的是匹配的文本第一个字符的位置。input属性表示的是被检索的字符串。在调用非全局的RegExp对象的exec()方法时,返回的数组与调用String.match()返回的数组是相同的。

但是当RegExpObject时一个全局正则表达式时,它会在RegExpObject的lastIndex属性指定的字符处开始检索字符串。当exec()方法找到了相匹配的文本后,它将把RegExpObject的lastIndex属性设置为匹配文本的最后一个字符串的下一个位置,因此可以反复调用exec()来遍历字符串中所有匹配的文本,当exec()再也找不到匹配的文本时,它将返回null,并且lastIndex属性也会重置为0。

注意:如果在一个字符串中完成了一次匹配之后,想要检索新的字符串,必须手动把lastIndex属性设置为0。

举个🌰
var str = "2019.10.21"
var regexp = /\b(\d+)\b/g
console.log( regexp.exec(str) )
console.log( regexp.lastIndex)
console.log( regexp.exec(str) )
console.log( regexp.lastIndex)
console.log( regexp.exec(str) )
console.log( regexp.lastIndex)
console.log( regexp.exec(str) )
console.log( regexp.lastIndex)
// => ["2019", "2019", index: 0, input: "2019.10.21"]
// => 4
// => ["10", "10", index: 5, input: "2019.10.21"]
// => 7
// => ["21", "21", index: 8, input: "2019.10.21"]
// => 10
// => null
// => 0
test()

RegExp.prototype.test()方法执行一个检索,用来查看正则表达式与指定的字符串是否匹配。如果正则表达式与指定的字符串匹配,则返回True,否则返回false。

举个🌰
const regexp = /\d+/
const str = 'abc123'
regexp.test(str) // true

其他相关API

search()

search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。返回第一个与regexp相匹配的子串的起始位置。

注意:search()放不执行全局匹配,它将忽略标志g。

举个🌰
const regexp = /\d/
const str = 'abc123'
str.search(regexp) // 3
match()

match()放可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。返回值是一个数组,存放匹配结果,该数组的内容依赖于正则表达式是否有全局标志g。

如果regexp没有标志g,那么match()就在string中只执行一次匹配。如果没有匹配就返回null。如果有匹配,它将返回一个数组,该数组的第0个元素存放的是匹配的文本,其余元素存放的是与正则表达式的子表达式(即捕获组)相匹配的文本。此外还包含两个对象属性,index属性声明的是匹配文本的起始字符在字符串中的位置,input属性声明的是对该字符串的引用。

如果regexp具有标志g,match()将执行全局检索,找到字符串中所有匹配的子字符串。如果没有找到就返回null。如果找到,就返回一个数组。数组中存放的元素是字符串中所有匹配到的子串,没有index和input属性。

举个🌰
const regexp = /(\d{4})-(\d{2})-(\d{2})/
const str = '2019-10-21'
str.match(regexp) // ['2019-10-21','2019','10','21',index: 0,input: '2019-10-21']
const regexp = /(\d{4})-(\d{2})-(\d{2})/g
const str = '2019-10-21'
str.match(regexp) // ['2019-10-21']
replace()

replace()方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。返回值是一个被替换后的字符串。如果regexp具有标志g,则会替换所有相匹配的文本,如果没有标志g,则只替换第一个匹配到的文本。替换的内容可以是字符串,也可以是函数。如果是字符串,那么替换文本中的$具有特殊的意义。

字符 替换文本
$1,$2……,$99 与 regexp 中的第 1 到第 99 个子表达式相匹配的文本。
$& 与 regexp 相匹配的子串。
$` 位于匹配子串左侧的文本。
$' 位于匹配子串右侧的文本。
? 直接量符号。
举个🌰
const regexp = /\w/g
const str = 'acdfe'
str.replace(regexp, 'b') // bbbbb
const regexp = /\w/
const str = 'acdfe'
str.replace(regexp, 'b') // bcdfe
const regexp = /\w/g
const str = 'acdfe'
str.replace(regexp, function(){
    return 'b'
}) // bbbbb

总结

很感谢大家花时间把我的文章看完。正则表达式是一门”玄学“,功能非常强大,并且里面还有很多不为人知的”骚操作“,希望大家能够继续探索它的奥秘,多多动手实际操作一下,毕竟纸上得来终觉浅,绝知此事要躬行。最后再给大家推荐一个正则表达式可视化的网站regexper