`
xf986321
  • 浏览: 160024 次
  • 性别: 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正则表达式

    如果你曾经用过Perl或任何其他内建正则表达式支持的语言,你一定知道用正则表达式处理文本和匹配模式是多么简单。如果你不熟悉这个术语,那么“正则表达式”(Regular Expression)就是一个字符构成的串,它定义了一...

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

    易语言正则表达式类匹配中文源码,正则表达式类匹配中文,创建,取正则文本,替换,取匹配数量,取匹配文本,取子匹配文本,取子匹配数量

    使用正则表达式的模式匹配

    JavaScript的RegExp类表示正则表达式,而String和RegExp都定义了使用正则表达式进行强大的模式匹配和文本检索与替换的函数。 ECMAScript v3对JavaScript正则表达式进行了标准化。JavaScript 1.2实现了ECMAScript v3...

    kettle 根据系统时间加正则表达式动态匹配获取多个文本文件

    kettle 根据系统时间加正则表达式动态匹配获取多个文本文件

    wps表格excel正则工具,excel正则表达式替换/匹配/查找/搜索/提取数字

    而本软件正是把强大的正则功能完美地添加到表格中,让 Excel、WPS 支持正则表达式的搜索、匹配提取、替换、定位等,让数据处理能力进化一个层次! 让 Office Excel、WPS 表格支持正则表达式的免费插件:「Excel ...

    《学习正则表达式》高清扫描版 PDF

    通过匹配特定单词、字符和模式,读者很快就可以自己动手使用正则表达式匹配、提取和转换文本。正则表达式是程序员必备的强大工具,得到了各种Unix实用程序,以及Perl、Java、JavaScript、C#等编程语言的支持。读完...

    Python程序设计:正则表达式检索与替换.pptx

    正则表达式是一个特殊的字符序列,它能方便的检查一个字符串是否与某种模式匹配,利用正则可以快速准确的对邮箱进行校验。 任务 邮箱验证 任务知识点 正则表达式概念与语法 常用匹配规则 正则表达式检索与替换 知识...

    正则表达式提取图片

    正则表达式提取图片,通过正则表达式提取段落中的问题,便于排版和美观

    正则表达式处理html文本例子

    正则表达式处理html文本例子,简单的正则表达式处理,仅供学习。

    正则表达式提取文本.bas

    excel自定义函数,在指定文本中使用正则表达式提取其中的符合文本并以指定分隔符分隔,如:文本:a1b2c3d4 正则:\d 分隔符:| 结果:1|2|3|4

    关于在LABVIEW中使用正则匹配公式.vi

    在labview中使用正则匹配模式很简单,难的就是使用正则表达式。很多人都搞不明白,查了很多资料还是不太明白。实际上就是一些类似通配符在作怪和其他语言也差不多。附件带上了,可以研究研究,构造正则表达式的方法...

    Delphi 10.4 最新版正则表达式(TRegExpr)源码

    正则表达式易于使用,功能强大,可用于复杂的搜索和替换以及基于模板的文本检查。这对于输入形式的用户输入验证特别有用-验证电子邮件地址等。您还可以从网页或文档中提取电话号码,邮政编码等,在日志文件中搜索...

    Python正则表达式标准库使用教程.pdf

    正则表达式的大致匹配过程是:依次拿出表达式和文本中的字符比较,如果每一个字符都能匹配,则匹配成功;一旦有匹配不成功的字符则匹配失败。如果表达式中有量词或边界,这个过程会稍微有一些不同,但也是很好理解...

    精通正则表达式~~~

    使用正则表达式匹配文本... 38 向更实用的程序前进... 40 成功匹配的副作用... 40 错综复杂的正则表达式... 43 暂停片刻... 49 使用正则表达式修改文本... 50 例子:公函生成程序... 50 举例:修整股票价格....

    [精通正则表达式(第三版)]

    使用正则表达式匹配文本 66 向更实用的程序前进 68 成功匹配的副作用 68 错综复杂的正则表达式 71 暂停片刻 77 使用正则表达式修改文本 78 例子:公函生成程序 78 举例:修整股票价格 79 自动的编辑

    精通正则表达式 中英文

    正则表达式已经成为众多语言及工具——Perl、PHP、Java、Python、Ruby、MysQL、VB.NET和c#(以及.NET Framework中的任何语言)——中的标准特性,依靠它,你能以之前完全不敢设想的方式进行复杂而精巧的文本处理。...

    JS正则表达式的使用以video标签为例

    使用JS正则表达式,选取video元素的src属性的值,利用jQuery选取元素

    正则表达式必知必会_正则表达式_

    正则表达式是一种威力无比强大的武器,几乎在所有的程序设计语言里和计算机平台上都可以用它来完成各种复杂的文本处理工作。本书从简单的文本匹配开始,循序渐进地介绍了很多复杂内容,其中包括回溯引用、条件性求值...

    正则表达式自动生成器 v2.0.0 专业版.zip

    一键导出匹配文本 一键导出提取字符组,并按自定义的分隔符进行分隔 保存项目以便重复测试和修改 在“设计元素”中学习并创建正则表达式 直接从正则表达式库中选择利用正则表达式 多语种 100%-200%大字体下...

    JAVA 正则表达式 教程

    正则表达式善于处理文本,对匹配、搜索和替换等操作都有意想不到的作用。正因如此,正则表达式现在是作为程序员七种基本技能之一*,因此学习和使用它在工作中都能达到很高的效率。 正则表达式应用于程序设计语言中,...

Global site tag (gtag.js) - Google Analytics