在正则表达式中,匹配是最最基本的操作。使用正则表达式,换种说法就是“用正则表达式去匹配文本”。但这只是广义的“匹配”,细说起来,广义的“匹配”又可以分为两类:提取和验证。所以,本篇文章就来专门讲讲提取和验证。
提取
提取可以理解为“用正则表达式遍历整个字符串,找出能够匹配的文本”,它主要用来提取需要的数据,常见的任务有:找出文本中的电子邮件地址,找出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条明确的规则:
- 密码的长度在6-12个字符之间
- 只能由小写字母、阿拉伯数字、横线组成
- 开头和结尾不能是横线
- 不能全部是数字
- 不容许有连续(2个及以上)的横线
下面依次列出对应5条规则的表达式:
- 密码长度在6-12个字符之间:其形式类似『.{6, 12}』
- 只能由小写字母、阿拉伯数字、横线组成:所有的字符都只能由『[0-9A-Za-z-]』匹配
- 开头和结尾不能是横线:开头『^(?!-)』,结尾『(?<!-)$』
- 不能全部是数字,也就是说必须出现一个『[^0-9]』或者『\D』
- 不容许有连续(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条规则的环视表达式依次是:
- 密码长度在6-12个字符之间:『^(?=.{6, 12}$)』
- 只能由小写字母、阿拉伯数字、横线组成:『^(?=[0-9A-Za-z-]*$)』
- 开头和结尾不能是横线:『^(?!-).*(?<!-)$』
- 不能全部是数字:『^(?=.*[^0-9])』(这里不需要出现$,只要出现了非数字字符就可以)
- 不容许有连续(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支持多种正则表达式函数,如`ereg()`、`ereg_replace()`、`eregi_replace()`和`split()`等。这些函数均接受正则表达式作为其第一个参数,帮助开发者执行匹配、替换和...
这篇文档“浅谈正则表达式实例入门共9页.pdf”似乎是一个初学者的教程,旨在帮助读者快速理解和应用正则表达式。 在正则表达式中,有几种基本的元字符和操作符,它们构成了各种可能的匹配模式: 1. **元字符**:如...
在探讨Javascript中的正则表达式应用时,我们主要关注如何利用正则表达式完成模式匹配、搜索、替换等常见的字符串操作。正则表达式在Javascript中是一种强大的文本处理工具,允许开发者定义搜索模式,匹配字符串中的...
在正则表达式中,模式匹配可以分为两种类型:贪婪模式和非贪婪模式。本篇文章将重点讲解在PHP中如何使用非贪婪模式来匹配字符串。 正则表达式中的非贪婪模式也称为懒惰模式,它尽可能少地匹配字符,即当模式中包含...
在JavaScript编程中,正则表达式是一种强大的文本匹配工具,可以通过两种不同的方式声明: 1. 正则字面量:这是最常见的方式,直接在代码中使用正则表达式,例如:`var reg = /pattern/;`。 2. RegExp构造函数:...
在正则表达式中,分组和引用是两个重要的功能,它们允许我们在文本中查找重复出现的模式,并且在后续的匹配中引用前面已经匹配过的模式。 分组是由一对圆括号"()"构成的,用于将正则表达式中的一部分模式组合成一个...
Java正则表达式是Java中的一种模式匹配机制,用于字符串的模式匹配和校验。Java正则表达式采用了Perl5正则表达式语法,提供了强大的字符串匹配和处理能力。Java正则表达式可以用于字符串的验证、提取、替换等操作。 ...
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。 正则表达式速记技巧: 1. 元字符和...
该方法可以接受两种类型的参数:正则表达式(regexp)和普通字符串(substr)。当使用正则表达式作为参数时,replace()方法会查找所有与之匹配的部分,并可以用指定的字符串或函数进行替换;当使用普通字符串作为...
这两种方式都可以将 `/` 替换为 `\`,但因为 `replace()` 不涉及正则表达式,所以使用 `replace()` 可能更直观。 总的来说,`replace()` 更适合简单的字符或字符串替换,而 `replaceAll()` 则适用于复杂的、基于...
这两种正则表达式的方法也能达到同样的效果,但需要注意的是,虽然正则表达式在大多数情况下都非常强大,但它们可能会在性能上略逊于简单的字符串操作,特别是在处理大量数据时。 为了确保兼容性,这些重写应该在...
在Oracle的Web服务器环境下,PL/SQL过程可以通过HTP和HTF包生成HTML输出,并且还有OWA-UTIL等实用程序包来提供额外功能,如优化锁策略、正则表达式匹配、文本处理、图像操纵以及cookie管理等。 在实际应用中,PL/...
它通过正则表达式匹配和替换操作来处理模板内容,将模板内的控制指令替换为相应的JavaScript语句。这里的关键在于利用正则表达式和字符串操作技巧,将模板语法转换为JavaScript能执行的代码片段。当模板引擎执行后,...
【Python字符串基础】 在Python编程语言中,字符串是一种基本且重要的数据类型,广泛应用于文本处理...对于更深入的字符串操作,如正则表达式匹配、字符串编码解码等,可以查阅Python官方文档或相关教程获取更多信息。
本文将深入探讨两种Python敏感词过滤的实现方式:NaiveFilter和BSFilter。 首先,我们来看NaiveFilter类。这个简单的实现主要依赖于Python的基本字符串操作。`__init__`方法初始化一个空的关键词集合。`parse`方法...