正则表达式(五):浅谈两种匹配操作

在正则表达式中,匹配是最最基本的操作。使用正则表达式,换种说法就是“用正则表达式去匹配文本”。但这只是广义的“匹配”,细说起来,广义的“匹配”又可以分为两类:提取和验证。所以,本篇文章就来专门讲讲提取和验证。

\

提取

\

提取可以理解为“用正则表达式遍历整个字符串,找出能够匹配的文本”,它主要用来提取需要的数据,常见的任务有:找出文本中的电子邮件地址,找出HTML代码中的图片地址、超链接地址……提取数据时,首先要注意的,就是准确性。

\

准确

\

准确性分为两方面:完整精确。前者是要提取出需要的所有文本,不能漏过;后者是要保证提取的结果中没有不需要的文本,不可出错。

\

为保证完整,我们需要考虑足够多的变体,覆盖所有情况。一般来说,要提取的数据都只有概念的描述(比如,提取一个电子邮件地址,提取一个身份证号),如果没有拿到完整规范的特征描述,可能只能凭经验总结出几条特征,然后逐步完善,也就是不断考虑新的情况,照顾到各种情况。

\

拿“提取文本中的浮点数字符串”为例。最容易想到的情况,就是3.14、3999.2、0.36之类,也就是“数字字符串 + 小数点 + 数字字符串”,所以用表达式『\\d+\\.\\d+』,按照我们上一篇文章说过的“与或非”,三个部分都是必须出现的,所以这个表达式似乎是没问题了。

\
\\\d+\\.\\d+ 
\

但是有些时候,0.7是写作.7的,上面的表达式无法照顾这种情况,所以必须修改表达式:整数部分是可能出现也可能不出现的,所以小数点之前的\\d+应该改为\\d*,就成了『\\d*\\.\\d+』。

\
\\\d*\\.\\d+ 
\

但是且慢,浮点数还包括负数,比如-0.7,但现在这个表达式无法匹配最开始的符号,所以还应该改成『-?\\d*\\.\\d+』。

\

-?\\d*\\.\\d+

\

但仅仅保证完整性还不够,提取的另一方面是精确,就是排除掉那些“能够由正则表达式匹配,但其实并非期望”的字符串,所以我们还需要仔细观察目前的正则表达式,适当添加限制条件。

\

仍然用上面的正则表达式作例子,『-?\\d*\\.\\d+』中,『-?』和『\\d*』都是可能出现的元素,所以它们可能都不出现,这时候表达式能匹配.7之类,没有错;如果只出现了『\\d*』能匹配的文本,可以匹配3.14之类,也没有错;但是,如果只出现『-?』呢?-.7,通常来说,负的浮点数是应该写作-0.7的,而-.7显然是不合法的。所以,这个表达式应该修改为『(-?\\d+|\\d*)\\.\\d+』。

\
\(-?\\d+|\\d*)\\.\\d+ 
\

事情到这里就完整了吗?似乎还不是。我们知道有些地方,日期字符串是“2010.12.22”的形式,如果你要处理的文本中不包含这种日期字符串还好,否则,上面的表达式会错误匹配2010.12.22或者2010.12.22。为了避免这种情况,我们需要给表达式加上更多的限制。最直接想法就是,限定表达式两端不能出现点号.,变成『(?!\u0026lt;.)(-?\\d+|\\d*)\\.\\d+(?!.)』。

\
\(?!\u0026lt;.)(-?\\d+|\\d*)\\.\\d+(?!.) 
\

这样确实避免了2010.12.22的错误匹配,但它也造成了新的问题,比如“…the value of π is 3.14. Therefore…”,3.14本来是我们需要提取的浮点数,但加上这个限制之后,因为3.14之后的有一个作为英文句号使用的点号,所以3.14无法匹配。仔细观察我们要排除的2010.12.22这类字符串,我们发现点号.的另一端仍然是数字,而用作句号的点号,另一端必定不是数字(一般是空白字符,或者就是字符串的开头/末尾),所以应当把限制条件表达的更精确些,变为『(?!\u0026lt;\\d.)(-?\\d+|\\d*)\\.\\d+(?!.\\d)』。

\
\(?!\u0026lt;\\d.)(-?\\d+|\\d*)\\.\\d+(?!.\\d) 
\

好了,关于浮点数的匹配就讲到这里。回过头想想得到最后的这个表达式,我们发现,如果要用正则表达式匹配,必须兼顾完整和精确,通常的做法就像这个例子中的一样:先逐步放宽限制,保证完整;再添加若干限制,保证精确。

\

f7c987d616291c25567ec77dc01f2ece.jpg

\

效率

\

提取数据时还有一点需要注意,就是效率。有时要处理的文本非常长,即便进行简单的字符串查找都很费力,更不用说可能出现各种变体的正则表达式了。这时候就应当尽量减少“变化”的范围。比如知道文本中只包含一个双引号字符串,希望将它提取出来,正则表达式写成了『\".*\"』。在文本不长时这样还可以接受,如果文本很长,『.*』这类子表达式就会导致大量的回溯,因为『.*』的匹配过程是这样的:

\

67cef31856b3618a53d8f7e9e313c433.jpg

\

观察匹配过程就会发现,如果字符串很长,而引号字符串又出现在比较靠前的位置,比如\"quoted string\" and long long long text…,匹配时就需要进行大量的回溯操作,严重影响效率。如果这种问题并不是任何情况下都可能发生,但效率确实非常重要的,如果正则表达式编写不当,可以产生极为严重的影响,比如ReDos(正则表达式拒绝服务),具体情况可以参考http://en.wikipedia.org/wiki/ReDoS

\

另一方面,正则表达式提取的效率,不仅与正则表达式本身有关,也与调用的API有关。如果文本很大,要提取出的结果很多,集中到一次操作进行,就可能影响性能,所以条件容许(比如只需要逐步提取出来,依次处理),就可以“逐步进行”,下面的表格列出了常用语言中的提取操作。\

\

语言

\
\

方法

\
\

备注

\
\

Java

\
\

Matcher.find()

\
\

只能逐步进行

\
\

PHP

\
\

preg_match(regex, string, result)

\
\

逐步进行

\
 \

preg_match_all(regex, string, result)

\
\

一次性进行

\
\

.NET

\
\

Regex.match(string)

\
\

逐次进行

\
 \

Regex.matches(string, regex)

\
\

一次性进行

\
\

Python

\
\

re.find(regex, string)

\
\

逐步进行

\
 \

re.finditer(regex, string)

\
\

逐步进行

\
 \

re.findall(regex, string)

\
\

一次性进行

\
\

Ruby

\
\

Regexp.match(text)

\
\

只能找到第一次匹配

\
 \

string.index(Regexp, int)

\
\

逐步进行

\
 \

string.scan(Regexp)

\
\

一次性进行

\
\

JavaScript

\
\

RegExp.exec(string)

\
\

一次性进行

\
 \

string.match(RegExp)

\
\

一次性进行

\

一次性提取所有匹配结果的操作这里不多说,我们要补充讲解的是,在“逐步进行”时,如何真正保证“逐步”?或者说,在第二次调用匹配时,如何保证是“承接”第一次调用,找到下一个匹配结果。通常的做法有几种,以下分别介绍。例子统一使用字符串为\"123 45 6\",查找其中的数字字符串,依次输出123、45、6。

\

如果采用的是面向对象式处理,表示匹配结果的对象,可能可以“记住”匹配的位置,下次调用时自动“继续”,Java就是这样,循环调用Matcher.find()方法,就可以逐个获得所有匹配,在.NET中,是循环调用Match.NextMatch()。

\

代码(以Java为例)

\
\String str = \"123 45 6\"; \Pattern p = Pattern.compile(\"\\\\d+\"); \Matcher m = p.matcher(str); \while (m.find()) { \    System.out.println(m.group()); \} 
\

如果不是面向对象式处理,无法记录匹配的状态信息,则可以手动指定偏移值。多数语言都有办法在匹配时指定偏移值,也就是“从字符串的offset位置开始尝试匹配”。如果要逐一获得所有匹配,每次将偏移值指定为上一次匹配的结束位置即可。注意,字符串处理时可能有人习惯将偏移值指定为“上一次匹配的起始位置+1”,但正则表达式处理时这样是不对的,比如正则表达式是『\\d+』,而字符串是\"123 45 6\",第一次匹配的结果是123,如果把偏移值设定为“上一次匹配的起始位置+1”,之后的匹配结果就是23,3……。在PHP、JavaScript、Ruby中,通常采用这种办法。

\

代码(以PHP为例)

\
\$string=\"123 45 6\"; \$regex=\"/\\\\d+/\"; \$matched = 1; \$oneMatch=array(); \$lastOffset = 0; \$matched = preg_match($regex, $string, $oneMatch, PREG_OFFSET_CAPTURE, $lastOffset); \while ($matched == 1) { \    $lastOffset = $oneMatch[0][1] + strlen($oneMatch[0][0]); \    echo $oneMatch[0][0].\"\u0026lt;br /\u0026gt;\"; \    $matched = preg_match($regex, $string, $oneMatch, PREG_OFFSET_CAPTURE, $lastOffset); \} 
\

第3种办法是使用迭代器,Python的re.finditer()会得到一个迭代器,每次调用next(),就会获得下一次匹配的结果。这种办法目前只有Python提供,其它语言尚不具备。

\

代码(以Python为例)

\
\for match in re.finditer(\"\\\\d+\
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值