工欲善其事必先利其器,我们这里的器自然是对字符串处理的核心工具之一——正则
很多小朋友对正则很头疼,甚至谈正色变,业务需要的情况下只要去百度一番。通常百度的内容是可以满足也位于开发需要的,但是这种好知识不成为自己的,多可惜。其实很多文章都可以在网上找到,详尽的规则也都有,但是很多人看不懂。
既然如此那就顺带学习一下基本的RegExp用法吧
首先我要澄清正则的一个误区,正则做的是字符匹配,而非内容比较,正则会尽量按照你需要的内查询规则符合的内容,而不是判断字符串存在不存在或者比较大小
var reg = /a/;
reg.test('a') // true
正则的目的在于找到,它会竭尽可能的告诉你有而不是没有
首先,在js中正则由 / /的字面构成,乍一看和注释//很像,中间嵌套的就是正则实际内容。同样的可以用字符串构成,这样就需要用到正则对象 RegExp。
var reg = /a/;
var reg2 = new RegExp('a');
这是正则的一般规则
1.正则由普通字符组成,完成一个正常匹配操作
普通字符的概念就是任意字符的字符串,可以是 A也可以是你好还可以是 空格,凡是你需要的都可以。把他们按照你想要的顺序拼接起来,组成一句话,一个句式,一个词组,然后匹配🎈
/*
假如句式为: 今天天气真的好啊!
那分析这句话中是不是有天气可以这么写
*/
var reg = /天气/;
reg.test("今天天气真的好啊!")
// true
最基础的查询在于你想查什么,查询`今天`,查询`真的好`都是可以的。
当普通字符存在时,如果想匹配到内部一些其他空白符或者特殊符号,就需要搭配转义符号\来使用。这里列举一些常用的,
| 表达式 | 含义 |
|---|---|
\r、 \n 代表回车和换行符 | r是return的意思,n是new line的意思,是从古老的打字机衍生过来的,打字机按行打印,打印下一行需要完成两个动作,第一将指针从最后面移到最前面,第二让指针从新行开始,合并起来就是\r\n,dos下\r\n代表新行,unix下\r代表新行,linux下用\n |
\t | 制表符就是Tab的意思 |
\\ | 代表的是\本身,毕竟一道斜杠会和其他字符配合,那么匹配本身就需要自己转义自己了 |
\^ | ^有两种含义,第一种写在一开始,表示从第一个字符开始匹配,比如天气今天好,如果匹配今天可以匹配到如果写成 ^今天就匹配不到了,这句话一开始是天气。而它的另一层用于排除可选匹配,接下来再详述 |
\$ | $的作用和^的第一种含义相反,代表末尾,比如匹配天气可以,但是匹配天气$就不行了,这句话的末尾是天好 |
\. | 匹配的是小数点本身.,你想问为什么小数点也要转义?那接下来就告诉你 |
\b | 单词边界,一般针对英文而言,比如weekend,end,就可以匹配end而不是weekend |
2.偷懒,同一类型的字符是有区间的
我们在正则的使用过程中,如果需要查询一种存在在,这种存在包含了任意的某一种类型,那怎么办呢?不慌,正则也提供了偷懒的技巧。这样就不需要写很多了
| 含义 | 范围 |
|---|---|
| 字母 | A-Z,a-z |
| 数字 | 0-9 |
| 汉字 | 一般的范围匹配是 \u4e00-\u9fa5 |
那这个怎么用呢?需要搭配[] 自定义字符集 一起使用
var reg = /[a-zA-Z]/
// 可以匹配到
reg.test("a") //true
reg.test("Z") //true
reg.test("1") //false
// 而之前提到的`^`则是配合[]而使用的
var reg = /[^a-zA-Z]/
reg.test("a") //false
reg.test("Z") //false
reg.test("1") //true
3.继续偷懒,普通字符可以缩写
| 表达式 | 含义 |
|---|---|
. | 这个厉害,代表除\n之外的任意内容,比如我们想知道这句话是不是空字符串,就可以用 /./匹配了 |
\d | 是digital的缩写,0~9之中任意纯数字 |
\w | 就是word的缩写,代表的是符合word规范的 a~z 、A~Z、 0~9 以及 _ 的任意一个,至于为什么有_的存在,符合命名规范 |
\s | space缩写,代表的是空格、空白符、换行符等空白字符的任意一个 |
任意的缩写字符可以随意组合,但是一般不会以 \d\d这种方式重复组合,不是不可以这么用,只是可以有更简单的方式,将以上内容这么一大写,就会变成值的排除。比如\D任何不是数字的项。
4.偷懒进阶,使用量词,重复内容简单写
当我们想知道一串字符是不是由纯数字(不包含小数点)组成的,怎么办?只要某一种类型全部重复不就可以了吗💨,那我们又不可能完全知道数字的长度,那么就需要引入重复计量方式了
| 表达式 | 含义 |
|---|---|
? | 重复0或者1次,一般表示可能存在要么不存在 |
* | 重复0次或者多次,是个可能存在的值 |
+ | 重复至少一次,是个必定存在的值 |
{n} | 重复n次 |
{n,} | 只少重复n次 |
{n,m} | 重复n-m之间的次数 |
就用`Google`单词举例
/a?/.test('Google') //true
/a*/.test('Google') //true
/o+/.test('Google') //true
/e{2}/.test('Google') //false
/o{2,3}/.test('Google') //true
/o{3,}/.test('Google') //false
我们最常用的应该是手机号的匹配,那么可以这么写,先写`/^$/`开头结尾,然后往中间插值
1. `/^\d{11}$/`代表开头是11位结尾也是11位的0-9之间的纯数字
那么012345678911这样的是合法的 18800008888也是合法的。
如果我们需要一个明确的手机号,从1开始的,那么就需要把正则修改一下
2. `/^1\d{10}$/` 代表开头是1,结尾是10位数字的组合,
那么之前的012345678911就不合法了,但是12300000000也是合法的,所以更进一步的话就需要了解手机号的发行号段
3. `/^1[3-9]\d{9}$/` 这样就修改为1加上3-9的组合然后再加9位数字的组合,就屏蔽了 10 11 12开头的手机号段
正则在熟悉的时候是一个循序渐进的过程,让匹配的结果更满足我们实际的需要
5.正则也可以分组,和Emmet规则一样
分组就是通过()实现,将你认为的词组包裹到一起,形成一个整体。 abcabcabc可以用(abc)包裹起来,这样再加上多次匹配符 + 完成 /(abc)+/,就可以匹配这条语句了。
分组的第二作用其实是取值,当你需要从一句话中取得某个符合条件的串,就需要用组包裹,一般用于数字的较多
"今天温度26℃,最高温度30℃"
匹配到其中的26,并且取值。
/(\d+)℃/g.exec("今天温度26℃,最高温度30℃") 将会得到
["26℃", "26", index: 4, input: "今天温度26℃", groups: undefined]
如果要匹配30则需要这么改
/(\d+)℃$/g.exec("今天温度26℃,最高温度30℃") 将会得到
["30℃", "30", index: 4, input: "今天温度26℃", groups: undefined]
但是如果我想得到所有的温度,这该如何表示?还是重申一句,正则做的是匹配,匹配的内容尽量一次满足需要,
而不是多次,所以如果要获取两个的话,可以确保的确有两个,所以需要在句式上做改变
/(\d+)℃.+(\d{2})℃/g.exec("今天温度26℃,最高温度30℃") 将会得到
["26℃,最高温度30℃", "26", "30", index: 4, input: "今天温度26℃,最高温度30℃", groups: undefined]
分组之后,会给匿名分组取名,默认是从1-999开始的序列。
5.1 给分组起个名字,具名分组(?<name>xxx)
通常我们用的是匿名分组,但是为了在代码中方便的获取内容的属名,让代码更加语义化,就需要给分组冠名。使用的官名方式为 (?<name>xxx)
"今天温度26℃,最高温度30℃"
/(?<temp>\d+)℃.+(?<htemp>\d{2})℃/g.exec("今天温度26℃,最高温度30℃")
[
"26℃,最高温度30℃",
"26",
"30",
groups: {temp: "26", htemp: "30"},
index: 4,input: "今天温度26℃,最高温度30℃"
]
那么我们就可以直接方便的从groups里知道temp和htemp的代表谁。
5.2 反向匹配
当我们进行分组之后,可以进一步缩写内容,以减少代码量,代价就是可读性变得更差了。就和数学符号一样,越抽象需要理解的就越复杂。那他的作用在哪儿?
如何匹配一个句式 "我...我的...","我爱我的祖国","我和我的家人"
正常来说应该这样写
/我.+我的.+/.exec("我爱我的祖国")
["我爱我的祖国", index: 0, input: "我爱我的祖国", groups: undefined]
那我们能不能对 "我...我的..." 这种句式进一步缩减?毕竟是重复就可以抽象。
/(我).+\1的.+/.exec("我爱我的祖国")
["我爱我的祖国", "我", index: 0, input: "我爱我的祖国", groups: undefined]
换成具名引用的情况是
/(?<me>我).+\k<me>的.+/.exec("我和我的家园")
["我和我的家园", "我", index: 0, input: "我和我的家园", groups: {…}]
6.左右逢源,任意满足任其一
在js中,我们用||代表或运算,而正则中我们用|代表任选其一,用在近似项的匹配上,通常还会配合分组使用
"typescript、javascript、coffeescript"
查找是否包含typescript和javascript,一般我们直接用完整单词匹配即可,
但是对于相类似的script部分,其实可以将单词拆分以减少代码量。
拆分后的为
/(type|java)script/.test("typescript、javascript、coffeescript") //true
那有人想问了,是不是可以查询字符产里面只包含这两个但是不能包括coffee呢?
答案是做不到,就像一开始说的,`正则只能尽可能告诉你有,而不是尽可能告诉你没有`
那我们可以先查询coffeescript的存在然后证明条件不符合要求。
这样的逆向解决方案也是可以满足我们需要的。
7.贪婪与非贪婪
贪婪与非贪婪是在增加4.的规则后呈现出来的一种匹配形式。默认情况下是尽可能多的满足需要。非贪婪就是尽可能少,那用什么标记为非贪婪呢?,没错,就是?,在4.一个量词后面追加?就可以变成非贪婪也就是尽可能少。
正所谓贪心不足蛇吞象,那我们就来区分一下这两种的区别
默认情况下是贪婪匹配,尽可能多的满足要求,我们这里是从字符里查询包含a和d的句子
/a.*d/.exec("abcdabcdabcd")
["abcdabcdabcd", index: 0, input: "abcdabcdabcd", groups: undefined]
当在量词 * 之后追加 ?,就修改为非贪婪模式那结果就会如下
/a.*?d/.exec("abcdabcdabcd")
["abcd", index: 0, input: "abcdabcdabcd", groups: undefined]
看出来区别了吗,
满足a-d的句子,有两种模式,第一种是尽可能长,也就是`abcdabcd`,第二种尽可能短`abcd`,都是满足需要的。
你要问中间?不好意思,正则只会尽力做,而不会区分。
不管是贪婪还是非贪婪都是从字符第一次出现的位置开始计算的
"aaab".match(/a+?b/);
["aaab", index: 0, input: "aaab", groups: undefined]
一定不要认为匹配的是 `ab`,因为满足规则的a,位置从 0 就已经出现了。
??也是可行的,但是意义呢就因人而异了,反正我觉得意义不大
/a??/.exec('aabc') // ""
/a?/.exec('aabc') // "a"
8. 捕获和非捕获
捕获和非捕获一般是针对默认分组而言的一种方式,捕获的内容会单独成项,非捕获组不记录所匹配的内容,比普通分组更节约内存资源。
捕获和非捕获一般用?:写在分组头部,下面是分组的区别
/(a)+/.exec('aabc')
["aa", "a", index: 0, input: "aabc", groups: undefined]
/(?:a)+/.exec('aabc')
["aa", index: 0, input: "aabc", groups: undefined]
除了?:之外还有一些非捕获分组的匹配规则,他们叫预搜索,预搜索有向前和向后两种,再配合否定搜索!一共可以组成 2*2=4 种,通常配合可选项来完成:
?=肯定向后搜索?!否定向后搜索
字面上也好理解,= 等于,! 不等于,意思就是字面意思,拿`排名`举个例子
/第(?=一|二|三)/.exec("第一")
["第", index: 0, input: "第一", groups: undefined]
/第(?!一|二|三)/.exec("第四")
["第", index: 0, input: "第四", groups: undefined]
?<=肯定向前搜索?<!否定向前搜索
我们拿`级别`来说,这种就适合向前搜索
一级,二级,三级,特级
/(?<=一|二|三)级/.exec("一级")
["级", index: 1, input: "一级", groups: undefined]
/(?<!一|二|三)级/.exec("特级")
["级", index: 1, input: "特级", groups: undefined]
那又有人有问题了,按照你这么写,其实换成非捕获`?:` 也是可以的,这么说对,但是也不对。
对是在于如果要做测试匹配内容,是可以的,但是不对在于,他们两个含义是完全不同的
/(?:一|二|三)级/.exec("一级")
["一级", index: 0, input: "一级", groups: undefined]
一目了然。前面查询的是`级`,他的前置条件需要满足 一|二|三,而后面查询的就是 `一级`、`二级`、`三级`
9. 匹配模式
匹配模式在js种有三种,他们跟在正则的最后一个斜杠末尾 / /igm,可以同时使用也可以单独使用:
i(IgnoreCase) 忽略大小写g(Global) 全局m(Multiline) 多行
第一个就是字面意思了,就不做多的解释了。第二个有的说到,全局匹配是什么意思呢?那么我们需要聊一个正则属性,lastIndex,这个属性的作用是什么,那我们来举个例子:
先定义好一个正则规则
var reg = /abc/g;
然后用来匹配这么一个字符串,三次输出的结果
reg.exec("abcabc")
["abc", index: 0, input: "abcabc", groups: undefined]
reg.exec("abcabc")
["abc", index: 3, input: "abcabc", groups: undefined]
reg.exec("abcabc")
null
看到问题了没有,同一个正则匹配的竟然不一样的结果,这就是lastIndex的作用。
在全局匹配模式下支持一个字符串的多次查询,只要匹配到,就会更新lastIndex位置,下一次就会从lastIndex的位置开始查询这样的话一般可以用到循环里去对每一次匹配结果做处理。一般什么时候用?当然是文字替换的时候了。
var reg = /-/;
var gReg = /-/g;
"A-B-C-D".replace(reg,"/");
--> "A/B-C-D"
"A-B-C-D".replace(gReg,"/");
"A/B/C/D"
所以小伙伴如果做字符串测试匹配不要乱写g,以免发生不必要的问题,仅仅是想要做整个语句的匹配,只要用好^$就可以了。如果是做字符串的内容替换,一定不要省略g,不然大概率得不到你想要的
那对于多行匹配是个是那么意思呢?那先了解什么叫行。行是由一串字符串中存在\r\n换行字符串而决定的,每增加一个换行符就会将整个字符串分成两行。
单行模式下,换行符看作特殊占位符,但是.不能匹配到 \n,所以当一行中存在换行符时想要用到任意字符,就需要开启多行,或者用 [\w\W]的等价写法,替代。
多行影响的其实是 ^和$,这两个是以行来区分的,^为行首,$为行尾,
/^abc/.exec("ab\r\nabc")
null
/^abc/m.exec("ab\r\nabc")
["abc", index: 4, input: "ab↵abc", groups: undefined]
🔴🟡🟢🔴🟡🟢🔴🟡🟢
以上就是整个正则的主要内容,了解了之后,开发就更进一步了。