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

33

我经常遇到需要从字符串中捕获多个标记的情况,尝试了无数次之后,我找不到简化该过程的方法。

假设文本如下:

start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end

此示例包含8个项目,但可能有3到10个项目。

理想情况下,我希望像这样:
start:(?:(\w+)-?){3,10}:end 简洁明了,但它只捕获最后一个匹配项。 请参见此处

在简单情况下,我通常使用以下内容:

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

因为最多只能有10组,所以必须有3组必填,另外还有7组选填,但这看起来并不'好看',如果最大限制是100并且匹配更加复杂,编写和跟踪将会很麻烦。 演示

到目前为止,我做得最好的是:

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

如果匹配比较复杂但是依然很长,使用正则表达式的话可能会变得更加简短。 演示

有没有人能够实现一个只用 1 个正则表达式、不需要编程就能解决问题的方案?

我主要关心如何在 PCRE 中实现此功能,但其他版本也可以。

更新:

目的是通过仅使用正则表达式来验证匹配并捕获match 0内的单个标记,而没有任何操作系统/软件/编程语言限制。

更新 2(悬赏):

借助 @nhahtdh 的帮助,我使用了以下 RegExp,并使用了\G

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

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

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


正则表达式主要是用于识别固定模式,但您正在尝试使用它来识别动态模式。您没有说明您所使用的操作系统,但 Awk(Unix / Linux)或 Powershell(Windows)可能可以满足您的需求... - Robbie Dee
@RobbieDee:更新了帖子以澄清,寻找在复杂情况下智能使用正则表达式而不使用任何软件辅助的方法。 - CSᵠ
1
@kaᵠ,不,你不能在JS中使用单个匹配/步骤来执行一般性操作。唯一的方法是在.NET中(捕获重复组内容),或者使用支持\G(或类似API功能)的正则表达式风格。 - Qtax
5个回答

37

首先阅读!

本文旨在展示问题的可能性,而不是支持“一切正则表达式”方法。作者编写了3-4个变体,在达到当前解决方案之前,每个变体都有微妙的错误很难检测出来。

对于您的特定示例,有其他更可维护的更好的解决方案,例如匹配和沿分隔符拆分匹配。

本文涉及您的具体示例。我真的怀疑一个完全概括是可能的,但背后的思想对于类似情况是可重用的。

摘要

  • .NET支持使用CaptureCollection类捕获重复模式。
  • 对于支持\G和反向查找的语言,我们可以构建一个与全局匹配函数一起使用的正则表达式。编写完全正确且容易编写微妙有bug的正则表达式并不容易。
  • 对于不支持\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+重复。这个结构更容易理解它的正确性,因为它的选择较少。 \G匹配边界在需要进行标记化(tokenization)时特别有用,您需要确保引擎不会跳过并匹配应该无效的内容。
解释: 让我们拆分一下正则表达式:
(?:
  start:(?=\w+(?:-\w+){2,9}:end)
    |
  (?<=-)\G
)
(\w+)
(?:-|:end)

最容易识别的部分是在倒数第二行的(\w+),这是你想要捕获的单词。

最后一行也很容易识别:匹配的单词可能会跟着一个-或者:end

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

至于解释,正则表达式的开头检查是否存在字符串start:,接下来的前瞻检查单词数量是否在指定限制范围内,并以:end结尾。或者我们检查前一个匹配之前的字符是否为-,并从上一个匹配继续。

对于另一种情况:

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

除了我们首先匹配形式为start:\w+的重复之前,一切几乎相同。与第一个构造不同的是,在第一个构造中,我们首先匹配start:\w+-\w+-(或最后一个重复的\w+:end)。

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

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

  • \G也会匹配字符串的开头! (?!^)用于防止这种行为。 如果不注意这一点,则可能在没有start:时产生匹配。

    对于第一个构造,回溯(?<=-)已经防止了这种情况((?!^)(?<=-)暗示)。

  • 对于第一个构造(?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end),我们需要确保在:end之后不匹配任何奇怪的东西。 回溯是为了这个目的:它防止:end后面的任何垃圾匹配。

    第二个构造不会遇到这个问题,因为在匹配所有中间标记之后,我们将卡在::end的)处。

验证版本

如果您想验证输入字符串是否符合格式(前面和后面没有额外的内容),并提取数据,则可以添加锚点:

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

(预查不是必需的,但我们仍然需要(?!^)来防止\G匹配字符串的开头)。

构造

对于所有想要捕获重复实例的问题,我认为没有一种通用的方法可以修改正则表达式。一个“困难”(或者说不可能?)的转换案例是当一个重复必须回溯一个或多个循环以满足某些条件来匹配时。

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

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

  • 编写覆盖重复之前部分的正则表达式(例如 start:)。我们称之为前缀正则表达式
  • 匹配并捕获第一个实例(例如 (\w+)
    (此时,应该已经匹配了第一个实例和分隔符)
  • \G添加为可选项。通常还需要防止它匹配字符串的开头。
  • 添加分隔符(如果有的话)(例如 -
    (此时,除了最后一个令牌可能没有被匹配之外,其余令牌都应该已经被匹配)
  • 添加覆盖重复之后部分的部分(如果有必要)(例如:end)。我们将重复之后的部分称为后缀正则表达式(是否将其添加到构造中并不重要)。
  • 现在是困难的部分。您需要检查以下内容:
    • 除了前缀正则表达式,没有其他启动匹配的方法。注意\G分支。
    • 在匹配后缀正则表达式之后,没有任何启动任何匹配的方法。注意\G分支如何开始匹配。
    • 对于第一次构造,如果您在可选项中混合使用后缀正则表达式(例如:end)和分隔符(例如-),请确保您不会允许后缀正则表达式作为分隔符。


实际上经过一番思考,我发现你的方法确实是一个可行的解决方案!甚至在某些情况下,比一次捕获所有组更好!顺便说一句:我修复了你的正则表达式,以允许最少 3 个组在这里检查。请更新你的答案,这样我就可以接受它了。 - CSᵠ
@kaᵠ:感谢提醒3-10部分的问题(那真是太粗心了)。但是你的也有问题http://regex101.com/r/zE5hU5。我需要大概10-15分钟来编辑。 - nhahtdh
很好的发现,这就是 (?!^)\G 很重要的地方 演示 - CSᵠ
@kaᵠ:请注意,现在正则表达式是有歧义的:由于重复标记中允许使用“:”,因此不清楚是匹配到第二个“:end”还是停止在“start:some-thing-here:end:end unrelated text”中的第一个“:end”。 - nhahtdh
1
@Enissay:请忽略我的先前评论。在某些情况下,前瞻匹配 确实是必要的。否则,如果不检查整个字符串是否正确重复并具有后缀,则正则表达式可能会匹配该内容,即使后缀缺失也是如此。 - nhahtdh
显示剩余4条评论

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

3
两步解决方案通常更易读和易于维护。 - nhahtdh

1

当然,您可以在这个引用字符串中使用正则表达式。

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

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


0

当你结合以下内容时:

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

可以推断出这是不可能的。

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


实际上不行。\G需要循环或重复(第3页),并且不允许您在正则表达式本身上设置重复次数限制(3到10)(第2页)。 - Lodewijk Bogaards
你可以使用 preg_match_all 来实现,这使得它变得非常简单且只需要正则表达式的解决方案。捕获限制可能可以通过前瞻(在 \G 的解决方案中)来实现。(我不保证它适用于所有情况,但是有一类情况可以使用这种方法)。 - nhahtdh
嗯,这是可能的,但写一个正确的并不容易。请看我的回答。 - nhahtdh
@mrhobo:根据您的来源,看起来在.NET中可能可以使用类似的简单正则表达式,尽管我不确定如何操作。我使用regexplanet/dotnet的工具进行了测试,并得到了相同的结果(仅最后一个)。 - CSᵠ
使用.NET中的命名捕获组很容易实现。对于任何特定的命名组,您可以循环遍历其所有匹配的实例。 - Triynko

0

不确定你能否以那种方式做到,但你可以使用全局标志来查找冒号之间的所有单词,例如:

http://regex101.com/r/gK0lX1

然而,您必须自己验证组的数量。如果没有全局标志,您只会得到单个匹配项,而不是所有匹配项 - 将 {3,10} 更改为 {1,5},您将获得结果“sir”。

import re

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

生成

['测试', '测试', '罗伦', '伊普苏姆', '先生', '多洛雷特', '等等', '等等', '某些内容']


不幸的是,在这个无效的字符串中,它会找到匹配项:invalid:test-lorem-ipsum-sir-doloret:end,除了验证少于 3 个项目和超过 10 个项目的相似字符串之外,还需要编程来进行匹配过程。 - CSᵠ
1
我认为你在这里尝试做太多的事情了,超出了正则表达式的范围。 - spiralx
1
正则表达式非常强大!在特定情况下,如果构造正确,您可以实现很好的结果。请参见已接受的答案^ - CSᵠ

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接