折叠并捕获单个正则表达式中的重复模式

时间:2013-03-07 10:18:40

标签: regex language-agnostic

我一直在遇到需要从字符串中捕获大量令牌的情况,经过无数次尝试后,我找不到简化过程的方法。

所以让我们说文字是:

  

开始:测试 - 测试 - LOREM-ipsum的-SIR-doloret-ETC-等出头:端

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

我最喜欢这样的事情:
start:(?:(\w+)-?){3,10}:end漂亮而干净但它只捕获最后一场比赛。 see here

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

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

3组强制性,另外7组是可选的,因为最大限制为10,但这看起来并不“好”,如果最大限制为100且匹配更复杂,写入和跟踪将是一件痛苦的事。 demo

到目前为止我能做的最好:

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

更短,特别是如果比赛很复杂但仍然很长。 demo

任何人都设法让它作为1个正则表达式的解决方案无需编程

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

更新

目的是仅通过RegEx验证匹配并捕获match 0内的单个令牌,而无需任何操作系统/软件/编程语言限制

更新2(赏金):

在@nhahtdh的帮助下,我使用\G进入了下面的RegExp:

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

demo甚至更短,但可以在不重复代码的情况下进行描述

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

5 个答案:

答案 0 :(得分:35)

首先阅读!

这篇文章是为了展示可能性,而不是支持"一切正则表达式"解决问题的方法。在达到当前解决方案之前,作者已经编写了3-4种变体,每种变体都有一些难以检测的细微错误。

对于您的具体示例,还有其他更易于维护的解决方案,例如在分隔符上匹配和拆分匹配项。

这篇文章介绍了您的具体示例。我真的怀疑是否有可能完全概括,但背后的想法可以用于类似案例。

摘要

  • .NET支持使用CaptureCollection类捕获重复模式。
  • 对于支持\G和后视的语言,我们可以构建一个与全局匹配函数一起使用的正则表达式。写它完全正确并且容易编写一个巧妙的错误正则表达式并不容易。
  • 对于没有\G和后备支持的语言:通过在单个匹配后选择输入字符串,可以使用\G模拟^。 (这个答案没有涉及)。

解决方案

此解决方案假设正则表达式引擎支持\G匹配边界,前瞻(?=pattern)和后瞻(?<=pattern)。 Java,Perl,PCRE,.NET,Ruby正则表达式支持上述所有这些高级功能。

但是,您可以使用.NET中的正则表达式。由于.NET支持捕获由通过CaptureCollection类重复的捕获组匹配的所有实例。

对于你的情况,它可以在一个正则表达式中完成,使用\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ᵠ的建议)。这种结构更容易推断其正确性,因为交替较少。

当您需要进行标记化时,

\G匹配边界特别有用,您需要确保引擎不会向前跳过并匹配应该无效的内容。

说明

让我们打破正则表达式:

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

最容易识别的部分是前一行中的(\w+),这是您要捕获的单词。

最后一行也很容易识别:要匹配的字词后跟-:end

我允许正则表达式自由地开始匹配字符串中的任何位置。换句话说,start:...:end可以出现在字符串中的任何位置,并且可以出现任意次数;正则表达式将简单地匹配所有单词。您只需要处理返回的数组,以匹配匹配的标记实际来自的位置。

至于解释,正则表达式的开头检查是否存在字符串start:,并且以下前瞻检查单词的数量是否在指定的限制范围内,并以{{1}结束}。 要么是,要么我们检查上一场比赛前的字符是:end,并从上一场比赛继续。

对于其他建筑:

-

除了我们在匹配表单(?: start:(?=\w+(?:-\w+){2,9}:end) | (?!^)\G- ) (\w+) 的重复之前首先匹配start:\w+之外,所有内容几乎相同。与第一个构造相反,我们首先匹配-\w+,并重复start:\w+-(或\w+-重复的实例)。

使这个正则表达式在字符串的中间匹配是非常棘手的:

  • 我们需要检查\w+:endstart:之间的字数(作为原始正则表达式要求的一部分)。

  • :end也匹配字符串的开头!需要\G来防止此行为。如果不考虑这一点,正则表达式可能会在没有(?!^)的情况下产生匹配。

    对于第一个构造,后视start:已经阻止了这种情况((?<=-)暗示(?!^))。

  • 对于第一个构建(?<=-),我们需要确保在(?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end)之后我们不匹配任何有趣的内容。后视是为了这个目的:它可以防止:end之后的任何垃圾匹配。

    第二种结构不会遇到这个问题,因为在我们匹配之间的所有令牌之后,我们会卡在:end:)。

    < / LI>

验证版本

如果你想验证输入字符串是否遵循格式(前后没有额外的东西),提取数据,你可以添加锚点:

:end

(也不需要Look-behind,但我们仍然需要(?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G-)(\w+) (?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G)(\w+)(?:-|:end) 来阻止(?!^)匹配字符串的开头。)

建筑

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

当原始正则表达式描述整个输入字符串(验证类型)时,与尝试从字符串中间匹配的正则表达式(匹配类型)相比,它通常更容易转换。但是,您始终可以与原始正则表达式匹配,并将匹配类型问题转换回验证类型问题。

我们通过以下步骤构建这样的正则表达式:

  • 编写一个覆盖重复前部分的正则表达式(例如\G)。我们称之为前缀regex
  • 匹配并捕获第一个实例。 (例如start:
    (此时,第一个实例和分隔符应该匹配)
  • 添加(\w+)作为替换。通常还需要阻止它匹配字符串的开头。
  • 添加分隔符(如果有)。 (例如\G
    (在此步骤之后,其余的令牌也应该匹配,除了最后一个)
  • 重复后添加覆盖部件的部件(如有必要)(例如-)。让我们在重复后缀正则表达式之后调用该部分(我们是否将它添加到构造中并不重要)。
  • 现在很难。你需要检查:
    • 前缀regex 外,没有其他方法可以开始匹配。记下:end分支。
    • 后缀正则表达式匹配后,无法启动任何匹配。记下\G分支如何开始匹配​​。
    • 对于第一个构造,如果您将后缀正则表达式(例如\G)与分隔符(例如:end)混合在一起,请确保您最终不允许使用后缀正则表达式作为分隔符。

答案 1 :(得分:6)

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

在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]));
}

答案 2 :(得分:1)

当然你可以在这个带引号的字符串中使用正则表达式。

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

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

答案 3 :(得分:0)

不确定是否可以这样做,但您可以使用全局标志查找冒号之间的所有单词,请参阅:

http://regex101.com/r/gK0lX1

您必须自己验证群组数量。如果没有全局标记,您只会获得一次匹配,而不是所有匹配 - 将{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']

答案 4 :(得分:0)

结合使用时:

  1. 您的观察:对单个捕获组的任何类型的重复将导致覆盖最后一次捕获,从而仅返回捕获组的最后一次捕获。
  2. 知识:基于部件而不是整体的任何类型的捕获使得不可能对正则表达式引擎重复的次数设置限制。限制必须是元数据(不是正则表达式)。
  3. 要求答案不能涉及编程(循环),也不要像你在问题中那样简单地复制粘贴捕获组。
  4. 可以推断出它无法完成。

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