第四篇技巧篇	52
一.匹配具有多种形态结构的字符串	52
	1.匹配下列文本中的
标签,它可能呈现的形式例举如下: 52 2.匹配浮点数,它可能有下列几种呈现形式: 53 3.匹配某范围内的数据 53 讨论: 54 二、匹配特定位置上的字符串 54 三、匹配其内部由相似结构字符串构成的字符串 56 四、匹配一段文本,这段文本中不能包含特定字符串 58 五、匹配一对特殊字符界定的之间的字符串,但其内部包含两端的界定字符 59 例1 目标文本 59 例2 目标文本 60 讨论: 62 结束语 63

文本处理归根到底是对特定串的捕捉(查找),所以我们讨论的所谓”技巧”聚焦于对特定字符串的描述.而现实中待处理的字符串在目标文本中的存在状态形形色色,与大多数文章以文本类型归类不同,本篇根据字符串结构特征分类进行讨论,试图从正则特性角度帮助初学者提高综合应用正则符号的能力.文中例举的是常见任务类型或任务的一部分,我们精选了一些较短小的,”天才”般构建的正则表达式进行讨论,相信通过你的仔细解读,一定会心有所悟,提升编写正则表达式的思维境界.也欢迎朋友们跟贴,把你所见所写的自认为有新意的正则表达式展示出来,供大家学习讨论.本篇的正则表达式有的来自世界级专家,也有的来自网络网友,在此一并致谢!

一. 匹配具有多种形态结构的字符串

 在现实的世界中,有些数据类型具有多种呈现形式,如HTML标签,它可能有属性也可能没有,在一些位置上可能有若干空格也可能没有等等.

下面来讨论几个实例:

 1. 匹配下列文本中的<HR>标签,它可能呈现的形式例举如下:

<HR>

<HR >

<HR size = 14 >

正则表达式:

<HR(\s+size\s*=\s*\d+)?\s*>

字符串中,”<HR”与”>”是标签的标志,是肯定出现的,所以用字面字符表示; 它的size属性可能出现也可能不出现,可用”?”描述,属性是一个相对独立的整体所以用括号包围,(…)?表示要么整体出现,要么都不出现;标签与属性之间至少必须有一个空格,用\+表示;而其它有些地方可以没有空格也可以有多空格用\s*表示.

 2. 匹配浮点数,它可能有下列几种呈现形式:

-56

0

+0.14

35.699

.123

正则表达式:

[+-]?\d+(\.\d+)?|\.\d+

+或-可用字符组[+-];它们可有可无可用[+-]?;

整数部分部分可用\d+

小数部分可用\.\d+ ,小数部分可有可无,可用可选项元字符?,由于小数部分如果出现则小数点与后面的数据将同时出现,反之,同时不出现,所以用分组括号包围让?号整体作用即:(?:\.\d+)?

于是整合起来就是[+-]?\d+(?:\.\d+)?

整数部分也不是必须出现的,即实际浮点数表示法,可以存在只是小数部分也合法.能否用[+-]?\d*\(\.\d+)?表示各种情形呢?这是不行的.因为在这个表达式中所有子表达式都可以不出现,就意味着什么都不匹配也匹配成功.也意味着什么都可以匹配.

它的解决办法是用选择符把它们作为两部分分别匹配两种可能的情况.即

[+-]?\d+(\.\d+)?|\.\d+

在这里选择分支的前后顺序很重要,不能交换,你知道为什么吗?如果不明白建议回头再研究一下原理.

 3. 匹配某范围内的数据

 有时候我们需要匹配一个数值范围,如一天内的小时数,一月(年)内的天数,等等.遗憾的是正则表达式的元字符(序列)里没有专门用来表示数据范围的符号.

在正则中想要匹配包含多于一个数字的整数,就必须罗列出所有的数字组合.先看几个例子:

1-12

正则表达式:

^(1[0-2]|[1-9])$

1-22范围内有,1, 2, 3, ...12共有12个数字,事实上我们可以用多选结构把它们连接起来:1|2|...|12, 显然这不是我们需要的方案.

我们把1-12分为两部分:1-9和10-12. 第一部分可用字符组[1-9];第二部分可用表达式1[0-2],然后用多选结构组合起来,即实现了表示1-12范围内的数值.这时也要注意子表达式在多选结构中的顺序.

1-31

正则表达式:

^3[01]|[12][0-9]|[1-9]$

同样的思路把数值范围分为了三个部分,然后用多选结构组合起来,朋友们可自己去分析.

0-100

^100|[1-9][0-9]?$

在这里巧妙地用了一个可选项元素?,令第二个子表达式即可表示两位数字的数值,也可以表示只有一位数字的数值.这个表达式也可以写为

^100|[1-9][0-9]|[0-9]

从前面原理部分知道,多选结构采用的是回溯算法,所以这个表达式的效率不如用可选项元素?的效率高.

0-255

正则表达式:

^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d$

这是一个IP4地址的各组数据允许的范围.我们把它分为四个独立的部分:第一25[0-5]表示250-255;第二2[0-4]\d表示200-249;第三1\d{2}表示100-199;第四[1-9]?\d表示0-9和10-99

 讨论:

 (1) 表示多个字符之一时用字符组,表示多个字符串之一时用多选结构,表示可出现可不出现用可选项元素,表示可出现多次时用”+”或”*”. 所以在表示具有多种可能呈现状态的复杂字符串时,一般方法是把它们分组,然后选择字符组和量词连结起来;事实上,当在现实的世界中有较多的问题都可以把它转化为这种匹配类型.

(2) 一个正则表达式中,是不允许所有子表达式都是可选的情况.解决的方法是用多选结构把它们分为独立的多项.

(3) 使用正则表达式来匹配整数区间的所有技巧:你只需要对区间进行简单拆分,直到拆分之后的所有区间都只包含固定个数的彼此无关的数字为止.这个拆分思维很重要,在以后的技巧中我们还会遇到.

二、匹配特定位置上的字符串

前几天看到一个正则求助的帖子,希望从一段包含汉字及数字的文本中提取所需数字数据.稍微麻烦的是文本中除了有要提取的数字数据外还有不需要提取的数字数据.很多坛友给出了正则表达式解法,但该求助者不满意,认为如果数字数据形式和在文本中位置改变了就不行了,于是坛友不断根据他的新要求修正正则表达式;有的回帖干脆直接用正则删除掉不要的数据与汉字,只留下需要的数据;事实上,坛友们的回帖解法方向都是正确的.结果求助者最后回帖十分遗憾地感叹到:”所有的回答都不尽我意,我是希望用正则表达式直接匹配要提取的数据!”

为什么他有这个想法呢?是因为他可能认为(也是较多初学者认为的)既然正则符号可以描述所有字符,那么一定可以描述要提取的数字数据(毕竟世界上没有一片相同树叶嘛).这种想法的误区是自觉不自觉地把正则符号孤立起来.

事实上,对于那些在特定文本中自己具有唯一数据结构特征的字符串,是可以通过直接描述它们,并加以处理的.但是如果要提取的字符串的结构特征在文本中不是唯一的,就必须以正则的思维将它们与之区别开来.而在正则中只能辅之以特定位置特征将提取字符串锁定.例如: ^\s+可锁定行首空格;\s+$可以锁定行尾空格等.

字符串在文本中的位置,可以用锚点^$,也可以用环视或者直接用与之相邻的其它特殊字符(串).

例1:从一段文本里的URL中抽取通信协议方案.

正则表达式

[a-z][a-z0-9+\-.]*(?=://)

URL中 ”://” 部分的前面是通信方案,如:http之类的.在这里用环视(?=://)锁定的特定位置上的字符串,即提取的字符串”通信方案”,它的位置特征是后面紧跟特定字符串”://”.

前面的两个字符组共同表达了”通信方案”的数据结构特征:第1个位置上必须是一个英文字母;字母后面可以是第二个字符组中列出的字符的任意组合.

例2:在一大段文本中查找独立存在的任意十进制正整数.

正则表达式:

(^|\s)\d+(?=$|\s)

表示正整数很简单,即\d+ .为了能在文本中把它与其它可能存在的正整数区分开来,按题目要求,在表达式的前面增加(^|\s),后面增加(?=$|\s),它们分别描述了要提取的正整数的位置特征: 即这个数的前面必须紧挨着行开始或者是一个不可见字符;后面必须紧跟着行尾或一个不可见字符.

例3:查找后面不跟着某个特定单词(如cat)的任意单词

正则表达式:

\b\w+\b(?!\W+cat\b)

单词可以用\w+表示,在一段文本中要用单词分界行描述”单词”存在形态. 后面否定顺序环视(?!\W+cat\b)表示如果紧跟着一个或一个以上非单词字符(如空格) 并且后面还有一个单词cat,那么否定环视将报告匹配失败,于是整个匹配失败,否则报告匹配成功.即后面不存在单词cat,则整个匹配成功.

使用表位置的正则符号,不仅常用于锁定特定字符串,而且有时可提高匹配效率. 如:假如你要在一个有若干行的文本中,搜索行首的字符串”ABCD”, 那么最好加上脱字符”^”, 即^ABCD. 这样对于有的引擎来说,它只会在行首查找,如果在行首搜索失败,则引擎马上报告失败,它不会继续在文本行的其它位置搜索.

即使对没有作此优化的引擎,当在行首搜索失败进,它也只会在其它位置上开始搜索此位置是否开始位置,不是则马上放弃,移至下一位置.

如果没有在前面加上脱字符”^”,那么当在开始处搜索失败,引擎必须在其它每一个位置上搜索. 当遇到有连续出现的A或AB或ABC等字符串,那么它会按部就班地尝试下去,直至失败.这样显然增加了报告失败结论的匹配尝试次数.

三、匹配其内部由相似结构字符串构成的字符串

 例1:匹配一个IPv4地址

正则表达式:

25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d(?:\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d){3}

看一个具体的IPv4地址: 61.135.136.142 我们知道IPv4地址有如下规范:

(1) 有四组阿拉伯数字组成的数据;

(2) 数据组之间用英文点号分隔;

(3) 每组数据范围是:0-255

数值范围0-255,在上面已经遇见过,我们可以用25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d表示;后面三组数据其结构相似:都是由英文句点与数据组成.所以,我们用括号包围起来,然后跟上一个表范围的量词{3},它表示出现并且只出现三次括号内的字符串.

这时用了非捕获性括号(?:…),只起分组作用.事实上,凡是不需要存储的分组都应该有非捕获性括号.本篇中,有些地方我们用了捕获性括号,是为了让代码清晰一点,在实际应用中可修改.

例2:验证一个URL地址

严格按URL地址规范编制的正则表达式较复杂,下面是一个具有给出条件的正则表达式:必须包含一个域名,不允许用户名或口令

(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+([/?].+)?

我们先来看一个具体的IPv4地址:(它就是本页面的URL地址)

http://club.excelhome.net/forum.php?mod=viewthread&tid=1128647&pid=7697090&page=6&extra=#pid7697090

它可分为三个部分:

(1) 通信协议.具体地址中是http:// .正则表达式中,用(https?|ftp)://表示.它可以匹配三种通信协议之一: http或https或ftp . 它是http|https|ftp更聪明的写法.后面的://是字面字符匹配也可作为识别IP地址的标志之一.

(2) 域. 具体地址中是club.excelhome.net .正则表达式中,用[a-z0-9-]+(\.[a-z0-9-]+)+表示 . 第一个字符组表示域中第一个项允许出现的字母,量词”+”作用于字符组表示可以连续出现多次,但至少出现一次,在这里是不能用量词”*”的.注意后面括号内表示的一个具有特定结构的字符串,该字符串的结构特点是英文句点后跟着多个连续的英文字符或短线.我们用量词”+”作用于这个括号内的字符串,表示可以连续出现一次或多次.

(3) 最后是URL的路径或参数.正则中用 [/?].+ 表示. 这部分前面用了一个字符组规定紧跟”域”后的必须是正斜线”/”或问号”?”之一; 而”.+”表示抓取直到换行符之前的一切字符.正则表达式中,用量词”?”作用于 [/?].+ 部分,表示整个正则表达式也可以匹配没有参数或路径的URL.

例3:验证一个电子邮箱

下面给出一个最严格的电子邮箱正则表达式:

^[\w!#$%&’*+/=?{|}~^-]+(?:\.[!#$%&”*+/=?{|}~^-]+)*@(?:[a-z0-9-]+\.)+[a-z]{2,6}$

它看起来够复杂的,但其实整个结构很简单.共分为五个部分:

第一部分: 也就是第一个字符组[….]+ 表示邮箱开始允许的字符及组合, 注意并没有包含句点;

第二部分: 是非捕获性括号内的内容,其结构为:(\.[…]+)*. 括号的作用量词是”*”,表示可以不出现的.为什么要这部分呢?是因为英文名句点是可以作为邮箱用户名中字符的,但它不能在用户名开头或结尾,也不能出现连续的两个或两个以上句点. 我们通过\.[...]+形式做到了这一点,即英文句点前或后必须至少一个其它字符,通过第一/二部分来表示邮箱用户名,可以严格验证用户名输入是否规范.

第三部分:即”@”,这是邮箱的标志符了.

第四部分:即”@”符号后括号内容.它表示子邮箱组织名,其结构为 […]\. 量词”+”作用于分组,表示至少出现一次;

第五部分:即[a-z]{2-6} . 是邮箱中的顶级域名.该表达式表示了它必须是由2-6个字母组成的字符.比如com

讨论:

我们看到,有些字符串其组成的内部,有相似结构,相似结构块的个数有可能是固定的如IP4地址,更多的是结构块的数目不定, 如URL的域部分和电子邮箱的组织名.这时我们可以用分组结合量词来表达它们.

要指出的是,正则表达式编写的严格程度,总是与上下文环境联系在一起的,在实际应用中,应因地制宜. 比如明明是去匹配一个规范的电子邮箱,你就没有必要用上面的表达式了,而直接用^\S+@\S+$,也许更加快捷.


 


四、匹配一段文本,这段文本中不能包含特定字符串

 我们曾遇到过匹配一对尖括号之间的字符串的例子.可以用<[^>]*>来表达.但如果这一对特定的字符串不是一个字符而是多个字符组成的呢?肯定是不能用否定字符组了,因为字符组内没有”词组”概念,只表达单个字符之一.这时我们可环视来解决这一问题.看下面的例子.

例1 有一目标文本,它也许是一大段文本中的一部分:

….cccc<B>aaa/bbb</B>dddd…..

要求: 提取标签<B>及标签内的内容.即要求结果为:<B>aaa/bbb</B>

正则表达式:

<B>((?!</B>).)*</B>

引擎找到连续字符串<B>后,将在其后的每个位置上尝试匹配否定顺序环视:(?!</B>), 它的意义是如果当前位置的后面没有连续字符串</B>,那么匹配成功,并接着继续扫描正则的下一部分即”.”; 英文句点可以匹配除换行符处的所有字符.我们把它们用括号包围后让量词”*”作用,它的意思就是可以无限次重复((?!</B>).)部分,直到它匹配失败.在上面目标文本中,显然”aaa/bbb”中每个字符之前位置上(?!</B>)都是成功的.所以每个字符都会得到成功匹配.

而当引擎移到</B>的”<”之前位置时,情况发生了变化:因为在该位置上环视的子表达式得到了成功匹配,而否定环视就会让引擎报告匹配失败.但这并不意味((?!</B>).)*部分匹配失败,因为量词”*”表达的是即使括号()内匹配到0个字符(即不匹配),整个子表达式也算匹配成功.于是引擎继续依次扫描正则的最后部分</B>,虽然目标文本中的</B>已经被(?!</B>)测试过,但它是不会”消耗”它们的,所以正则中的最后部分也得到成功匹配.这时引擎发现正则表达式中已经没有元素了,并且各子表达式或元素都匹配成功,于是报告整体匹配成功.

假如标签</B>的结束标签</B>不在同一行中怎么办呢? 元字符”.”是不能匹配换行符的.这时我们可以把”.”换为[\s\S]. 即

<B>((?!</B>)[\s\S])*</B>

字符组[\s\S]可以匹配任意字符之一. 同样也可以用[\w\W]或[\d\D],它们都是等价的.这没有什么好解读的,它是常用技巧之一.可以理解为不可见字符与它的补集就构成了全部.

例2: 查找不包含另一单词(如cat)的任意单词

\b(?:(?!cat)\W)+\b

它的原理与例1是一样的.单词分界符”\b”,限定了连续字符必须在两个字边界之内;这时用了\W,即非英文单词字符代替”.”,目的是明显. 如果用”.”或[\s\S],那么它将匹配到文本中的所有字符.用了非捕获性括号是不想把匹配的单词放入特殊变量$中.因为匹配集合中就是所需要的单词,不必须用Submatches方法提取了.

例3 匹配不包含某个单词(如Cat)的整行

正则表达式

^(?:(?!\bcat\b).)*$

注意例2,例3中单词边界符的应用差别.

讨论:

用上面的方法,其匹配效率是很低的.因为它将在每个位置上尝试(?!</B>)或(?!cat).所以,如果可能,比如例2,3,我们可以用VBA的split方法生成单词数组,然后逐一检查.

 五、匹配一对特殊字符界定的之间的字符串,但其内部包含两端的界定字符

 我们要匹配一对双引号包围的内容,但被包围的内容中含有双引号;匹配一对括号内的内容,但括号内又嵌套括号……看下面的例子:(提示,如果直接输入VBE代码中进行测试,注意在字符变量与正则表达式中双引号的转义)

 例1 目标文本

 ….Needs a “2 \” x3 \” likeness” of …

要求匹配结果:

“2 \” x3 \” likeness”

正则表达式:

“”(\\.|[^\\””])+””

显然,不能用以前用过的办法:如””[^””]””或””.*?”” , 也不能用””.*”” ,因为文本的其它地方还可能出现双引号对.

编写正则表达式的重要技巧之一是: 集中关注在特定时刻真正容许匹配的字符.观察文本中匹配内容中的双引号特点:它始终与反斜线联结在一起.于是情况变得明朗起来,匹配内容实际上是由两部分组成的:一部分是 \” ,即反斜线与双引号的组合;一部分是非反斜线和非双引号的任意字符. 用正则来表达这个意思就是:\\.|[^\\""]我们用分组括号包围它们,然后用量词”+”作用(\\.|[^\\""])+,表示可以重复匹配无限次这样的字符[^\\””]或字符串\\. ,直到它遭遇失败.

现在我们来看它的匹配过程:当在….Need a 后的位置上时,它成功匹配双引号,接着尝试(\\.)部分与文本中”2”匹配失败,于是尝试[^\\””],匹配成功,重复分组,继续下一个位置......当引擎移动到likeness后的位置上时,此时文本的字符是一个双引号,而双引号既不能与(\\.)匹配也不能与[^\\””]匹配,所以此时正则括号部分匹配失败,回溯,而括号的作用量词是”+”,引擎报告整个量词作用的括号部分匹配成功,最后用双引号匹配文本中的双引号成功,完成整个匹配.

再给一个例子,请你自己尝试一下:匹配input.目标文本:

…..<input name=dir value=”>”>…..

 例2 目标文本

 Val=foo(bar(this),3.7)+2*(that-1)

要求:提取文本中括号中的内容,即提取出(bar(this),3.7)和(that-1)

仿例1的思路,真正要匹配的是两部分:一是内嵌的(…)部分; 二是其它普通字符.正则表达式:

\(([^()]|\([^)]+\))*\)

匹配内嵌的(…)部分可用表达式:\([^)]+\);第二部分怎样表达呢?我们用了子表达式[^()]. 因为一当遇到内嵌”(“时,引擎将尝试第二个子表达式,即: \([^)]+\) ,该子表达式会一口气把内嵌括号匹配完.所以整个正则表达式能顺利完成任务.

我们能否用下列正则表达式呢?

\(([^()]*|(\([^)]+\))*)*\)

这个表达式用量词*号作用[^()],可以一次把所有内嵌括号前或后的字符匹配完成,从而减少每次进入退出正则括号的处理次数;同样道理,用”*”作用\([^]]+\)部分,处理有多个内嵌括号的情况.这个主意看来不错.但这是绝对不行的,原理部分讨论过一个问题:灾难回溯.而这个表达式就符合灾难回溯的特征.量词被量词作用.

不过,下面这个表达式应该是一个效率更高的正则表达式:

\([^()]*(\([^()]*\))*[^()]*\)

请自己解读吧.

例3 下面是一个由excel转换为csv文件格式的文本

Ten Thousand,1000,2710,,"10,000","It’s ""10Grand"",baby",10K

说明:excel转存为csv文本,如果原来内容中有双引号,它将自动转换为重复的双引号.

要求:提取由逗号分隔的各项数据

为了更清晰一点下面列出各项数据(共七项):

The Thousand

1000

2710

空值

“10,000”

“It’s “”10 Grand””,baby”

10k

正则表达式:

(?:^|,)(?:""((?:[^""]|"""")*)""|[^"",]*)

除第一个数据项外,其所有数据之前都有逗号分隔,所以我们可以用”^”或”,”来分别锁定各数据,即用(?:^|,)(….)形式分别提取各数据项,最后的结果中可能含有”,”,容易用VBA代码处理.

第二个问题是如何表达各数据项,在技巧一中,已经知道如果匹配项呈现多种状态,可以用多选结构与合适的量词来处理.现在来看它们的各种形态:一是由双引号包围的内容形式; 二是空值; 三是除一二外的形式.

第一种形态可以用例1,例2的方法:关注双引号内的字符实际只有两种类型即连续的双引号,转义后要用””””表达,其次是非双引号的字符用[^””]表达. 于是第一种形态可以表达为 “”(([^””]|””””)*)””,在本例中它可以匹配”10,000” 和"It’s""10 Grand"",baby"

第三种形态实际就是非双引号[^””],而第二种形态空值,可与第三种形态合并即[^””]* ;量词”*”,可以表示不出现任何字符.

把它们整合起来就是:

(^|,)(“”(([^””]|””””)*)””|[^””]*)

这个表达式还有一个问题,即^或,匹配成功后,一当遇到非双引号部分时即尝试[^””]*时,它会匹配后面所有的字符. 如在位置0处它会匹配整个双引号前的部分即: Ten Thousand,1000,2710,,.所以为了迫使它在遇到逗号时停止,我们可以在[^””]中增加一个逗号即[^””,]*.最后的结果是:

(^|,)(“”(([^””]|””””)*)””|[^””,]*)

说明:你可以根据情况,将捕获性括号修改为非捕获括号.

 讨论:

 集中关注在特定时刻真正容许匹配的字符,经常会解决多数问题,在具体处理的时候又利用了前面讨论过的”拆分”思想.即把需要匹配的字符(串),拆分成互不包含的若干部分.最后用多选结构及量词组合起来.


结束语

看来没有什么人关注了,就在此打住了吧.

再次感谢liucqa老师、赵刚老师和香川才女的鼓励和肯定,也非常感谢前面各位朋友的捧场。对有位朋友将之转化为word文档所付出的劳动表示敬意,不过由于有时上帖的疏忽,所以修改了一些内容,并没有包括其中;另外转换的过程中,也有一些小问题,所以请慎用之.由于水平有限,帖中难免有错,热忱欢迎朋友们指正!

原贴地址:

Excel 正则表达式入门与提高-VBA平台的正则学习参考资料-ExcelVBA程序开发-ExcelHome技术论坛 -  http://club.excelhome.net/thread-1128647-1-1.html