阅读 96

前端如何理解正则-由浅入深的学习

引言

  正则在平时工作中用得蛮多的,比如说验证文本搜索文本替换服务配置...。之前就常有同事直接发我规则,让我写个正则给他。自己也因为在编辑一个公众号的内容,需要将图片上的文本录入图文(文章)。于是就想着调用百度的图片识别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]匹配abc
   [a-z]匹配az的小写字母。
   [^a-z]匹配除az之间字符以外的任意单字符,包括空字符。
{} 包含一个(段)数量的量词,给匹配符添加数量,不能为负整数。
例如:/a{2}/,匹配连续的2a
   /a{2,}/,匹配连续的>=2a
   /a{0,5}/,匹配连续的>=0 && <=5a

当然元字符并不止这么一点,还有更多。 但是只要知道以上几种元字符,就能书写大部分正则规则了,以下用例子把上面描述的内容实际展示一下。


正则语法练习

获取字符串内[](含)内的数据(使用字符串方法 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~091[0-2] 匹配 10~12。 第三部分(0[1-9]|[1-2][0-9]|3[01]) 匹配日期,0[1-9] 匹配 0~9[1-2][0-9] 匹配 10~293[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~199200~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~199200~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] ); // => @
复制代码

可以看到,都能匹配到 @ 符号,不但能单个匹配也能区间匹配,比如匹配AD

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],另外还有很多。完整的ASCIIUnicode码表参考。

修饰符g m i

gglobal全局匹配,默认情况下是非全局匹配,匹配到一个就结束,全局匹配是匹配所有数据。还有两个修饰符分别是 i m iignoreCase 忽略大小写的意思很好理解 mmultiline 多行匹配,比如 /^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}$/;
复制代码

在这里插入图片描述
这里是三种类型字符取两种,这里的 零宽断言 判断的就是所有两两组合的情况,以此类推,四种类型取三种或者两种也是可以的,只是组合情况太多正则就比较长,就建议通过代码分开判断。

结语

正则使用的地方有很多,用熟悉了能极大的提高工作效率,比如多数编辑器的搜索替换都是支持正则的,如果要替换或者搜索某个规律的字段,用正则无疑是最方便快捷的方式了。

以上对正则的介绍就这些,看完这篇信息后,可以去百度百科上看更加详细的文档介绍,不过要注意,上面也有疏漏的地方,毕竟谁都可以改百度百科的内容。

以上内容如有疏漏,或错误的地方欢迎指正,谢谢。

关注下面的标签,发现更多相似文章
评论