前端如何理解正则-由浅入深的学习
引言
正则在平时工作中用得蛮多的,比如说验证、文本搜索、文本替换、服务配置…。之前就常有同事直接发我规则,让我写个正则给他。自己也因为在编辑一个公众号的内容,需要将图片上的文本录入图文(文章)。于是就想着调用百度的图片识别API,将返回的数据格式化 (通过正则判断需要获取的值) 后,再插入网页版公众号编辑器,所以对于正则用得更多了。
所以我觉得正则这东西,只需要掌握其中几个核心的元字符,然后简单练习一下,再找几个稍复杂的案例详细解释,这样就能掌握书写正则的规律,最后再学习缩写后的常用符号就能完全理解了。
正则简单语法
除了普通字符,还有一些元字符
则具有特殊的含义,比如下面的这些:
元字符 | 描述 |
---|---|
\ | 正则的转义符,有三种情况: 1. \ 加上元字符,表示匹配元字符所使用的普通字符,比如要匹配普通字符 \ ,就要写\\ 。 2. \ 加上非元字符,组成一种由具体实现方式规定其意义的元字符序列 如\d 表示匹配一个数字字符。 3. \ 加上任意其他字符,默认情况就是匹配此字符,也就是说,反斜线被忽略了。 |
^ | 匹配文本行首。如果设置了RegExp对象的Multiline 属性, ^ 也匹配\n 或\r 之后的位置。用到[] 元字符中第一位时是取反的意思。例如: /^abc/ 匹配 abc 开头的字符串。/^abc/m 匹配多行 abc 开头的字符串。 |
$ | 匹配文本行尾。如果设置了RegExp对象的Multiline 属性, ^ 也匹配\n 或\r 之后的位置。例如: /abc$/ 匹配 abc 结尾的字符串。/abc$/m 匹配多行 abc 结尾的字符串。 |
| | 逻辑 或 的意思。例如:/a|b/ 匹配 a 或者 b 。 |
() | 将( 和 ) 之间的表达式定义为“组”(group),并且将匹配这个表达式的字符保存到一个临时区域(一个正则表达式中最多可以保存9个),它们可以用\1 到 \9 的符号来引用。例如: /([a-z])\1/ ,假如第一个括号内的[a-z] 匹配到字母 d ,那么\1 就相当于d 。以此类推, \2 就是第二个括号内匹配到的内容。(后面深入部分举例讲) |
[] | 带有 或 关系的一组数据,并可定义区间。 例如: [abc] 匹配a 或b 或c 。[a-z] 匹配a 到z 的小写字母。[^a-z] 匹配除a 到z 之间字符以外的任意单字符,包括空字符。 |
{} | 包含一个(段)数量的量词,给匹配符添加数量,不能为负整数。 例如: /a{2}/ ,匹配连续的2 个a 。/a{2,}/ ,匹配连续的>=2 个a 。/a{0,5}/ ,匹配连续的>=0 && <=5 个a 。 |
当然元字符并不止这么一点,还有更多。
但是只要知道以上几种元字符,就能书写大部分正则规则了,以下用例子把上面描述的内容实际展示一下。
正则语法练习
获取字符串内[]
(含)内的数据(使用字符串方法 match
)
var str = '今天学习了[RegExp]对象';
var reg = /\[[a-zA-Z]{0,}\]/;
console.log( str.match(reg) );
// => ["[RegExp]"]
这里就用到了[a-zA-Z]
,里面规则是匹配大小写字母,而紧跟着的{0,}
,是匹配0个或多个大小写字母。
前后的\[
\]
,就是用到了\
元字符的第一种情况。
-
在[a-z]
、[0-9]
等等之间属于连字符,表示之间的意思
[a-z-]
中z
后面的-
表示匹配普通字符-
,实在不清楚就用\
转义
[a-z]
匹配所有小写字母
[a\-z]
匹配a
,-
,z
。
判断字符串是否存在英文以外的字符(使用正则方法 test
)
var str = 'StackOverflow';
var str2 = '我在TrendyTech上班';
var reg = /[^a-zA-Z]$/;
console.log( reg.test(str) ); // => false
console.log( reg.test(str2) ); // => true
这里在[]
内用到了^
,意思就是取反中括号内的匹配项,整体的意思就是匹配除大小写字母以外的任意字符。
判断字符串是否为xxxx-xx-xx格式的日期(使用正则方法 test
)
var str = '2020-01-12';
var str2 = '2020年1月1日';
var reg = /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[01])$/;
console.log( reg.test(str) ); // => true
console.log( reg.test(str2) ); // => false
这一段正则看起来长,其实拆分一下很简单,总共分为三部分
第一部分[0-9]{4}
匹配年份,年份为四个数字组成
第二部分(0[1-9]|1[0-2])
匹配月份,0[1-9]
匹配 01~09,1[0-2]
匹配 10~12。
第三部分(0[1-9]|[1-2][0-9]|3[01])
匹配日期,0[1-9]
匹配 0~9,[1-2][0-9]
匹配 10~29,3[01]
匹配 30,31
虽然此正则不是很严谨,比如小月和平月没有31天,不过能说明规则就好。
从字符串中获取日期(使用字符串方法 match
)
var str = '今天是2020-01-12,马上就放假了';
var reg = /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[01])/;
console.log( str.match(reg)[0] ); // => 2020-01-12
这次的正则对比上面的只移除了^
$
,使用match
方法,获取到了字符串中的 xxxx-xx-xx
格式的时间字符串。
常用正则分析
好了,以上几个例子已经能够把正则基础的信息完整讲明了,那我们再解析几个常用的正则,最终你会发现,其实看起来很复杂的正则也是一个一个短的逻辑段拼凑而成。
IP 正则的验证与或获取
大多数情况,验证与获取的区别在于是否添加了行首^
、行尾$
验证。
var ipReg = /^(((2(5[0-5]|[0-4][0-9]))|[0-1]{0,1}[0-9]{1,2})\.){3}((2(5[0-5]|[0-4][0-9]))|[0-1]{0,1}[0-9]{1,2})$/;
IP是由 xxx.xxx.xxx.xxx
格式组成,xxx
的值为 0~255
,所以我们第一步写个0~255
的正则。
0~255
的正则 就是 (2(5[0-5]|[0-4][0-9]))|[0-1]{0,1}[0-9]{1,2}
太长了我们在拆分一下,分为 0~199
,200~255
0~199
的正则是 [0-1]{0,1}[0-9]{1,2}
解释:百位是0-1,匹配0-1次就是可以没有百位。个位十位取值0-9,匹配1-2次就是0-99之间的数。
200~255
的正则是 2(5[0-5]|[0-4][0-9])
解释:百位固定为2,十位这里分为5和0-4,5的情况下个位为0-5,0-4的情况下,个位是0-9。
因为0~199
和 200~255
拼接起来就要用()
+或|
连接起来,就成了上面0~255
的正则。
0~255
由.
拼接,就成了 0~255
.0~255
.0~255
.0~255
。
这里由于.
是匹配除换行和回车符以外的任意单字符的元字符,所以加斜线\.
转义为普通字符.
。
因为上面 有重复规律就是 0~255
. 出现三次所以用()
括起来,再用量词{3}
乘以三。
0~255
.0~255
.0~255
. 的正则就是 ((2(5[0-5]|[0-4][0-9]))|[0-1]{0,1}[0-9]{1,2})\.){3}
加上最后的0~255
就是完整匹配IP的正则了。
用一张图表明:
正则很长,其实可以稍微减短一点,之前说过,元字符\
加非元字符,会有一些常用匹配的集合,比如:
[0-9]
可以用 \d
替换,{0,1}
可以用?
替换。
简写一下上面的规则就是
var ipReg = /^(((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})\.){3}((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})$/;
像这样的常用匹配集合有很多,在未熟练掌握正则之前先不要使用,可以把写完的正则再一一对应替换。
去掉首尾的^
$
,用来匹配字符串中的IP。
var ipReg = /(((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})\.){3}((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})/;
var str = '这个项目部署在192.168.101.255上面。'
console.log( str.match(ipReg)[0] ); // => 192.168.101.255
邮箱 正则的验证与获取
var emailReg = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,}@([a-zA-Z0-9_-]{1,}\.){1,}[a-zA-Z0-9_-]{1,}$/;
普通邮箱格式:邮箱名称由 字母、数字、.
、_
、-
组成,首字母为字母或数字
域名部分由 字母、数字、_
、-
组成,.
连接
邮箱名称正则 [a-zA-Z0-9][a-zA-Z0-9_.-]{1,}
解释:字母、数字开头,后面跟着字母、数字、_、.、-,重复1次或多次
中间加 @
连接
邮箱域名正则 ([a-zA-Z0-9_-]{1,}\.){1,}[a-zA-Z0-9_-]{1,}
拆分为 [a-zA-Z0-9_-]{1,}
和 .
,然后组合成xxx.xxx.xxx
格式的邮箱域名正则。
解释:字母、数字、-、_,重复1次或多次
用一张图表示:
同样邮箱域名也可以缩写,元字符 +
和 {1,}
等价,\w
类似 [a-zA-Z0-9_]
(这里是类似,不是等价)。
缩写后的正则就是
var emailReg = /^[a-zA-Z0-9][\w.-]+@([\w-]+\.){1,}[\w-]+$/;
还有很多邮箱的规则这里并不完全匹配,如果要匹配比较特殊的邮箱,比如有中文,可以根据以上所学到的自行添加。
去掉首尾的 ^
$
,用来匹配字符串中的邮箱。
var emailReg = /[a-zA-Z0-9][\w.-]+@([\w-]+\.){1,}[\w-]+/;
var str = '我的google邮箱是zhouyu0229@gmail.com,你的邮箱呢?。'
console.log( str.match(emailReg)[0] ); // => zhouyu0229@gmail.com
正则深入学习
匹配ASCII码与Unicode码表数据
比如说匹配 @
,我们不单可以用普通字符@
,也可以使用 ASCII码的八进制、十六进制匹配和Unicode码匹配,看下面例子。
var str = '我是@符号';
var OCTReg = /\100/; // 八进制ASCII码
var sexadecimalReg = /\x40/; // 十六进制ASCII码
var unicodeReg = /\u0040/; // Unicode码
console.log( str.match(OCTReg)[0] ); // => @
console.log( str.match(sexadecimalReg)[0] ); // => @
console.log( str.match(unicodeReg)[0] ); // => @
可以看到,都能匹配到 @
符号,不但能单个匹配也能区间匹配,比如匹配A
到 D
var str = '我是在AB幼儿园上学,小明在CD幼儿园上学,小刚在EG幼儿园上学';
var OCTReg = /[\101-\104]/g; // 八进制ASCII码
var sexadecimalReg = /[\x41-\x44]/g; // 十六进制ASCII码
var unicodeReg = /[\u0041-\u0044]/g; // Unicode码
console.log( str.match(OCTReg) ); // => ["A", "B", "C", "D"]
console.log( str.match(sexadecimalReg) ); // => ["A", "B", "C", "D"]
console.log( str.match(unicodeReg) ); // => ["A", "B", "C", "D"]
可以看到把字符串内的 A
B
C
D
,都取出来了,最后的 g
,是修饰符。(下面解释)
所以可以用过区间方式匹配两个码表的所有字符,比如用unicdoe匹配中文字符,中文字符的编码范围是4E00-9FA5
,正则就写成[\u4E00-\u9FA5]
,另外还有很多。完整的ASCII和Unicode码表参考。
修饰符g
m
i
g
是 global
全局匹配,默认情况下是非全局匹配,匹配到一个就结束,全局匹配是匹配所有数据。还有两个修饰符分别是 i
m
i
是 ignoreCase
忽略大小写的意思很好理解
m
是 multiline
多行匹配,比如 /^a/
只匹配第一行开头是否为 a
,而加了m
,就是每一行开头都匹配。比如下面:
var ignoreCaseStr = '[a-z]和[A-Z]是不同的';
var multilineStr = `a同学大了b同学,b同学不满a同学打了他,就还手打了
a同学`;
var ignoreCaseReg = /[A-Z]/gi;
var multilineReg = /^a/gm;
console.log( multilineStr.replace(multilineReg,'A') );
// => A同学大了b同学,b同学不满a同学打了他,就还手打了
// => A同学
console.log( ignoreCaseStr.replace(ignoreCaseReg, 'x') );
// => [x-x]和[x-x]是不同的
可以看到行首的 a
替换成了 A
,而中间的没有,第二个正则内只写了大写的字母匹配,加了 i
修饰符,小写的也被匹配到用replace
方法替换成了x
。
()
组的用法
元字符语法中说道,将(
和 )
之间的表达式定义为“组”(group),并且将匹配这个表达式的字符保存到一个临时区域(一个正则表达式中最多可以保存9个),它们可以用 \1
到 \9
的符号来引用。特殊用法除外。比如:
var groupReg = /([A-Z])([A-Z])\2/g;
var groupStr = '我们公司有很多ABB格式名字的同事,ABC、AB格式的不多,我们吃饭一般都是AA制';
console.log( groupStr.match(groupReg) ); // => ["ABB"]
上面输出了包含 ABB
的数组,正则的意思就是第一个()
内的匹配到A
,如果后面要引用 就用\1
。但我们这里的例子用的 \2
,就是用的第二个()
内匹配到的数据,也就是B
,所以\2
内临时存的就是 B
,因此这里只能匹配第二、第三个字母相同的数据。
用上面写过的获取日期格式的正则来描述一下()
组的用法。
var str = '今天是2020-01-12,天气很好。';
var reg = /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[01])/;
console.log( str.replace(reg, `$1年$2月$3日`) ); // => 今天是2020年01月12日,天气很好。
这里可以看到2020-01-12
替换成了 2020年01月12日
,因为上面有三个()
组,分别\1
存了年、\2
存月、\3
存 日,然后使用replace
。这里在替换中的用法就是$
+ number,和在正则中使用\
+ number,是一样的,都是一一对应的,并且也最多支持临时存9个。
零宽断言 正则的预查
断言用来声明一个应该为真的事实。正则表达式中只有当断言为 真 时才会继续进行匹配。
零宽断言分为以下四种:
(?=pattern)
零宽度 正预测先行 断言(也叫正向肯定预查)
举例:查找15岁的小伙伴
var str = '小明17岁,小刚15岁,小红16岁,小茗15岁';
var reg = /[\u4E00-\u9FA5]{2}(?=15岁)/g;
console.log( str.match(reg) ); // => ["小刚", "小茗"]
以上正则匹配 15岁 之前的两个中文字(不包含断言内的数据),所以输出了 小刚,小茗
(?<=pattern)
零宽度 正回顾后发 断言(也叫反向肯定预查)
举例:查找小茗多少岁
var str = '小明17岁,小刚15岁,小红16岁,小茗15岁';
var reg = /(?<=小茗)\d{2}/g;
console.log( str.match(reg) ); // => ["15"]
以上正则查找小茗后面的 两个数字(不包含断言内的数据),所以输出了 15
(?<=pattern)
和 (?=pattern)
同时使用就可以查某某区间的值,比如:
var str = '<div>我是div里的内容</div><div>我是第二个div的内容</div>';
var reg = /(?<=<(div)>).*(?=<\/\1>)/;
var reg2 = /(?<=<(div)>).*?(?=<\/\1>)/;
console.log( str.match(reg)[0] ); // => 我是div里的内容</div><div>我是第二个div的内容
console.log( str.match(reg2)[0] ); // => 我是div里的内容
这里就输出了 div
标签里的内容,但是我们看到了两种情况。
第一种输出最前端和最后端div
之间的数据,第二种是只输出了前面div
内的数据。
这也涉及到贪婪模式(*
,+
,?
,{n}
,{n,}
,{n,m}
默认是贪婪模式),这些限制符后加上?
,就是非贪婪模式,就像上方的例子一样,中间的 .
元字符 一个是尽可能的多匹配,一个是尽可能的少匹配。
我们在这里也使用了前面学到的,第一个()
(零宽断言的括号不存数据)把取到的 div
暂存,在后面用 \1
取了出来,相当于<\/div>
,/
需要转义所以使用了 \/
。
(?!pattern)
零宽度 负预测先行 断言(也叫正向否定预查)
举例:查找不是 15岁 的小伙伴
var str = '小明17岁,小刚15岁,小红16岁,小茗15岁';
var reg = /[\u4E00-\u9FA5]{2}(?!15岁)/g;
console.log( str.match(reg) ); // => ["小明", "小红"]
这个正则就查找了不是 15岁 结尾的前两个字(不包含断言内的数据),输出了不是15岁的 小明、小红
(?<!pattern)
零宽度 负回顾后发 断言(也叫反向否定预查)
举例:查找 小红以外 的小伙伴年龄
var str = '小明17岁,小刚15岁,小红16岁,小茗15岁';
var reg = /(?<!小红)\d{2}/g;
console.log( str.match(reg) ); // => ["17", "15", "15"]
这个正则就查找了 小红以外 后面跟着两个数字的数据(不包含断言内的数据),输出了 小红以外 其他小伙伴年龄。
零宽断言之密码复杂度
零宽断言不但能匹配数据,同样也能判断数据,比如设置判断密码复杂度的正则:
规则:密码必须包含 字母、数字、_,6~32位。
var reg = /(?=.*[a-zA-Z])(?=.*\d)(?=.*_)^\w{6,32}$/;
这个正则前面的零宽断言 (?=.*[a-zA-Z])(?=.*\d)(?=.*_)
判断字符串是否出现 字母、数字、_ ,有的话正则就继续往下执行,直到执行消耗匹配 ^\w{6,32}$
,判断字符必须是字母、数字、_ 开头和结尾。
至于为什么前面要写 .*
,用两个连续的零宽断言测试就能知道了。
(?=[a-zA-Z])
能判断字符串内是否出现字母
(?=[a-zA-Z])(?=\d)
无法判断字母或数字是否出现,因为判断存在冲突。
从这张gif动图不难发现,当要匹配的数据是字母开头跟着数字时,断言数字的正则前方必须写已断言匹配到的a
,反过来毅然。
因为我们不能控制用户先输入字母、数字、_ 中的哪一个,所以我们在 零宽断言 上加 .*
匹配0个或多个.
,是为了不管哪个类型的字符先输入,或者间隔多少字符再输入其它类型字符时,判断其它类型的零宽断言也能继续判断下去(再讲一遍 .
是匹配除换行和回车符以外任意单字符的)。
简单讲,不管单个零宽断言还是多个零宽断言,都是断言的字符串位置,多个断言组合起来判断字符串中是否出现这个组合规则的位置,出现就返回true,不出现就返回fasle。
以上内容不易理解就多读几遍,或者根据gif内容自行测试几遍。
再写一个密码验证
规则:密码必须包含 字母、数字、_中的至少两种6~32位。
var reg = /(?=.*[a-zA-Z\d])(?=.*[\d_])(?=.*[a-zA-Z_])^\w{6,32}$/;
这里是三种类型字符取两种,这里的 零宽断言 判断的就是所有两两组合的情况,以此类推,四种类型取三种或者两种也是可以的,只是组合情况太多正则就比较长,就建议通过代码分开判断。
结语
正则使用的地方有很多,用熟悉了能极大的提高工作效率,比如多数编辑器的搜索替换都是支持正则的,如果要替换或者搜索某个规律的字段,用正则无疑是最方便快捷的方式了。
以上对正则的介绍就这些,看完这篇信息后,可以去百度百科上看更加详细的文档介绍,不过要注意,上面也有疏漏的地方,毕竟谁都可以改百度百科的内容。
以上内容如有疏漏,或错误的地方欢迎指正,谢谢。