正则表达式 – 进阶篇

DinS          Written on 2017/6/3

在阅读本文前确保你阅读并理解了《正则表达式 – 基础篇》的内容。

第六节:位置匹配

当需要匹配的内容跟位置有关时,我们需要引进表达位置的字符。
使用\b指定单词边界。b=boundary,其匹配的是一个能够构成单词的字符(\w)和一个不能够构成单词的字符(\W)
\b只匹配一个位置,其本身不是要匹配的内容。也可以只在单词的前或后加\b,这样只匹配开头或结尾。
\B表示不匹配一个单词的边界,通常不会用到

//case6-1: find cat
string strText6_1 = “The cat scattered the milk.”;
regex r6_1(“\\bcat\\b”);
RegexExamplePrintAll(strText6_1, r6_1);

语义功能:找出单词cat
结果:cat
分析:因为使用了\b,所以scattered里的cat没有匹配


使用^匹配字符串开头,使用$匹配字符串结尾

//case6-2: get notes that occupy a whole line
string strText6_2 = “int arrScores[10]\r\n//This array is used for storing students’ scores\r\nfor(int i = 0; i != 10; ++i)\r\n//traverse whole array\r\n{ //begin\r\n arrScores[i]=0 //initialize\r\n} //end”;
regex r6_2(“^\\s*//.*”);
RegexExamplePrintAll(strText6_2, r6_2);

语义功能:提取单独成行的注释
结果://This array is used for storing students’ scores //traverse whole array
分析:^\s*表示一行的开头可以有任意空白字符,之后紧接//表示注释开始,然后接任意字符。
.一般不会匹配换行符,所以放在这里可以。
注意开头的^,有了这个限定像{ //begin这样的就不会匹配

第七节:子表达式

子表达式能够让pattern更加精确,而且也是使用一些高级表达的必要条件
使用()标明子表达式。
连接子表达式可以用|,表示或

//case7-1: IP address
string strText7_1 = “12.159.46.200 abc.987.gd.48 172.0.0.1 99.999.99.999”;
regex r7_1(“(\\d{1,3}\\.){3}\\d{1,3}”);
RegexExamplePrintAll(strText7_1, r7_1);

语义功能:找到合法的IP地址格式
结果:12.159.46.200 172.0.0.1 99.999.99.999
分析:子表达式(\d{1,3}\.){3}匹配了一个1-3位数加上一个.,对应IP地址的前3段,最后一段没有.所以单独写出来
这个pattern并不能筛选出合法的IP地址,比如99.999.99.999


子表达式嵌套:复杂但是功能强大
rule of thumb: 把必须匹配的情况考虑周全,并写出一个结果符合预期的正则表达式容易
把不需要匹配的情况也考虑周全并确保它们被排除在匹配结果之外要困难得多
案例:上述的IP地址匹配不完善,会把非法IP也匹配进来,每一组数字的范围应该在0-255之间
解法:运用逻辑思维,把相匹配什么和不想匹配什么定义清楚
合法的IP地址应该符合以下模式:
任何一个1位或2位数字 0-99
任何一个以1开头的3位数 100-199
任何一个以2开头、第二位数字在0-4之间的三位数 200-249
任何一个以25开头、第三位数字在0-5之间的三位数 250-255
综合起来就完成了0-255的范围限定

//case7-2: valid IP address
regex r7_2(“(((\\d{1,2})|(1\\d{2})|(2[0-4]\\d)|(25[0-5]))\\.){3}((\\d{1,2})|(1\\d{2})|(2[0-4]\\d)|(25[0-5]))”);
RegexExamplePrintAll(strText7_1, r7_2);

语义功能:找到合法的IP地址
结果:12.159.46.200 172.0.0.1
分析:pattern看起来很复杂,实际上就是上例pattern的扩展。解法中的四个任何写成四个子表达式,用|连接,乍看起来摸不着头脑,分解看是这样:
(\\d{1,2}) = 任何一个1位或2位数字
(1\\d{2}) = 任何一个以1开头的3位数
(2[0-4]\\d) = 任何一个以2开头、第二位数字在0-4之间的三位数
(25[0-5]) = 任何一个以25开头、第三位数字在0-5之间的三位数
当然你可能会问为什么不用类似0 < x < 255之类的表达式呢?因为正则表达式不懂数学,它只能进行纯文本的一个一个字符匹配,于是一个范围需要四个子表达式组合
该例中给出的pattern是一个典范,因为其准确无误地做到了只匹配合法IP不匹配非法IP

第八节:回溯引用(back reference)

也称前后一致匹配。回溯引用指的是pattern的后半部分引用前半部分中定义的子表达式。可以把回溯引用想象成变量。
使用\1或\2……来回溯引用,\1代表引用第一个子表达式,\2第二个子表达式,以此类推
不同实现对回溯引用的语法有区别,有些语言里使用$而不是\。
通常\0代表第0个匹配,即整个正则表达式。

//case8: which is a valid date – revisited
string strText8 = “4/8/03 5_9/12 32/6/2015 10-6-2004 11-123-98763 4.13-2010 5-12+1995 7/4-2017”;
regex r8(“\\d{1,2}([-/])\\d{1,2}\\1\\d{2,4}”);
RegexExamplePrintAll(strText8, r8);

语义功能:掌握回溯引用后再来看看合法日期问题
结果:4/8/03 32/6/2015 10-6-2004
分析:通过()把连字符[-\]当作子表达式,然后使用回溯引用\1,成功实现了连字符前后一致的标准

第九节:文本替换

虽然之前都在讲搜索,但是文本替换也是正则表达式的重要应用,尤其在转换数据格式方面有奇效。
需要两个pattern,一个用来搜索匹配,另一个指定替换后的格式。特别注意回溯引用可以跨模式使用。

//case9-1: telephone number format
string strText9_1 = “313-555-1234 010-287-4879”;
regex r9_1(“(\\d{3})(-)(\\d{3})(-)(\\d{4})”);
string fmt9_1(“($1) $3-$5”); //注意这里是string
RegexExampleReplaceAndPrintAll(strText9_1, r9_1, fmt9_1);

语义功能:把电话号码的格式转换为(XXX) XXX-XXXX的形式
结果:(313) 555-1234 (010) 287-4879
分析:将pattern划分为5个子表达式,然后在fmt中引用第1、3、5个,即电话号码的实质信息,将其放入指定的格式中
注意这里使用了$,也是回溯引用。经测试对于C++而言在fmt中必须使用$才能起到回溯效果

替换时可以进行大小写转换
\E 结束\L或\U
\l 把下一个字符换成小写
\L 把\L和\E之间的字符全部换成小写
\u 把下一个字符换成大写
\U 把\U和\E之间的字符全部换成大写

//case9-2: to upper case
string strText9_2 = “<H1>Welcome to my homepage!</H1>”;
regex r9_2(“(<H1>)(.*?)(</H1>)”);
string fmt9_2(“$1\\U$2\\E$3”);
RegexExampleReplaceAndPrintAll(strText9_2, r9_2, fmt9_2);
经测试C++的<regex>暂时不支持该特性

第十节:前后查找(look around)

有时会遇到这种情况:匹配模式本身并不是我们想要的东西,我们只是利用模式确定正确的位置。也就是说匹配模式本身并不是匹配结果的一部分。
这时候就需要用到前后查找。比如搜索html标签内的内容
向前查找指定了一个必须匹配但是不在结果中的返回模式,使用(?=……)
向后查找与向前查找类似,使用(?<=……)

//case10-1: get url
string strText10_1 = “http://www.forta.com\r\nftp://ftp.forta.com\r\nhttps://www.forta.com”;
regex r10_1(“.+(?=:)”);
RegexExamplePrintAll(strText10_1, r10_1);

语义功能:提取URL的协议部分
结果:http ftp https
分析:(?=:)匹配一个:,但是:并不在输出结果里。
任何一个子表达式都可以前后查找,可以有多个前后查找且出现在不同位置

//case10-2: get price
string strText10_2 = “ABC01: $23.45”;
regex r10_2(“(?<=\\$)[0-9]+”);
RegexExamplePrintAll(strText10_2, r10_2);
//经测试C++的<regex>暂时不支持该特性

第十一节:嵌入条件

使用这个功能是为了满足一些复杂的匹配条件,形式上很像if…else…
回溯引用条件:
语法形式:?(back reference)true-regex。 back reference是一个回溯引用,true-regex是只在back reference成立条件下才执行的子表达式
注意:?()里的回溯引用不需要转义,比如应该写(1)而不是(\1)

//case11-1: valid html tags
string strText11_1 = “<A><IMG SRC=home.gif></A>\r\n<IMG SRC=spacer.gif>\r\n<A><IMG SRC=not_valid.png>”;
regex r11_1(“(<A>?)<IMG\\s+[^>]+>(?(1)</A>)”);
RegexExamplePrintAll(strText11_1, r11_1);
分析:第一个子表达式匹配<A>,有个?说明可有可无。中间的匹配任意实质内容。最后是回溯引用条件:如果第一个子表达式存在,即 <A>存在,
则需要匹配一个</A>,否则就不用匹配任何东西
//经测试C++的<regex>暂时不支持该特性

上例相当于if…,下面再看一个if…else
语法形式:?(back reference)true-regex|false-regex
案例:匹配美国电话号码,合法格式(123) 456-7890 或者 123-456-7890
pattern: (\()?\d{3}(?(1)\)|-)\d{3}-\d{4}
分析:最前面的表达式(\()?表达的语义是一个可选的(,之后接3位数字。然后是一个条件式:如果子表达式存在,即(存在,则需要匹配一个)
,否则需要匹配一个-。后面就是简单地匹配数字
嵌入表达式很复杂,最好先对整个pattern的各个部分分别解读,最后再把它们拼装到一起

Bonus: 常见案例:

中国身份证号码:前6位户口所在地编号,其中第一位是1-8,此后是出生年月日,前两位只可能是18、19、20,
pattern: [1-8]\d{5}(18|19|20)\d{2}[0-1]\d[0-3]\d\d{4}

IP地址: 前例已讲过

URL:相当有难度,取决于想获得什么程度的匹配
简单模式: https?://[-\w.]+(:\d+)?(/([\w._/]*)?)?
note:             协议       主机名 端口号 文件路径:外层子表达式匹配可选的/(URL常见于末尾),内层子表达式匹配路径本身
完备模式: https?://(\w*:\w*@)?[-\w.]+(:\d+)?(/([\w._/]*(\?\S+)?)?)?
note:增加了                  用户名和口令字                                     查询字符串

电子邮箱:
pattern: (\w+\.)*\w+@(\w+\.)+[A-Za-z]+
note:                                                  顶级域名
精度并非完全,但是通常够用