# JavaScript 基础之正则表达式

正则表达式(Regular Expression)其实就是一门工具,目的是为了字符串模式匹配,从而实现搜索和替换功能。从它的命名我们可以知道,它是一种用来描述规则的表达式,使用单个字符串来描述匹配一系列符合某个句法规则的字符串。

# 从字符出发

正则表达式的基本组成元素可以分为:字符和元字符。字符很好理解,就是基础的计算机字符编码,通常正则表达式里面使用的就是数字、英文字母。而元字符,也被称为特殊字符,是一些用来表示特殊语义的字符。如 ^表示非,|表示或。利用这些元字符,才能构造出强大的表达式模式(pattern)。接下来,我们就来从这些基本单位出发,来学习一下如何构建正则表达式。

# 单个字符

最简单的正则表达式可以由简单的数字和字母组成,没有特殊的语义,纯粹就是一一对应的关系。如想在 'apple' 这个单词里找到 ‘a' 这个字符,就直接用a这个正则就可以了。

但是如果想要匹配特殊字符的话,就得请出我们第一个元字符**\**, 它是转义字符,顾名思义,就是让其后续的字符失去其本来的含义。举个例子,如果想匹配*这个符号,由于*这个符号本身是个特殊字符,所以就要利用转义元字符\来让它失去其本来的含义,即\*

如果本来这个字符不是特殊字符,使用转义符号就会让它拥有特殊的含义。我们常常需要匹配一些特殊字符,比如空格、制表符、回车、换行等, 而这些就需要我们使用转义字符来匹配。为了便于记忆,我整理了下面这个表格,并附上记忆方式:

特殊字符 正则表达式 记忆方式
换行符 \n new line
换页符 \f form feed
回车符 \r return
空白符 \s space
制表符 \t tab
垂直制表符 \v vertical tab
回退符 [\b] backspace,之所以使用[]符号是避免和 \b重复

# 多个字符

单个字符的映射关系是一对一的,即正则表达式被用来筛选匹配的字符只有一个。而这显然是不够的,只要引入集合区间和通配符的方式就可以实现一对多的匹配了。

在正则表达式里,集合的定义方式是使用中括号[]。如[123]这个正则就能同时匹配1、2、3三个字符。那如果想匹配所有的数字怎么办呢?[0123456789] 这种写法当然可以,但是显然太过低效,所以元字符-就可以用来表示区间范围。

匹配区间 正则表达式
匹配单个小写字母 [a-z]
匹配单个大写字母 [A-Z]
匹配单个字母 [A-Za-z]
匹配单个数字 [0-9]
匹配单个字母或数字 [A-Za-z0-9]
匹配单个汉字 [\u4e00-\u9fa5]

即便有了字符集的定义方式,如果要同时匹配多个字符也还是要一一列举,仍然是低效的。所以在正则表达式里衍生了一批预定义字符集,用来简化最常用的字符集定义的写法:

匹配区间 正则表达式 记忆方式
除了换行符之外的任何字符 . 英文句号
单个数字, [0-9] \d digit
除了[0-9] \D not digit
包括下划线在内的单个字符,[A-Za-z0-9_] \w word
非单字字符 \W not word
匹配空白字符,包括空格、制表符、换行符、换页符、回车符[\t\n\f\r\p{Z}] \s space
匹配非空白字符 \S not space

1、在[]中,特殊字符不需要转义,可以直接使用,比如[.()],但是在外面,是需要转义的\(\.等。

2、在[]中,-连接符放在第一个字符时,则表示连字符本身,如果放在中间时,比如[a-z],就表示是从字母 a 到字符 z。

3、在[]中,^有着不同的含义。 [ab] 表示 a 或者 b,[^ab] 表示非 a 或 b,相当于取反。

# 循环与重复

一对一和一对多的字符匹配都讲完了,接下来,就该介绍如何同时匹配多个字符。要实现多个字符的匹配我们只要多次循环,重复使用我们的之前的正则规则就可以了。那么根据循环次数的多与少,我们可以分为 0 次,1 次,多次,特定次。

匹配规则 正则表达式
0次或1次,可有可无,最多一次 ?
0次或无数次,可有可无,多了不限 *
1次或无数次,至少一次,多了不限 +
特定次数,不多不少,恰好n次 {n}
特定次数,至少min次,至多max次 {min, max}
特定次数,至少min次,多了不限 {min, }

# 位置边界

在长文本字符串查找过程中,我们常常需要限制查询的位置。比如我只想在单词的开头结尾查找。

# 单词边界

单词是构成句子和文章的基本单位,一个常见的使用场景是把文章或句子中的特定单词找出来。如:

The cat scattered his food all over the room.
1

我想找到 cat 这个单词,但是如果只是使用cat这个正则,就会同时匹配到catscattered这两处文本。这时候我们就需要使用边界正则表达式\b,其中 b 是 boundary 的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。

上面的例子改写成\bcat\b这样就能匹配到 cat 这个单词了。

# 字符串边界

匹配完单词,我们再来看一下一整个字符串的边界怎么匹配。元字符^用来匹配字符串的开头。而元字符$用来匹配字符串的末尾。注意的是在长文本里,如果要排除换行符的干扰,我们要使用多行模式。试着匹配I am scq000这个句子:

I am scq000.
I am scq000.
I am scq000.
1
2
3

我们可以使用/^I am scq000\.$/m这样的正则表达式,其实m是multiple line的首字母。正则里面的模式除了m外比较常用的还有ig。前者的意思是忽略大小写,后者的意思是找到所有符合的匹配。

边界和标志 正则表达式 记忆方式
单词边界 \b boundary
非单词边界 \B not boundary
字符串开头 ^ 头尖尖那么大个
字符串结尾 $ 终结者,美国科幻电影,美元符$
多行模式 m 标志 multiple of lines
忽略大小写 i 标志 ignore case, case-insensitive
全局模式 g 标志 global

# 子表达式

字符匹配我们介绍的差不多了,更加高级的用法就得用到子表达式了。通过嵌套递归和自身引用可以让正则发挥更强大的功能。

从简单到复杂的正则表达式演变通常要采用分组、回溯引用和逻辑处理的思想。利用这三种规则,可以推演出无限复杂的正则表达式。

# 分组

其中分组体现在:所有以元字符()所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。

对于电话号码212-555-1234来说,\d{3}-\d{3}-\d{4}这种匹配的方式,是将整个电话号码作为一个组(group)匹配起来。 我们把212-555-1234这样的叫Group0

这个时候,如果我们加了一个括号\d{3}-(\d{3})-\d{4},那么匹配到的555就叫Group1。 以此类推,如果有两个小括号\d{3}-(\d{3})-(\d{4})那么分组就是下面的情况:

212-555-1234   Group0
555            Group1
1234           Group2
1
2
3

如果只是使用简单的(regex)匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。

# 回溯引用

所谓回溯引用(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像\1\2,....,其中\1表示引用的第一个子表达式,\2表示引用的第二个子表达式,以此类推。而\0则表示整个表达式。

假设现在要在下面这个文本里匹配两个连续相同的单词,你要怎么做呢?

Hello what what is the first thing, and I am am scq000.
1

利用回溯引用,我们可以很容易地写出\b(\w+)\s\1这样的正则。

回溯引用在替换字符串中十分常用,语法上有些许区别,用$1$2...来引用要被替换的字符串。下面以 js 代码作演示:

var str = 'abc abc 123';
str.replace(/(ab)c/g,'$1g');
// 得到结果 'abg abg 123'
1
2
3

如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)这样就可以避免浪费内存。

var str = 'scq000'.
str.replace(/(scq00)(?:0)/, '$1,$2')
// 返回scq00,$2
// 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2
1
2
3
4

有时,我们需要限制回溯引用的适用范围。那么通过前向查找和后向查找就可以达到这个目的。

# 前向查找

前向查找(lookahead)是用来限制后缀的。凡是以(?=regex)包含的子表达式在匹配过程中都会用来限制后面的表达式的匹配。例如happy happily这两个单词,我想获得happilyhapp,那么就可以使用前向查找的正则happ(?=ily),来限制后缀ily进行匹配。如果我想过滤所有以happ开头的副词,那么也可以采用前向负查找的正则happ(?!ily),就会匹配到happy单词的happ前缀。

# 后向查找

介绍完前向查找,接着我们再来介绍一下它的反向操作:后向查找(lookbehind)。后向查找是用来限制前缀的,通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字符串。举个简单的例子: applepeople都包含ple这个后缀,那么如果我只想找到appleple,该怎么做呢?我们可以采用(?<=app)ple来限制app这个前缀,就能唯一确定ple这个单词了。

其中(?<=regex)的语法就是我们这里要介绍的后向查找regex指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向后查找。另外一种限制匹配是利用(?<!regex)语法,这里称为负后向查找。与正前向查找不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找appleple也可以这么写成(?<!peo)ple

需要注意的,不是每种正则实现都支持后向查找。在 JavaScript 中是不支持的,所以如果有用到后向查找的情况,有一个思路是将字符串进行翻转,然后再使用前向查找,作完处理后再翻转回来。看一个简单的例子:

// 比如我想替换apple的ple为ply
var str = 'apple people';
str.split('').reverse().join('').replace(/elp(?=pa)/, 'ylp').split('').reverse().join('');
1
2
3

从 ES2018 之后,Chrome 中的正则表达式也支持反向查找了。不过,在实际项目中还需要注意对旧浏览器的支持,以防线上出现 Bug。详情请查看http://kangax.github.io/compat-table/es2016plus/#test-RegExp_Lookbehind_Assertions

最后总结一下这部分内容:

回溯查找 正则表达式 记忆方式
引用 \0,\1,\2 和 $0, $1, $2 转义+数字
非捕获组 (?:regex) 引用表达式(()),本身不被消费(?),引用(😃
前向查找 (?=regex) 引用子表达式(()),本身不被消费(?), 正向的查找(=)
前向负查找 (?!regex) 引用子表达式(()),本身不被消费(?), 负向的查找(!)
后向查找 (?<=regex) 引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),正的查找(=)
后向负查找 (?<!regex) 引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),负的查找(!)

# 逻辑处理

在正则里面,默认的正则规则都是的关系,所以这里不讨论。

关系,分为两种情况:一种是字符匹配,另一种是子表达式匹配。在字符匹配的时候,需要使用^这个元字符。在这里要着重记忆一下:只有在[]内部使用的^才表示非的关系。子表达式匹配的非关系就要用到前面介绍的前向负查找子表达式(?!regex)或后向负查找子表达式(?<!regex)

或关系,通常给子表达式进行归类使用。比如,我同时匹配 a , b 两种情况就可以使用(a|b)这样的子表达式。

逻辑关系 正则元字符
|
[^regex] 和 !

# 常用正则手册

# 校验数字的表达式

数字:^[0-9]*$

n位的数字:^\d{n}$

至少n位的数字:^\d{n,}$

m-n位的数字:^\d{m,n}$

零和非零开头的数字:^(0|[1-9][0-9]*)$

非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$

带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?$

正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$

有两位小数的正实数:^[0-9]+(.[0-9]{2})?$

有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$

非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$

非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$

非负整数:^\d+$^[1-9]\d*|0$

非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$

非负浮点数:^\d+(\.\d+)?$^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$

非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$

正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$

负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$

# 校验字符的表达式

汉字:^[\u4e00-\u9fa5]{0,}$

英文和数字:^[A-Za-z0-9]+$^[A-Za-z0-9]{4,40}$

长度为3-20的所有字符:^.{3,20}$

由26个英文字母组成的字符串:^[A-Za-z]+$

由26个大写英文字母组成的字符串:^[A-Z]+$

由26个小写英文字母组成的字符串:^[a-z]+$

由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$

由数字、26个英文字母或者下划线组成的字符串:^\w+$^\w{3,20}$

中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$

中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$

可以输入含有^%&',;=?$"等字符:[^%&',;=?$\x22]+

禁止输入含有~的字符:[^~\x22]+

# 特殊需求表达式

Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$

域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?

InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$

手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$

电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$

国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}

身份证号(15位、18位数字):^\d{15}|\d{18}$

短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$

帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$

密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$

强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$

日期格式:^\d{4}-\d{1,2}-\d{1,2}

一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$

一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$

钱的输入格式:

有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^[1-9][0-9]*$ 这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|[1-9][0-9])$ 一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9])$ 这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧。下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$ 必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$ 这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$ 这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$ 1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$ 备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里

xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$

中文字符的正则表达式:[\u4e00-\u9fa5]

双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))

空白行的正则表达式:\n\s*\r (可以用来删除空白行)

HTML标记的正则表达式:<(\S*?)[^>]*>.*?|<.*? /> ( 首尾空白字符的正则表达式:^\s*|\s*$(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换 页符等等),非常有用的表达式)

腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)

# 参考目录

正则表达式不要背

面试官!让我们聊聊正则

可能是最好的正则表达式的教程笔记了吧...

上次更新: 2020年6月18日星期四 14:37