`
xf986321
  • 浏览: 163863 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

正则表达式匹配文本

阅读更多

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

提取

提取可以理解为“用正则表达式遍历整个字符串,找出能够匹配的文本”,它主要用来提取需要的数据,常见的任务有:找出文本中的电子邮件地址,找出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])(?!.*--)』 

总结

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

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

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

关于作者

余晟,程序员,曾任抓虾网高级顾问,现就职于盛大创新院,感兴趣的方向包括搜索和分布式算法等。翻译爱好者,译有《精通正则表达式》(第三版)和《技术领导之路》,目前正在写作《正则表达式傻瓜书》(暂定名),希望为国内开发同行贡献一本实用的正则表达式教程。


分享到:
评论

相关推荐

    java正则表达式匹配工具

    Java正则表达式匹配工具是IT领域中一种强大的文本处理工具,它利用正则表达式(Regular Expression)的规则来查找、替换或者提取文本中的特定模式。正则表达式是一种特殊的字符序列,能够帮助程序员或者用户高效地...

    易语言正则表达式匹配中文

    本文将深入探讨易语言中的正则表达式匹配中文的原理、方法以及应用。 正则表达式(Regular Expression)是一种模式匹配的语言,用于描述一种字符串的集合。在易语言中,我们可以通过内置的字符串函数来实现正则...

    正则表达式匹配调试工具

    为了更好地理解和调试正则表达式,开发者通常会使用专门的正则表达式匹配调试工具。 正则表达式匹配调试工具的功能通常包括以下几点: 1. **实时匹配测试**:用户可以输入正则表达式和待匹配的文本,工具会即时...

    易语言正则表达式类匹配中文

    6. **取子匹配文本**:除了整个匹配外,正则表达式可能还包含捕获组,这些是用括号定义的子模式。`取子匹配文本`可以获取这些子模式的文本,这对于提取特定部分的文本非常有用。 7. **取子匹配数量**:这个方法返回...

    正则表达式匹配算法

    这些基础语法是构建正则表达式匹配算法的基础。 接下来,我们可以采用DFA(确定有限状态自动机)或NFA(非确定有限状态自动机)来实现正则表达式的匹配。在这个案例中,C++代码可能是基于NFA实现的,因为NFA通常更...

    易语言正则表达式文本替换

    - `子程序_正则匹配数量`:计算文本中符合正则表达式的匹配次数。 3. **易语言中的正则表达式替换实例** - 基本替换:如将所有数字替换为星号,可以使用模式`\d`和替换字符串`'*'`。 - 分组替换:如果需要保留...

    JavaScript正则表达式匹配 div style标签

    在进行正则表达式匹配时,需要构建能够正确描述目标模式的正则表达式。对于`&lt;div&gt;`和`&lt;style&gt;`标签,通常需要考虑的是标签的开始`&lt;div&gt;`、结束`&lt;/div&gt;`,以及它们之间的内容。对于`&lt;style&gt;`标签,需要识别它的开始`...

    正则表达式匹配小工具源码

    本项目提供了一个正则表达式匹配的小工具源码,旨在帮助开发者和学习者快速验证正则表达式的正确性。 该工具的核心功能是验证正则表达式,确保其能正确匹配目标字符串。在开发过程中,正则表达式的调试往往是一项...

    易语言正则表达式取网址和名称

    局部变量 匹配结果, 结构体(正则表达式匹配) .局部变量 目标文本, 文本型 = "这里是包含网址的文本,比如 http://example.com 和 www.example.org" .局部变量 网址, 文本型 .程序段 .创建对象 正则表达式对象, ...

    基于正则表达式的文本抽取软件

    【基于正则表达式的文本抽取软件】是一款由个人开发者用Java编程语言编写的软件,主要功能是利用正则表达式的强大匹配能力,帮助语言工作者、外语教师和学生进行文本的高效处理和信息提取。在现代信息化社会,面对...

    Java使用正则表达式提取XML节点内容的方法示例

    Java使用正则表达式提取XML节点内容的方法示例主要介绍了Java使用正则表达式提取XML节点内容的方法,结合具体实例形式分析了java针对xml格式字符串的正则匹配相关操作技巧。 一、正则表达式简介 正则表达式是指一...

    正则表达式匹配/可以自定义正则表达式

    正则表达式(Regular Expression,简称regex)是一种强大的文本处理工具,它用于匹配字符串...在压缩包文件"正则表达式匹配"中,你可能找到更多关于正则表达式实际应用的实例和教程,这将有助于你加深理解和提升技能。

    正则表达式匹配算法小结

    ### 正则表达式匹配算法概述 正则表达式(Regular Expression, 简称 Regex 或 RE)是一种用于描述字符序列的模式语言,被广泛应用于文本处理、数据验证以及搜索等场景。正则表达式的匹配算法是实现这些功能的基础,...

    基于FPGA的正则表达式匹配引擎设计.pdf

    正则表达式匹配引擎是一种用于模式匹配的工具,它在网络入侵检测、文本搜索和数据过滤等领域中广泛应用。正则表达式允许复杂、灵活的模式定义,因此它在处理大量数据时,尤其是在网络环境中,能够快速识别出符合特定...

    PB实现的正则表达式

    在IT领域,正则表达式(Regular Expression,简称regex)是一种强大的文本处理工具,它能够进行复杂的模式匹配、查找、替换等操作。在本话题中,我们将探讨如何使用PowerBuilder 11.5这一经典的开发环境来实现正则...

    正则表达式转换工具

    正则表达式(Regular Expression,简称regex)是一种强大的文本处理工具,它用于匹配、查找、替换等操作,涉及字符串处理的各个领域。正则表达式转换工具是专门针对这一需求而设计的,它能帮助用户将输入的内容转换...

    vb正则表达式实例(正则表达式测试程序)

    在VB.NET中,正则表达式(Regular Expression)是一种强大的文本处理工具,它允许程序员通过模式匹配来处理字符串。这个“vb正则表达式实例”很可能是为了帮助开发者测试和理解正则表达式的工作原理而设计的一个应用...

    正则表达式在数据库查询中的应用

    1. **模糊查询**:通过正则表达式匹配相似的文本,如查询包含特定单词或短语的记录。 ```sql SELECT * FROM articles WHERE content REGEXP '.*example.*'; ``` 2. **格式验证**:确保输入的数据符合预期的格式,...

    正则表达式匹配(自动转换)

    "正则表达式匹配(自动转换)"指的是通过特定的软件或编程库,将正则表达式应用到文本中,自动进行匹配操作,无需用户手动处理。 在描述中提到的"无需再为正则表达式而烦恼",意味着这样的工具或功能提供了便捷的...

Global site tag (gtag.js) - Google Analytics