在单个正则expression式中折叠并捕获重复模式

我经常遇到需要从string中获取许多令牌的情况,经过无数次的尝试,我找不到简化过程的方法。

所以我们假设文字是:

启动:testing – testing – LOREM-存有-SIR-doloret – 等 – 等 – 的东西:结束

这个例子里面有8个项目,但是说可能有3到10个项目。

我理想的喜欢这样的事情:
start:(?:(\w+)-?){3,10}:end很好,干净,但它只捕获最后一场比赛。 看这里

我通常在简单的情况下使用这样的东西:

 start:(\w+)-(\w+)-(\w+)-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?:end 

3组是强制性的,另外7组是可选的,因为最大限制为10,但是这看起来并不“好”,如果最大限制为100并且比赛更复杂,那么写和跟踪将是一个痛苦。 演示

我能做到的最好的:

 start:(\w+)-((?1))-((?1))-?((?1))?-?((?1))?-?((?1))?-?((?1))?-?((?1))?:end 

特别是如果比赛复杂但仍然很长的话更短。 演示

任何人都设法使它作为1正则expression式解决scheme没有编程

我最感兴趣的是如何能够在PCRE中完成,但其他口味也可以。

更新:

目的是通过RegEx单独validation匹配并捕获match 0的单个令牌,而没有任何操作系统/软件/编程语言限制

更新2(赏金):

在@ nhahtdh的帮助下,我通过\G获得了RegExp:

 (?:start:(?=(?:[\w]+(?:-|(?=:end))){3,10}:end)|(?!^)\G-)([\w]+) 

演示甚至更短,但可以描述不重复的代码

我也对ECMA风格感兴趣,因为它不支持\G想知道是否有另一种方法,特别是不使用/g修饰符。

先阅读这个!

这篇文章是为了展示可能性,而不是赞同“一切正则expression式”的方法来解决问题。 作者已经写出了3-4个变体,每个变体都有一些微妙的缺陷,在到达目前的解决scheme之前就很难检测到。

对于您的具体示例,还有其他更好的解决scheme,比如沿着分隔符匹配和拆分匹配。

这篇文章处理你的具体例子。 我真的怀疑完全泛化是可能的,但背后的想法是可重用的类似的情况。

概要

  • .NET支持使用CaptureCollection类捕获重复模式。
  • 对于支持\G和后视的语言,我们可以构build一个与全局匹配函数一起工作的正则expression式。 把它写得完全正确并且容易编写一个巧妙的正则expression式并不容易。
  • 对于没有\G和后援支持的语言:可以用^来模拟\G ,通过在单个匹配之后敲击inputstring。 (不包括在这个答案)。

此解决scheme假定正则expression式引擎支持\G匹配边界,预读(?=pattern)和后退(?<=pattern) 。 Java,Perl,PCRE,.NET,Ruby正则expression式支持上述所有高级特性。

不过,你可以在.NET中使用你的正则expression式。 由于.NET支持捕获所有与CaptureCollection类重复的捕获组相匹配的实例。

对于你的情况,它可以在一个正则expression式中完成,使用\G匹配边界,并预先约束重复次数:

 (?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end) 

DEMO 。 结构是\w+-重复,然后\w+:end

 (?:start:(?=\w+(?:-\w+){2,9}:end)|(?!^)\G-)(\w+) 

DEMO 。 第一项是\w+ ,然后-\w+重复。 (感谢kaᵠ的build议)。 这种结构更容易推断其正确性,因为这种结构更less。

\G匹配边界在您需要进行标记化时特别有用,您需要确保引擎不会跳过,并且匹配本应该是无效的东西。

说明

让我们分解正则expression式:

 (?: start:(?=\w+(?:-\w+){2,9}:end) | (?<=-)\G ) (\w+) (?:-|:end) 

要识别的最简单的部分是(\w+)在最后一行,这是你想要捕捉的单词。

最后一行也很容易识别:要匹配的单词可以跟随-:end

我允许正则expression式自由地匹配string中的任何地方 。 换句话说, start:...:end可以出现在string的任何地方,并且可以多次; 正则expression式将简单地匹配所有的单词。 您只需要处理返回的数组,以便将匹配的令牌实际来自哪里。

至于说明,正则expression式的开始检查是否存在stringstart: :,以及下列先行检查字数是否在指定的限制内,并以:end或者,或者我们检查前一场比赛之前的angular色是- ,然后继续前一场比赛。

对于其他build筑:

 (?: start:(?=\w+(?:-\w+){2,9}:end) | (?!^)\G- ) (\w+) 

一切都差不多,除了匹配表格的重复之前匹配start:\w+ first -\w+ 。 与第一个结构相反,我们匹配start:\w+- first,以及\w+- (或\w+:end为最后一次重复)的重复实例。

使这个正则expression式在string中间匹配是相当棘手的:

  • 我们需要检查start::end之间的词数(作为原始正则expression式的一部分)。

  • \G匹配string的开头也! (?!^)来防止这种行为。 如果没有考虑到这一点,正则expression式可能会在没有任何start:时产生匹配start:

    对于第一个构造,后面(?<=-)已经阻止了这种情况( (?!^)(?<=-) )隐含。

  • 对于第一个结构(?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end) ,我们需要确保在:end之后我们不会匹配任何有趣的东西。 look-behind就是为了这个目的:它防止在匹配:end后发生垃圾。

    第二个构造不会遇到这个问题,因为在匹配所有的记号之后,我们会陷入: (of :end )。

validation版本

如果你想要validationinput的string是否符合格式(在前面和后面没有额外的东西), 提取数据,你可以像这样添加锚点:

 (?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G-)(\w+) (?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G)(\w+)(?:-|:end) 

(Look-behind也是不需要的,但是我们仍然需要(?!^)来防止\G匹配string的开头)。

施工

对于所有想要捕获所有重复实例的问题,我不认为存在修改正则expression式的一般方法。 要转换的“难”(或不可能)情况的一个例子是当重复必须回溯一个或多个循环以满足某些条件匹配时。

当原始正则expression式描述整个inputstring(validationtypes)时,通常比起试图从string中间匹配的正则expression式(匹配types)更容易进行转换。 但是,您始终可以与原始正则expression式匹配,并将匹配types问题转换回validationtypes问题。

我们通过以下步骤来构build这样的正则expression式:

  • 编写覆盖重复之前的部分的正则expression式(例如start: :)。 让我们称这个前缀正则expression式
  • 匹配并捕获第一个实例。 (例如(\w+)
    (此时,第一个实例和分隔符应该已经匹配)
  • 添加\G作为替代。 通常也需要防止它匹配string的开始。
  • 添加分隔符(如果有的话)。 (例如-
    (在这一步之后,其余的代币也应该被匹配,除了最后的代码)
  • 在重复之后添加覆盖零件的部分(如有必要)(例如:end )。 让我们在重复后缀正则expression式之后调用这个部分(不pipe我们把它添加到构造中是没有关系的)。
  • 现在困难的部分。 你需要检查:
    • 除了前缀正则expression式之外,没有其他方法可以开始匹配。 注意\G分支。
    • 后缀正则expression式匹配没有办法开始任何匹配。 记下\G分支是如何开始比赛的。
    • 对于第一个构造,如果在后缀中混合后缀正则expression式(例如:end )和分隔符(例如- ),请确保最终不允许使用后缀正则expression式作为分隔符。

虽然理论上可以编写单个expression式,但是首先匹配外部边界然后在内部部分执行分割会更加实际。

在ECMAScript中,我会这样写:

 'start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end' .match(/^start:([\w-]+):end$/)[1] // match the inner part .split('-') // split inner part (this could be a split regex as well) 

在PHP中:

 $txt = 'start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end'; if (preg_match('/^start:([\w-]+):end$/', $txt, $matches)) { print_r(explode('-', $matches[1])); } 

当然,你可以在这个引用的string中使用正则expression式。

 "(?<a>\\w+)-(?<b>\\w+)-(?:(?<c>\\w+)" \ "(?:-(?<d>\\w+)(?:-(?<e>\\w+)(?:-(?<f>\\w+)" \ "(?:-(?<g>\\w+)(?:-(?<h>\\w+)(?:-(?<i>\\w+)" \ "(?:-(?<j>\\w+))?" \ ")?)?)?" \ ")?)?)?" \ ")" 

这是一个好主意吗? 不,我不这么认为。

不知道你可以这样做,但你可以使用全局标志来查找冒号之间的所有单词,请参阅:

http://regex101.com/r/gK0lX1

你不得不自己validation组的数量。 如果没有全局标志,你只会得到一个匹配,而不是所有的匹配 – 改变{3,10}{1,5}而你得到的结果'先生'。

 import re s = "start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end" print re.findall(r"(\b\w+?\b)(?:-|:end)", s) 

产生

['test', 'test', 'lorem', 'ipsum', 'sir', 'doloret', 'etc', 'etc', 'something']

当你结合:

  1. 您的观察结果:任何一种重复的单个捕获组都将导致覆盖最后一次捕获,从而只返回捕获组的最后一次捕获。
  2. 知识:基于部件而不是整体捕获的任何一种捕获使得不可能对正则expression式引擎的重复次数设置限制。 限制将不得不是元数据(而不是正则expression式)。
  3. 有一个要求,答案不能涉及编程(循环),也不涉及简单地复制粘贴捕获组的问题。

可以推断,这是不能做到的。

更新:有一些正则expression式引擎的p。 1不一定是真的。 在这种情况下,你指定的正则expression式start:(?:(\w+)-?){3,10}:end将完成这个工作。