# 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.
我想找到 cat 这个单词,但是如果只是使用cat这个正则,就会同时匹配到cat和scattered这两处文本。这时候我们就需要使用边界正则表达式\b,其中 b 是 boundary 的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。
上面的例子改写成\bcat\b这样就能匹配到 cat 这个单词了。
# 字符串边界
匹配完单词,我们再来看一下一整个字符串的边界怎么匹配。元字符^用来匹配字符串的开头。而元字符$用来匹配字符串的末尾。注意的是在长文本里,如果要排除换行符的干扰,我们要使用多行模式。试着匹配I am scq000这个句子:
I am scq000.
I am scq000.
I am scq000.
2
3
我们可以使用/^I am scq000\.$/m这样的正则表达式,其实m是multiple line的首字母。正则里面的模式除了m外比较常用的还有i和g。前者的意思是忽略大小写,后者的意思是找到所有符合的匹配。
| 边界和标志 | 正则表达式 | 记忆方式 |
|---|---|---|
| 单词边界 | \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
2
3
如果只是使用简单的(regex)匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。
# 回溯引用
所谓回溯引用(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像\1,\2,....,其中\1表示引用的第一个子表达式,\2表示引用的第二个子表达式,以此类推。而\0则表示整个表达式。
假设现在要在下面这个文本里匹配两个连续相同的单词,你要怎么做呢?
Hello what what is the first thing, and I am am scq000.
利用回溯引用,我们可以很容易地写出\b(\w+)\s\1这样的正则。
回溯引用在替换字符串中十分常用,语法上有些许区别,用$1,$2...来引用要被替换的字符串。下面以 js 代码作演示:
var str = 'abc abc 123';
str.replace(/(ab)c/g,'$1g');
// 得到结果 'abg abg 123'
2
3
如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)这样就可以避免浪费内存。
var str = 'scq000'.
str.replace(/(scq00)(?:0)/, '$1,$2')
// 返回scq00,$2
// 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2
2
3
4
有时,我们需要限制回溯引用的适用范围。那么通过前向查找和后向查找就可以达到这个目的。
# 前向查找
前向查找(lookahead)是用来限制后缀的。凡是以(?=regex)包含的子表达式在匹配过程中都会用来限制后面的表达式的匹配。例如happy happily这两个单词,我想获得happily的happ,那么就可以使用前向查找的正则happ(?=ily),来限制后缀ily进行匹配。如果我想过滤所有以happ开头的副词,那么也可以采用前向负查找的正则happ(?!ily),就会匹配到happy单词的happ前缀。
# 后向查找
介绍完前向查找,接着我们再来介绍一下它的反向操作:后向查找(lookbehind)。后向查找是用来限制前缀的,通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字符串。举个简单的例子: apple和people都包含ple这个后缀,那么如果我只想找到apple的ple,该怎么做呢?我们可以采用(?<=app)ple来限制app这个前缀,就能唯一确定ple这个单词了。
其中(?<=regex)的语法就是我们这里要介绍的后向查找。regex指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向后查找。另外一种限制匹配是利用(?<!regex)语法,这里称为负后向查找。与正前向查找不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找apple的ple也可以这么写成(?<!peo)ple。
需要注意的,不是每种正则实现都支持后向查找。在 JavaScript 中是不支持的,所以如果有用到后向查找的情况,有一个思路是将字符串进行翻转,然后再使用前向查找,作完处理后再翻转回来。看一个简单的例子:
// 比如我想替换apple的ple为ply
var str = 'apple people';
str.split('').reverse().join('').replace(/elp(?=pa)/, 'ylp').split('').reverse().join('');
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开始)