33

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

所以假设文本是:

开始: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仅通过 RegEx 在内部捕获单个令牌,没有任何操作系统/软件/编程语言限制

更新 2(赏金):

在@nhahtdh 的帮助下,我使用以下方法获得了正则表达式\G

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

demo更短,但是可以不用重复代码来描述

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

4

5 回答 5

36

先读这个!

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

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

这篇文章处理你的具体例子。我真的怀疑一个完整的概括是可能的,但背后的想法是可重复用于类似的情况。

概括

  • .NET 支持使用CaptureCollection类捕获重复模式。
  • 对于支持\G和look-behind 的语言,我们也许可以构造一个与全局匹配函数一起工作的正则表达式。完全正确地编写它并不容易,编写一个有细微错误的正则表达式也很容易。
  • 对于没有\G和后向支持的语言:可以通过在单个匹配后对输入字符串进行 chomping来模拟\Gwith 。^(此答案未涵盖)。

解决方案

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

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

对于您的情况,它可以在一个正则表达式中完成,使用\G匹配边界,并提前限制重复次数:

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

演示\w+-重复构造\w+:end

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

演示。构造是\w+针对第一个项目,然后-\w+重复。(感谢 ka ᵠ 的建议)。这种结构更容易推断其正确性,因为交替较少。

\G当您需要进行标记化时,匹配边界特别有用,您需要确保引擎不会向前跳过并匹配本应无效的内容。

解释

让我们分解正则表达式:

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

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

最后一行也很容易识别:要匹配的单词后面可能跟着-or :end

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

至于解释,正则表达式的开头检查字符串的存在,start:下面的前瞻检查单词的数量是否在指定的限制内,并以 . 结尾:end要么,要么我们检查上一个匹配之前的字符是 a -,然后从上一个匹配继续。

对于其他结构:

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

一切都几乎相同,只是我们start:\w+先匹配,然后再匹配 form 的重复-\w+。与第一个构造相反,我们start:\w+-首先匹配,以及重复的实例\w+-(或\w+:end最后一次重复)。

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

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

  • \G也匹配字符串的开头!(?!^)需要防止这种行为。如果不考虑这一点,正则表达式可能会在没有任何start:.

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

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

    第二个构造不会遇到这个问题,因为在匹配完所有标记之后,我们会卡在:(of ) 上。:end

验证版本

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

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

(也不需要look-behind,但我们仍然需要(?!^)防止\G匹配字符串的开头)。

建造

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

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

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

  • 编写一个覆盖重复之前部分的正则表达式(例如start:)。让我们将此前缀称为 regex
  • 匹配并捕获第一个实例。(例如(\w+)
    (此时,第一个实例和分隔符应该已经匹配)
  • 添加\G作为替代。通常还需要防止它匹配字符串的开头。
  • 添加分隔符(如果有)。(例如-
    (在这一步之后,其余的标记也应该已经匹配,除了最后一个可能)
  • 在重复之后添加覆盖部分的部分(如果需要)(例如:end)。让我们在重复后缀正则表达式之后调用部分(我们是否将其添加到构造中并不重要)。
  • 现在是困难的部分。您需要检查:
    • 除了前缀 regex之外,没有其他方法可以开始匹配。注意\G分支。
    • 匹配后缀正则表达式,无法开始任何匹配。注意分支如何开始匹配​​。\G
    • 对于第一个构造,如果您将后缀正则表达式(例如:end)与分隔符(例如-)交替混合,请确保您最终不会允许后缀正则表达式作为分隔符。
于 2013-03-14T19:54:40.617 回答
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]));
}
于 2013-04-10T09:39:50.097 回答
1

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

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

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

于 2013-03-14T23:06:41.463 回答
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']

于 2013-03-07T17:35:08.583 回答
0

当你结合:

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

可以推导出做不到。

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

于 2013-03-08T21:10:13.947 回答