正则表达式 – 基础篇

DinS          Written on 2017/6/3

资料来源:[美]Ben Forta:《正则表达式必知必会》,人民邮电出版社2007年版.

正则表达式这部分内容基本上就是阅读这本书的笔记,涵盖了大部分语法,以供需要时查阅。

正则表达式(regular expression)是强大的文本匹配工具,可以解决几乎所有这类问题。其自身有一套独特的语法,所有编程语言和平台都支持,只不过语法形式上略有出入。正则表达式使用模式(pattern)来确定匹配内容,我们需要做的就是设计一套模式,用这个模式来匹配即可。
正则表达式虽然强大,不过颇为消耗资源,能使用简单的string搜索操作解决的问题就不需要用正则表达式,另外也尽量避免在循环内建立模式。

准备工作:标准库<regex>使用方法

正则表达式主要是自身的语法问题,我们先来看看如何在c++里使用正则表达式库,之后把精力放到语法上去。

#include<regex>包含头文件,就可以使用了。主要用法如下:

该函数的作用是给定原始文本和模式,输出第一个匹配的子串。

regex对象即正则表达式的模式,然后专门有一个smatch用于储存结果。

如果要匹配所有字串,需要用到正则表达式迭代器。看起来略复杂,不过是固定套路,这么写就行。如果深入的话,从语义上将讲即在for循环里定义了一个开始和结束的迭代器,然后迭代器依次访问所有字串。

正则表达式还可以承担替换工作,如上写法即可。

之后在本专题里都将使用这三个函数处理代码层面的问题。

第一节:正则表达式与纯文本匹配

下面开始正式进入正则表达式的语法。

//case1: find cat
string strText1 = “The cat scattered the milk.”;   //原始文本
regex r1(“cat”);                                 //建立模式
RegexExamplePrintFirstHit(strText1, r1);
RegexExamplePrintAll(strText1, r1);

语义功能:找出子串cat
结果:第一个函数输出cat,第二个函数输出cat cat
分析:使用正则表达式匹配纯文本是一种浪费,不过确实能够干这个事。利用这个熟悉C++下的正则表达式语法。
第二个输出有问题,把scattered里的cat也给找出来了。后面将学习如何处理这种情况

第二节:匹配任意单个字符

正则表达式真正的威力在于特殊字符集,这是一套预先定义的具有不同功能的字符。
首先学习字符‘.’,它可以用来匹配任意单个字符。如果想要匹配‘.’而不是使用其特殊功能,需要使用转义字符\。这对其它特殊字符也一样。当然如果需要匹配\就需要使用\\。

注意:因为\在C++中本身也是一个特殊字符,所以必须使用\\来表达\的含义

//case2: sales sheet
string strText2 = “na.xls\r\nTom.xls\r\nsa.xls\r\nBob.xls”;
regex r2(“.a\\.xls”);
RegexExamplePrintAll(strText2, r2);

语义功能:找出特定名字的xls文件
结果:na.xls sa.xls
分析:第一个.匹配任意字符,第二个匹配.本身。\r\n是windows的换行标志

第三节:匹配一组字符

使用[]匹配一组字符集,将希望匹配的字符放入[]即可

//case 3-1: find directions
string strText3_1 = “Tom went to North Club from south islands. He chose to run forth to his destination.”;
regex r3_1(“[Nsf]o[ru]th”);
RegexExamplePrintAll(strText3_1, r3_1);

语义功能:找出带方向的词
结果:North south forth
分析:第一个[]匹配首字符,第二个[]匹配第三个字符

因为匹配时经常会用到区间,所以可以使用‘-’来表示区间内的所有字符。
比如[0-9]表示所有数字,[A-Z]表示所有大写字母,[a-f]表示abcdef
‘-’只在[]里有意义,在区间外就表示该字符本身,不需要转义
更复杂的例子:[A-Za-z0-9]表示所有字母和数字,[0-9A-Fa-f]经常用于匹配颜色,比如#FF00ff(品红色)
还可以对[]取非,使用[^……]

//case 3-2: hit treasure in number jungle
string strText3_2 = “1827 109837 192878 H 277 2109 377673 I 20973 1773 T 2098 3878 2093”;
regex r3_2_1(“[^0-9]”);
regex r3_2_2(“[A-Za-z]”);
RegexExamplePrintAll(strText3_2, r3_2_1);
RegexExamplePrintAll(strText3_2, r3_2_2);

语义功能:在一堆数字中找字母
结果:第一个除了HIT外还有一堆空格,第二个输出HIT
分析:使用了两种pattern,[^0-9]的用意是找出非数字字符,虽然找到了HIT但是混进了其他字符
[A-Za-z]的用意是找出字母,成功完成了任务。
这个例子告诉我们一个重要道理:验证某个pattern能不能匹配到我们想要的东西很容易,但是如何验证其不会匹配到我们不想要的东西可就不简单了。
我们在设计第一个pattern时肯定没有想到会把空格带进来

第四节:元字符

元字符是正则表达式定义的特殊字符,通常前面有一个\
匹配非打印字符的元字符:
[\b] 回退字符(backspace)
\f 换页符
\n 换行符
\r 回车符
\t 制表符(tab)
\v 垂直制表符
比如说可以使用\r\n\r\n来寻找空行,因为连续两个行末必然意味着空行。对于unix\linux只需要\n\n即可

使用字符类简化pattern
\d 任何一个数字字符,等价于[0-9]
\D 任何一个非数字字符,等价于[^0-9]
\w 任何一个数字、字母(大小写均可)、下划线,等价于[a-zA-Z0-9_]。常见于各种名字里
\W 任何一个非\w
\s 任何一个空白字符,等价于[\f\n\r\t\v]。注意:并不包括退格键
\S 任何一个非\s

POSIX字符:也是一种简写形式。注意两个[]
元字符(Metacharacter) 匹配(Matches)
[[:alnum:]] 字母和数字
[[:alpha:]] 字母
[[:blank:]] 空格和制表符
[[:cntrl:]] 控制字符
[[:digit:]] 数字
[[:graph:]] 非空白字符
[[:lower:]] 小写字母
[[:print:]] 类似[[:graph:]],但是包含空白字符
[[:punct:]] 标点符号
[[:space:]] 空白字符
[[:upper:]] 大写字母
[[:xdigit:]] 十六进制中容许出现的数字(例如 0-9a-fA-f)

//case 4: postman
string strText4 = “192876 A4U9H5 398CIJ FU87IO M1B8V2”; //假设某国家邮编格式为6位,偶数位必须是数字,基数位必须是大写字母
regex r4(“[[:upper:]]\\d[[:upper:]]\\d[[:upper:]]\\d”);
RegexExamplePrintAll(strText4, r4);

语义功能:找出合法邮编格式
结果:A4U9H5 M1B8V2
分析:使用了POSIX和元字符简化表达

第五节:重复匹配

经常会遇到重复匹配问题,即匹配若干个字符
使用+来重复匹配一个及以上的字符。跟[]结合的话+要放在外面才能达到+的预期效果
使用*来重复匹配零个及以上的字符。可以这样理解*:其代表的字符是可选的
使用?来匹配零个或一个字符。

//case 5-1: which is a valid email
string strText5_1 = “ben@forta.com fake@com dummy.com ben.forta@forta.com @health.com ben@urgent.forta.com”;
regex r5_1(“\\w+@\\w+\\.\\w+”);
RegexExamplePrintAll(strText5_1, r5_1);

语义功能:找出合法邮箱
结果:ben@forta.com forta@forta.com ben@urgent.forta
分析:邮箱可以拆成两部分,@之前用\w+表示,@之后用\w+ . \w+组合而成。因为.不在\w里所以单独列出来
从结果可以看出明显的假邮箱确实被排除了,但是后两个的结果不正确,也就是说我们把合法的邮箱也给删减了

//case 5-2: which is a valid email (improved version)
regex r5_2(“[\\w.]+@[\\w.]+”);
RegexExamplePrintAll(strText5_1, r5_2);

语义功能:找出合法邮箱
结果:ben@forta.com ben.forta@forta.com ben@urgent.forta.com
分析:在原来的基础上增加了对.的匹配,结果正确。不过要注意这增加了新的风险,比如说ben@forta..com也会被匹配
这里并没有给.转义。通常而言当.+这类字符在[]里使用时被当做字符对待。不过加一个转义字符也没有坏处。
这个案例说明了一个道理:通常而言一个问题有多种正则表达式解法,可以在不同程度上解决问题。我们更关注的是复杂度和精度。

//case 5-3: which is a valid URL
string strText5_3 = “http://www.forta.com httpz://www.what/net https://www.forta.com/”;
regex r5_3(“https?://[\\w./]+”);
RegexExamplePrintAll(strText5_3, r5_3);

语义功能:找出合法URL
结果:http://www.forta.com https://www.forta.com/
分析:以为http后面的s可以有可以没有,所以使用?。


使用{}来指定重复次数,让重复匹配更加精准。比如#[[:xdigit:]]{6}匹配#33FF66
还可以指定重复次数的范围,使用{n,m}表示至少重复n次至多重复m次。
n可以为0,表示0次,比如{0,3}。m可以为空,表示次数不限,比如{3,}。注意不要漏了逗号,否则就成了精确匹配

//case5-4: which is a valid date
string strText5_4 = “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 r5_4(“\\d{1,2}[-/]\\d{1,2}[-/]\\d{2,4}”);
RegexExamplePrintAll(strText5_4, r5_4);

语义功能:找出合法日期格式(这里是美国的写法)
结果:4/8/03 32/6/2015 10-6-2004 7/4-2017
分析:月和日是1或2位数,年份是2或4位数。使用{}指定次数,连字符-/都可以。
这里仅仅是检测格式,通常这一步在有效性检测之前。比如32/6/2015虽然无效但是格式正确
还有一个需要注意的地方,7/4-2017这个格式并不正确,但是使用我们目前掌握的技术无法解决这个问题。加上要求是“连字符必须前后一致”
这又回到了正则表达式永恒的主题:复杂度与精度之间的矛盾。如果你认为数据中不会出现-/这样的问题就不必处理。如果想更加robust,pattern就要更加复杂,阅读和维护的难度也会加大


使用+、*时要注意过度匹配的问题,因为这两个字符会一直吃进去字符。下面看一个经典的例子,你迟早会遇到类似问题。

//case5-5 : html tags
string strText5_5 = “This offer is not available to customers living in <B>AK</B> and <B>HI</B>.”;
regex r5_5(“<B>.*</B>”);
RegexExamplePrintAll(strText5_5, r5_5);

语义功能:找出html一对标签和里面的内容
结果:<B>AK</B> and <B>HI</B>
分析:从pattern上看好像十分正确,匹配首尾标签,内部内容随意。但是结果不正确,多出了and
为什么会这样?因为使用了贪婪型元字符*和+,并且与.组合。这样会一直匹配下去。

//case5-6: html tags(improved version)
regex r5_6(“<B>.*?</B>”);
RegexExamplePrintAll(strText5_5, r5_6);

语义功能:找出html一对标签和里面的内容
结果:<B>AK</B> <B>HI</B>
分析:这次结果正确了,因为使用了懒惰型元字符*?和+?,以及{n,}?。其意义是尽可能少地匹配字符
通常而言应该使用懒惰型元字符,这样更加robust

更高级的正则表达式语法见《正则表达式 – 进阶篇》。