正则表达式入门与提高
前几天看到一个正则求助的帖子,希望从一段包含汉字及数字的文本中提取所需数字数据.稍微麻烦的是文本中除了有要提取的数字数据外还有不需要提取的数字数据.很多坛友给出了正则表达式解法,但该求助者不满意,认为如果数字数据形式和在文本中位置改变了就不行了,于是坛友不断根据他的新要求修正正则表达式;有的回帖干脆直接用正则删除掉不要的数据与汉字,只留下需要的数据;事实上,坛友们的回帖解法方向都是正确的.结果求助者最后回帖十分遗憾地感叹到:”所有的回答都不尽我意,我是希望用正则表达式直接匹配要提取的数据!”
为什么他有这个想法呢?是因为他可能认为(也是较多初学者认为的)既然正则符号可以描述所有字符,那么一定可以描述要提取的数字数据(毕竟世界上没有一片相同树叶嘛).这种想法的误区是自觉不自觉地把正则符号孤立起来.
事实上,对于那些在特定文本中自己具有唯一数据结构特征的字符串,是可以通过直接描述它们,并加以处理的.但是如果要提取的字符串的结构特征在文本中不是唯一的,就必须以正则的思维将它们与之区别开来.而在正则中只能辅之以特定位置特征将提取字符串锁定.例如: ^\\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+表示,在一段文本中要用单词分界行描述”单词”存在形态. 后面否定顺序环视
55
VBA平台的正则学习参考资料
(?!\\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地址)
56
正则表达式入门与提高
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的域部分和电子邮箱的组织名.这时我们可以用分组结合量词来表达它们.
57
VBA平台的正则学习参考资料
要指出的是,正则表达式编写的严格程度,总是与上下文环境联系在一起的,在实际应用中,应因地制宜. 比如明明是去匹配一个规范的电子邮箱,你就没有必要用上面的表达式了,而直接用^\\S+@\\S+$,也许更加快捷.
四、匹配一段文本,这段文本中不能包含特定字符串
我们曾遇到过匹配一对尖括号之间的字符串的例子.可以用<[^>]*>来表达.但如果这一对特定的字符串不是一个字符而是多个字符组成的呢?肯定是不能用否定字符组了,因为字符组内没有”词组”概念,只表达单个字符之一.这时我们可环视来解决这一问题.看下面的例子.
例1 有一目标文本,它也许是一大段文本中的一部分: ….ccccaaa/bbbdddd…..
要求: 提取标签及标签内的内容.即要求结果为:aaa/bbb 正则表达式: ((?!).)*
引擎找到连续字符串后,将在其后的每个位置上尝试匹配否定顺序环视:(?!), 它的意义是如果当前位置的后面没有连续字符串,那么匹配成功,并接着继续扫描正则的下一部分即”.”; 英文句点可以匹配除换行符处的所有字符.我们把它们用括号包围后让量词”*”作用,它的意思就是可以无限次重复((?!).)部分,直到它匹配失败.在上面目标文本中,显然”aaa/bbb”中每个字符之前位置上(?!)都是成功的.所以每个字符都会得到成功匹配.
而当引擎移到的”<”之前位置时,情况发生了变化:因为在该位置上环视的子表达式得到了成功匹配,而否定环视就会让引擎报告匹配失败.但这并不意味((?!).)*部分匹配失败,因为量词”*”表达的是即使括号()内匹配到0个字符(即不匹配),整个子表达式也算匹配成功.于是引擎继续依次扫描正则的最后部分,虽然目标文本中的已经被(?!)测试过,但它是不会”消耗”它们的,所以正则中的最后部分也得到成功匹配.这时引擎发现正则表达式中已经没有元素了,并且各子表达式或元素都匹配成功,于是报告整体匹配成功.
假如标签的结束标签不在同一行中怎么办呢? 元字符”.”是不能匹配换行符的.这时我们可以把”.”换为[\\s\\S]. 即
((?!)[\\s\\S])*
字符组[\\s\\S]可以匹配任意字符之一. 同样也可以用[\\w\\W]或[\\d\\D],它们都是等价的.这没有什么好解读的,它是常用技巧之一.可以理解为不可见字符与它的补集就构成了全部.
例2: 查找不包含另一单词(如cat)的任意单词
58