正则表达式(Regular Expression,简称regex)作为一种工具,它擅长的领域是文本处理。最基本的用途是查找(search/match)和替换(replace)特定的文本内容,在此之上可以延伸到:
-
验证(validation)内容是否符合指定的规则
-
提取(Extract)指定的内容
1 JavaScript中正则表达式
JavaScript中为正则表达式提供了专门的RegExp类型,可以使用new RegExp()
实例化一个正则表达式,不过通常我们使用如下的语法创建一个正则表达式。
let expression = /pattern/flags;
/pattern/
表示正则表达式的匹配模式,flags表示该表达式支持的标志。
1.1 正则表达式的标志
一个正则表达式可以带一个或多个flag,flag有如下取值:
- g 表示全局模式(global),默认情况下,js的正则表达式在找到第一个匹配项时,就会停止匹配,加上g标志后,表达式会查找目标字符串中所有的匹配项。
- i 忽略大小写
- m 多行模式
flag可以组合使用,比如/exp/gi
表示exp表达式用于全局匹配,且忽略大小写。
1.2 正则表达式的用法
我们先不看正则表达式是怎么来的,先看它是怎么用的。使用RegExp主要是其实例方法以及外部支持RegExp的方法。后续的章节中,我们以patt表示表达式,text表示目标字符串。
- RegExp.exec()方法,exec是RegExp的实例方法,用法是
patt.exec(text)
,在text中查找匹配patt的内容,并以数组的方式返回第一个匹配的且未返回的结果,当无匹配时返回null。注意前面这句话中的"第一个"和"未返回的"我打了重点符号,
let text="112233";
let patt=/(\d)\1/g;
let matchs = patt.exec(text);
//["11", "1", index: 0, input: "112233", groups: undefined]
matchs = patt.exec(text);
//["22", "2", index: 2, input: "112233", groups: undefined]
matchs = patt.exec(text);
//["33", "3", index: 4, input: "112233", groups: undefined]
matchs = patt.exec(text);
//null
本列中/(\d)\1/g
表达式用来查找连续相同的数字,因为还没有讲表达式,所以这里不需要知道表达式的细节。patt.exec(text)
查找text中匹配patt表达式的内容,第一次返回了一个数组并赋值给matchs,这个数组有index和input属性,index表示当前匹配的内容在text中的起点位置,input表示目标字符串(及text文本内容)。matchs[0]表示当前匹配的内容,这里匹配的内容是11,如果表达式产生了分组(还没讲分组),会在matchs中添加分组内容,这里是1就是分组内容。
再次执行exec方法,返回第二个匹配的内容数组,直到返回为null,表示所有的匹配内容都已提取完。
如果表达式没有设置全局模式g,那么每次执行patt.exec(text)时,都会从头开始匹配,及每一次执行exec()方法就会返回第一次的内容。
-
RegExp.test()方法用于测试patt与text是否匹配,它只会返回目标字符串是否与表达式匹配,不会返回文本内容,这个方法一般用于验证用户的输入。
let patt = /^d{3}-d{8}$/; let phoneNumber = "010-88888888"; let flag = patt.test(phoneNumber); //true
上例中,patt表达式用于验证电话号码的格式,而使用test方法,只返回目标字符串是否为匹配,而不返回具体的内容。
-
string.match()方法是string的实例方法,与RegExp的方法类似,返回匹配的字符集合,注意,这里不包含组信息。
let text="11112223455"; let patt=/(\d)\1/g; text.match(patt); // ["11", "11", "22", "55"]
-
string.search()方法,返回第一个匹配的字符串的位置,如果没有匹配的内容,返回-1。
let text="12345"; let patt=/(\d)\1/g; text.search(patt); //-1 没有任何匹配的内容 text = "1233445"; text.search(patt); //2 返回字符串的第一个匹配内容的位置,这里是33,而后面44的位置不会返回
-
string.replace()方法是string的实例方法,用于替换指定的字符串,返回一个替换完成的字符串,相对之前的方法要复杂些,它接收两个参数:
- 第一个参数时要匹配的内容,是一个RegExp对象或字符串。
- 第二个参数要替换的内容,可以是字符串或函数。
当第一个参数时字符串时,只能替换第一个匹配的内容,如果要全局替换,需要使用全局模式的正则表达式。
let text = "cat,bat,sat,fat"; let tmp = text.replace("at","ond"); //text "cat,bat,sat,fat" //tmp "cond,bat,sat,fat"
第一个参数使用字符串时,只能替换第一个匹配的内容,后面的bat并没有被替换。
let text = "cat,bat,sat,fat"; let tmp = text.replace(/at/g,"ond"); // tmp "cond,bond,sond,fond"
第一个参数使用带全局标志(g)的正则表达式,会替换全部的匹配的内容。替换内容还可以使用特殊的字符(如:$n,这个叫回溯引用),引用前面匹配的结果。
let text = "01088888888,01077777777,01066666666"; let patt = /(\d{3})(\d{8})/g let tmp = text.replace(patt,"$1-$2"); //tmp "010-88888888,010-77777777,010-66666666"
上面使用特殊字符引
$n
引用了匹配结果的分组信息,并将匹配的结果格式化为自定的样式。$1
引用了第一个分组信息(\d{3})
,$2
引用了第二个分组信息(\d{8})
。replace方法的第二个参数是Function时,replace会向该函数传递三个参数,分别是,匹配的内容,匹配项的位置,原生字符串。使用function可以做更精细的替换,这里不深入讲解。
-
string.split()方法,该方法用指定的分隔符将指定的字符串分隔为多个子字符串。这个分隔符可以是字符串也可以是RegExp对象。使用RegExp作为分隔符时,该方法在不同浏览器存在差异。
2 编写正则表达式
正则表达式的符号较多,可分为普通字符,转义字符,特殊符号,定位符,限定符与组共6大类。
2.1 普通字符
普通字符代表字符本身,如:
let text = "cat,bat";
let patt = /at/g;
let tmp = text.match(patt);
//tmp ["at", "at"]
目标字符串text,会找到两处与patt相关的内容。
2.2 转义字符
有些普通字符转义后代表特殊的含义,比如:w在转义后\w
表示任意的0-9,a-z,A-Z以及_下划线。
转义字符 | 说明 |
---|---|
\w | 匹配任一0-9,a-z,A-Z,_字符 |
\W | 匹配任一非0-9,a-z,A-Z,_的字符 |
\d | 匹配任一0-9的数字 |
\D | 匹配任一非0-9的数字 |
\s | 匹配任一空格,tab和换行符\n |
\S | 匹配任一非空格,tab和换行符\n的字符 |
转义字符有很多,这里我只列出了常用的3个w、d、s,他们的大写是对应的含义取反。
let text = "要出差3天,还是1周";
let patt = /\d天/g;
let tmp = patt.exec(text);
// ["3天", index: 3, input: "要出差3天,还是1周", groups: undefined]
上面的例子使用\d
表示匹配数字,后面跟普通字符"天",这样就能从上面的一段文字中提取出几天,注意:虽然1也是数字,但是它不符合表达式,因为表达式是数据后面跟着"天"。
2.3 特殊符号
普通字符代表字符本身,有些普通字符通过转义代表了另一类含义。而有些字符不需要转义就代表了与本身不同的含义,如果要表示这些字符本身的含义,还得通过转义符来转义。这些字符就是我们说的特殊字符:
特殊字符 | 说明 |
---|---|
\ | 转义符,如果要表示\本身,还得通过转义符,写成\\ ,后面的特殊字符同理,不在累述 |
. | 表示任意字符,但不包括换行符 |
| | 表示或 |
[ ] | 表示匹配中括号中的任意一个字符,如[ab],表示匹配a或b,与|的含义相同 |
[^ ] | 表示不能匹配中括号中的任意一个字符 |
.
表示任意字符,但是包括换行符,如果我们要代表任意字符可以使用前面的\s
和\S
的组合[\s\S]
。
2.4 定位符
定位符比较特别,前面我们讲的不管是普通字符、还是特殊字符它匹配的要么是单个字符,要么就是字符集中的某个字符,但终归说来就是匹配的字符,而定位符却是匹配的位置,注意是位置而不是字符。如果要表示字符本身,也需要使用转义符。
定位符 | 说明 |
---|---|
^ | 匹配字符串开始位置,注意^ 与[^ ] 中的^不同,[^ ] 中^与[ 是一个整体的,表示非的意思 |
$ | 匹配字符串的结束位置 |
\b | 表示单词的边界 |
\B | 表示非单词边界 |
^
和$
表示字符串的开始和结束位置,这个很好理解。而\b
表示单词的边界,我们知道单词是以空格隔开的,那么单词的边界就是一个单词的开始或结尾处,\B
表示非单词的边界,即非单词的开始或结尾处。例如:
let text = "element release bele";
let patt = /ele/g;
let tmp = patt.exec(text);
//["ele", index: 0, input: "element release", groups: undefined]
tmp = patt.exec(text);
//["ele", index: 9, input: "element release", groups: undefined]
tmp = patt.exec(text);
//["ele", index: 17, input: "element release bele", groups: undefined]
tmp = patt.exec(text);
//null
通过普通字符能够找到3个匹配的字符,位置分别在0,9和17。下面我们看看通过\b
和\B
查找。
let text = "element release bele";
//使用\b定位在单词的开始位置
let patt = /\bele/g;
patt.exec(text);
//["ele", index: 0, input: "element release", groups: undefined]
//使用\B定位ele元素不在开始或结束的任意位置
patt = /\Bele/g;
patt.exec(text);
//["ele", index: 9, input: "element release", groups: undefined]
//使用\b定位在单词的结束位置
patt = /ele\b/g;
tmp = patt.exec(text);
//["ele", index: 17, input: "element release bele", groups: undefined]
使用\B
能找到不是以ele开头或结尾的内容,\b
表示单词的边界,边界分为开始处和结尾处,\b如果加在前面代表匹配单词的开始处\bele
,加在后面表示匹配单词的结尾处ele\b
2.5 限定符
限定符表示重复的次数,与其它的类搭配使用。如果要表示字符本身的含义,也需要使用转义符
限定符 | 说明 |
---|---|
+ | 表示+前面的内容,重复1次到N次 |
* | 表示*前面的内容,重复0次到N次 |
? | 表示?前面的内容,重复0次或1次 |
{n} | 表示{}前面的内容,重复n次 |
{n,m} | 表示{}前面的内容,重复n次到m次 |
{n,} | 表示{}前面的内容,至少重复n次 |
? | 限定符之后,表示懒惰模式 |
上面出现了两个?
号,表示?
有2种用法,一种是在字符或分组之后,表示重复0次或1次。第2中是在限定符之后,表示懒惰模式。
通常正则表达式在加了限定符后,默认都会匹配尽可能多的字符,这种叫做贪婪模式,比如:
let text = "gooooogle;
// 贪婪模式
let patt = /o+/g;
text.match(patt);
//["ooooo"]
// 懒惰模式
let patt = /o+?/g;
text.match(patt);
//["o", "o", "o", "o", "o"]
可以看出贪婪模式下,表达式匹配了5个连续o,返回了一个"ooooo"字符串,在懒惰模式下,表达式匹配了1个连续o
5次,所以返回了5个o。
2.6 分组
分组使用()
来表示,它将括号内的表达式看做一个整体。可以将分组内的表达式看做整个表达式的一个子表达式。子表达式还可以嵌套子表达式。
2.6.1 分组的限定
我们前面做限定(如+,*,?,{n,m})时,都是针对限定符前面的单个字符,比如https?
表示的是对s这个字符重复0或1次,代表的是http和https,分组的一个用途是将字符组成一个整体,这样就可以对这个整体(分组)做限定。
let text = "haha"
let patt = /haha/g
let tmp = text.match(patt);
// ["haha"]
patt = /(ha)+/g
tmp = text.match(patt);
// ["haha"]
/(ha)+/g
是对ha分组做+限定。
2.6.2 回溯引用
回溯引用的含义是,在同一个表达式中,后面的部分可以用\n
的方式引用前面分组匹配的结果。正则表达式会为每个分组从1开始顺序的分配一个编号。结合前面的\n方式,当n=1,表示第一个分组的内容,以此类推。比如我们要查找html文档中所有的h标签,可以使用如下表达式:
let patt = /<(h[1-6])[\S\s]*?<\/\1>/ig
/<(h[1-6])[\S\s]*?<\/\1>/ig
表达式使用全局标记g和忽略大小写标记i,<是普通字符,(h[1-6])作为一个分组,能匹配h1-h6的内容,这个分组的编号为1,表达式的后面部分使用\1
引用这个分组的内容,[\S\s]*
表示任一空格和非空格字符重复0到n次,及表示的是任意行数的全部内容,注意.*
表示的是单行内容,因为.
不包含换行符,所以重复n次,也只能是一行的内容,这里的?
加载限定符之后,表示懒惰模式。
回溯引用还可以用于高级的替换场景,这个需要结合具体的脚本语言来实现,下面以js为例
let text = "<p>如有需要,请发邮件到 xxx@gmail.com </p>";
let patt = /(\b\w+[\w\.]*@[\w+\.]+\.\w+\b)/g;
let tmp = text.replace(patt,"<a href='mailto:$1'>$1</a>");
//<p>如有需要,请发邮件到 <a href='mailto:xxx@gmail.com'>xxx@gmail.com</a> </p>
上面的场景是将邮箱地址替换为可点击的链接,在js中使用$符号来获取回溯引用。
2.7 前后查找
前后查找(Lookaround)有时又叫零宽度匹配,它和定位符(^,$,\b,\B)有点像,都是匹配的是位置,与定位符不同的是,定位符的位置是预先定义好的,要么是字符串的开始或结尾,要么就是单词的边界与非边界。而前后查找可以通过子表达式(定位条件)来定位位置。
与单词边界\b
相似,在前后查找中,定位的位置(表达式所匹配的内容)也分为前与后,及向前查找与向后查找
类型 | 语法格式 | 说明 |
---|---|---|
向前查找 | (?=exp) | 返回表达式exp所匹配内容之前的位置 |
向后查找 | (?<=exp) | 返回表达式exp所匹配内容之后的位置 |
前后查找也可以组合起来使用,比如要取标签p中的内容:
let text = "<p>如有需要,请发邮件到 <a href='mailto:xxx@gmail.com'>xxx@gmail.com</a> </p>";
let patt =/(?<=<p>)[\s\S]*?(?=<\/p>)/g
let tmp = text.match(patt);
//["如有需要,请发邮件到 <a href='mailto:xxx@gmail.com'>xxx@gmail.com</a> "]
上面组合使用了向前查找和向后查找,(?<=<p>)
是向后查找,返回条件<p>
后面的位置,(?=<\/p>)
是向前查找,返回</p>
结束标签前面的位置。[\s\S]*?
中间采用懒惰模式匹配两个位置(及p标签)之间的内容。
let text = "1234567890";
let patt = /\B(?=(\d{3})+$)/g
let tmp = text.replace(patt,",");
//1,234,567,890
text = "1234567890.54";
patt = /\B(?=(\d{3})+\.)/g
tmp = text.replace(patt,",");
//"1,234,567,890.54"
上例是给数字按3位加逗号,使用了向前查找表达式,整个表达式开始使用了\B,表示位置只会出现在字符串的非边界上,防止逗号加在整个字符串的外面。(?=(\d{3})+$)
是向前查找,返回满足(\d{3})+$
内容前面的位置,注意表达式最后的定位符是$,可以理解为结束位置是字符串的最后。当有小数点的时候,结束位置应该换为\.
。
前后查找是查找满足条件的内容的前或后的位置。还有一种是查找不满足条件的内容的前后位置,称为负向前后查找,这种用的比较少。
类型 | 语法格式 | 说明 |
---|---|---|
负向前查找 | (?!exp) | 返回不能匹配表达式exp内容之前的位置 |
负向后查找 | (?<!exp) | 返回不能匹配表达式exp内容之后的位置 |
2.8 正则表达式中的条件
在处理复杂的业务逻辑时,可能需要在正则表达式中嵌入条件,及只有满足特定条件时,才执行相应的表达式。嵌入条件语法使用?,它有两种情况:
- 根据回溯引用来进行条件处理
- 根据前后查找来进行条件处理
类型 | 语法格式 | 说明 |
---|---|---|
回溯引用 | (?(ref)true_exp|false_exp) | ref是前面的表达式的引用编号,这里填写数字即可,从1开始(与回溯引用的\n相同),true_exp表示ref匹配到内容时执行,false_exp表示ref匹配不到内容时执行,false_exp是可选内容 |
前后查找 | (?(?=exp)true_exp|false_exp) | (?=exp)是前后查找的表达式,也可以换为(?>=exp),true_exp表示前后查找能匹配时执行,false_exp表示前后查找不能匹配时执行,false_exp是可选内容 |
3 参考
[1] 正则表达式30分钟入门教程
[2] JavaScript高级程序设计(第3版)
[3] 正则表达式必知必会(修订版)
网友评论