Linux

正则表达式

2019-04-16  本文已影响3人  书上得来终觉浅

正则表达式(Regular Expression,简称regex)作为一种工具,它擅长的领域是文本处理。最基本的用途是查找(search/match)和替换(replace)特定的文本内容,在此之上可以延伸到:

1 JavaScript中正则表达式

JavaScript中为正则表达式提供了专门的RegExp类型,可以使用new RegExp()实例化一个正则表达式,不过通常我们使用如下的语法创建一个正则表达式。

let expression = /pattern/flags;

/pattern/表示正则表达式的匹配模式,flags表示该表达式支持的标志。

1.1 正则表达式的标志

一个正则表达式可以带一个或多个flag,flag有如下取值:

flag可以组合使用,比如/exp/gi表示exp表达式用于全局匹配,且忽略大小写。

1.2 正则表达式的用法

我们先不看正则表达式是怎么来的,先看它是怎么用的。使用RegExp主要是其实例方法以及外部支持RegExp的方法。后续的章节中,我们以patt表示表达式,text表示目标字符串。

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()方法就会返回第一次的内容。

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个连续o5次,所以返回了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] 正则表达式必知必会(修订版)

上一篇 下一篇

猜你喜欢

热点阅读