`
carvin
  • 浏览: 212613 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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

    博客分类:
  • java
阅读更多

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

提取

提取可以理解为“用正则表达式遍历整个字符串,找出能够匹配的文本”,它主要用来提取需要的数据,常见的任务有:找出文本中的电子邮件地址,找出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。为了避免这种情况,我们需要给表达式加上更多的限制。最直接想法就是,限定表达式两端不能出现点号.,变成『(?!<.)(-?\d+|\d*)\.\d+(?!.)』。

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

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

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

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

效率

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

观察匹配过程就会发现,如果字符串很长,而引号字符串又出现在比较靠前的位置,比如"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]."<br />"; 
    $matched = preg_match($regex, $string, $oneMatch, PREG_OFFSET_CAPTURE, $lastOffset); 
} 

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

代码(以Python为例)

for match in re.finditer("\\d+", "123 45 6") 
print match.group(0) 

验证

另一类“匹配”是数据验证,也就是“检查字符串能否完全由正则表达式匹配”,它主要用来测试和保证数据的合法性。比如有些网站要求你设定密码,密码只能由数字或小写字母构成,长度在6到12个字符之间,如果输入的密码不符合条件,则会提示你修改,这个任务,一般使用JavaScript的正则表达式来完成。

初看起来,这也是用正则表达式在字符串中查找匹配文本。但仔细想想,两者又不一样:一般来说,提取时正则表达式匹配的开始/结束位置都是不确定的,需要逐次试错,才能决定;验证时,同样需要考虑准确性,但效率并不是重点考虑的因素(一把验证的文本是用户名、手机号、密码之类,不会太长),虽然也要求准确性,但匹配的开始/结束位置都是确定的,只要从文本的开头验证即可,不用反复推进-尝试;而且只要发现任何一个“硬性”条件无法满足(比如长度、锚点),即可失败退出。

正因为验证操作有这些特点,有些语言中提供了专门的方法进行正则表达式验证。如果没有,我们也可以使用简单的查找功能,只是在正则表达式的首尾加上匹配字符串起始/结束位置的锚点来定位,这样既保证表达式匹配的是整个字符串,也可以在无法匹配时尽早判断失败退出。

常见语言中的验证方法

语言

验证方法

备注

Java

String.matches(regex)

专用于验证,返回boolean值,不需要『^』和『$』

PHP

preg_match(regex, string) != 0

preg_match返回匹配成功的次数,需要『^』和『$』

.NET

Regex.IsMatch(string, regex)

专用于验证,返回boolean值,不需要『^』和『$』

Python

re.search(regex, string) != None

成功则返回True,否则返回False,需要『^』和『$』

 

re.match(regex, string) != None

成功则返回True,否则返回False,需要『$』

Ruby

Regexp.match(text) != nil

Regexp.match(text)返回匹配成功的起始位置,若无法匹配则返回nil,需要『^』和『$』

JavaScript

Regexp.test(string)

专用于验证,返回boolean值,需要『^』和『$』

 

前面说过,在验证时,文本的开始/结束位置是预先知道的,所以验证的表达式编写起来更加简单。比如之前匹配浮点数的表达式,我们首先得到的是『(-?\d+|\d*)\.\d+』,在进行数据提取时,需要在两端加上环视,防止错误匹配其它字符;但是如果是验证浮点数,就不需要考虑两端的环视,应该/不应该出现什么字符,直接在首尾加上『^』和『$』即可,所以验证用的表达式是『^(-?\d+|\d*)\.\d+$』。

我们甚至可以简单将各个条件叠加起来,直接得到最后的表达式,比如下面这个例子:

需要验证密码字符串,前期的分析总结出5条明确的规则:

  1. 密码的长度在6-12个字符之间
  2. 只能由小写字母、阿拉伯数字、横线组成
  3. 开头和结尾不能是横线
  4. 不能全部是数字
  5. 不容许有连续(2个及以上)的横线

下面依次列出对应5条规则的表达式:

  1. 密码长度在6-12个字符之间:其形式类似『.{6, 12}』
  2. 只能由小写字母、阿拉伯数字、横线组成:所有的字符都只能由『[0-9A-Za-z-]』匹配
  3. 开头和结尾不能是横线:开头『^(?!-)』,结尾『(?<!-)$』
  4. 不能全部是数字,也就是说必须出现一个『[^0-9]』或者『\D』
  5. 不容许有连续(2个及以上)的横线,也就是说不能出现『--』

如果用来提取数据,就必须把这5条规则糅合到一起。前3条规则比较好办,可以合并为『^(?!-)[0-9A-Za-z-]{6,12}(?<!-)$』,但它与第4和第5个条件合并都不简单。

与第4条规则合并的难点在于,我们无法确定这个『[^0-9]』出现的位置,如果简单改为『^(?!-)[0-9A-Za-z-]{6,12}[^0-9][0-9A-Za-z-]{6,12}(?<!-)$』,看似正确,却无法保证整个字符串的长度在6-12之间——目前这个表达式的长度在13(6+1+6)到25(12+1+12)之间。这显然有问题,但照这个方式也确实无法保证整个字符串的长度,因为我们无法跨越『[^0-9]』,为两端『[0-9A-Za-z-]』的量词建立关联,让它们的和为5-11之间。同样,与第5条规则的合并也存在这类问题,因为我们无法确认『--』的出现位置。

看起来,把这5条规则糅合成一个正则表达式,找到能够匹配的文本,真不是件容易的事情。不过,如果我们要做的只是验证,不妨换个思路:我们要匹配的并不是所有的文本,而是文本的开始位置,它后面的文本满足5个条件,而每个条件都可以不用实际匹配任何文本,而用环视来满足。

对应5条规则的环视表达式依次是:

  1. 密码长度在6-12个字符之间:『^(?=.{6, 12}$)』
  2. 只能由小写字母、阿拉伯数字、横线组成:『^(?=[0-9A-Za-z-]*$)』
  3. 开头和结尾不能是横线:『^(?!-).*(?<!-)$』
  4. 不能全部是数字:『^(?=.*[^0-9])』(这里不需要出现$,只要出现了非数字字符就可以)
  5. 不容许有连续(2个及以上)的横线:『^(?!.*--)』

下面就是寻找这样一个文本起始位置,它后面的文本同时满足这5个条件。实际上,因为锚点并不真正匹配文本,所以多个锚点可以重叠在一起,因此我们完全可以寻找5个锚点,把它们串联起来:

『(^(?=.{6, 12}$))(^(?=[0-9A-Za-z-]*$))(^((?!-).*(?<!-)$))(^(?=.*[^0-9])(^(?!.*--))』

意思就是:先寻找这样一个字符串起始位置,它之后的字符串满足条件1;然后寻找这样一个字符串其实位置,它之后的字符串满足条件2;…… 如果能找到5个这样的字符串起始位置(实际上,因为只有一个字符串起始位置,所以这5个位置是重叠的),就算验证成功。

其实我们也可以不用那么多的括号,只用一个『^』即可:

『^(?=.{6, 12}$)(?=[0-9A-Za-z-]*$)(?=(?!-).*(?<!-)$)(?=.*[^0-9])(?!.*--)』 

总结

虽然“匹配”是正则表达式的常见操作,但细分起来,“匹配”又可分为提取和验证两种操作。

提取时需要照顾准确性和效率,因为此时字符串的起始/结束位置是不确定的,应当添加适当的环视结构,避免匹配了不期望的数据。

验证时对效率的要求并不高,因为验证的字符串一般都很短,而且验证的起始/结束位置都是确定的,直接在字符串两端添加^和$即可。而且验证有时候要比提取简单得多,我们可以改换思路,改“查找文本”为“查找位置”,针对验证时容许/不容许出现的每一个条件,写出对应的环视功能,作为一个将它们并列在一起。

 

原文地址:http://www.infoq.com/cn/articles/regular-expressions-5-match

分享到:
评论

相关推荐

    正则表达式(一)从初学到精通正则表达式

    本文将对正则表达式的基础知识进行详细的介绍,从什么是正则表达式开始,逐步深入浅出地讲解正则表达式的基本概念、正则表达式引擎、文字符号、特殊字符、不可显示字符、正则表达式引擎的内部工作机制等。...

    浅谈正则表达式 实例入门

    正则表达式是编程语言中用于模式匹配和文本操作的强大工具,尤其在JavaScript中,它提供了丰富的功能来处理字符串。本文将深入浅出地探讨JavaScript中的正则表达式及其基本用法。 首先,创建JavaScript正则表达式有...

    浅谈PHP+正则表达式格式

    本文将浅谈PHP中的正则表达式格式及其应用。 PHP支持多种正则表达式函数,如`ereg()`、`ereg_replace()`、`eregi_replace()`和`split()`等。这些函数均接受正则表达式作为其第一个参数,帮助开发者执行匹配、替换和...

    浅谈正则表达式实例入门共9页.pdf.zip

    这篇文档“浅谈正则表达式实例入门共9页.pdf”似乎是一个初学者的教程,旨在帮助读者快速理解和应用正则表达式。 在正则表达式中,有几种基本的元字符和操作符,它们构成了各种可能的匹配模式: 1. **元字符**:如...

    浅谈Javascript常用正则表达式应用

    在探讨Javascript中的正则表达式应用时,我们主要关注如何利用正则表达式完成模式匹配、搜索、替换等常见的字符串操作。正则表达式在Javascript中是一种强大的文本处理工具,允许开发者定义搜索模式,匹配字符串中的...

    浅谈php正则表达式中的非贪婪模式匹配的使用

    在正则表达式中,模式匹配可以分为两种类型:贪婪模式和非贪婪模式。本篇文章将重点讲解在PHP中如何使用非贪婪模式来匹配字符串。 正则表达式中的非贪婪模式也称为懒惰模式,它尽可能少地匹配字符,即当模式中包含...

    浅谈js正则字面量//与new RegExp的执行效率

    在JavaScript编程中,正则表达式是一种强大的文本匹配工具,可以通过两种不同的方式声明: 1. 正则字面量:这是最常见的方式,直接在代码中使用正则表达式,例如:`var reg = /pattern/;`。 2. RegExp构造函数:...

    浅谈正则表达式中的分组和引用实现方法

    在正则表达式中,分组和引用是两个重要的功能,它们允许我们在文本中查找重复出现的模式,并且在后续的匹配中引用前面已经匹配过的模式。 分组是由一对圆括号"()"构成的,用于将正则表达式中的一部分模式组合成一个...

    浅谈关于Java正则和转义中\\和\\\\的理解

    Java正则表达式是Java中的一种模式匹配机制,用于字符串的模式匹配和校验。Java正则表达式采用了Perl5正则表达式语法,提供了强大的字符串匹配和处理能力。Java正则表达式可以用于字符串的验证、提取、替换等操作。 ...

    浅谈正则速记法的技巧

    正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。 正则表达式速记技巧: 1. 元字符和...

    浅谈javascript中replace()方法

    该方法可以接受两种类型的参数:正则表达式(regexp)和普通字符串(substr)。当使用正则表达式作为参数时,replace()方法会查找所有与之匹配的部分,并可以用指定的字符串或函数进行替换;当使用普通字符串作为...

    浅谈java中replace()和replaceAll()的区别

    这两种方式都可以将 `/` 替换为 `\`,但因为 `replace()` 不涉及正则表达式,所以使用 `replace()` 可能更直观。 总的来说,`replace()` 更适合简单的字符或字符串替换,而 `replaceAll()` 则适用于复杂的、基于...

    浅谈js中startsWith 函数不能在任何浏览器兼容的问题

    这两种正则表达式的方法也能达到同样的效果,但需要注意的是,虽然正则表达式在大多数情况下都非常强大,但它们可能会在性能上略逊于简单的字符串操作,特别是在处理大量数据时。 为了确保兼容性,这些重写应该在...

    PL_SQL的Web应用浅谈.pdf

    在Oracle的Web服务器环境下,PL/SQL过程可以通过HTP和HTF包生成HTML输出,并且还有OWA-UTIL等实用程序包来提供额外功能,如优化锁策略、正则表达式匹配、文本处理、图像操纵以及cookie管理等。 在实际应用中,PL/...

    浅谈javascript中自定义模版

    它通过正则表达式匹配和替换操作来处理模板内容,将模板内的控制指令替换为相应的JavaScript语句。这里的关键在于利用正则表达式和字符串操作技巧,将模板语法转换为JavaScript能执行的代码片段。当模板引擎执行后,...

    浅谈Python中的字符串

    【Python字符串基础】 在Python编程语言中,字符串是一种基本且重要的数据类型,广泛应用于文本处理...对于更深入的字符串操作,如正则表达式匹配、字符串编码解码等,可以查阅Python官方文档或相关教程获取更多信息。

    浅谈Python 敏感词过滤的实现

    本文将深入探讨两种Python敏感词过滤的实现方式:NaiveFilter和BSFilter。 首先,我们来看NaiveFilter类。这个简单的实现主要依赖于Python的基本字符串操作。`__init__`方法初始化一个空的关键词集合。`parse`方法...

Global site tag (gtag.js) - Google Analytics