首先:精彩的问题。我认为尝试将正则表达式引擎发挥到极致是很有启发性的。
基本的 .NET 解决方案
你们在评论中说使用.NET 会很容易,但是由于还没有答案,我想我会写一个。
您可以使用 .NET 的可变长度后视和平衡组来解决问题 1. 和 2.。大部分工作由平衡组完成,但可变长度的后视对于能够检测到从同一行开始的多个匹配项至关重要。
无论如何,这是模式:
(?<= # lookbehind counts position of X into stack
^(?:(?<a>).)* # push an empty capture on the 'a' stack for each character
# in front of X
) # end of lookbehind
X # match X
(?=.*\n # lookahead checks that there are two more Xs right below
(?:(?<-a>)(?<b>).)* # while we can pop an element from stack 'a', push an
# element onto 'b' and consume a character
(?(a)(?!)) # make sure that stack 'a' is empty
X.*\n # match X and the rest of the line
(?:(?<-b>).)* # while we can pop an element from stack 'b', and consume
# a character
(?(b)(?!)) # make sure that stack 'b' is empty
X # match a final X
) # end of lookahead
此模式必须与 一起使用RegexOptions.Multiline
以^
匹配行的开头(显然与RegexOptions.IgnorePatternWhitespace
自由空间模式一起工作)。
以下是一些额外的评论:
通过将除初始 X 之外的所有内容放入环视中,我们不会遇到重叠匹配甚至从同一行开始的匹配的问题。但是,lookbehind 必须是可变长度的,这肯定会将任何此类解决方案限制在 .NET 中。
其余的依赖于对平衡组的良好掌握。我不会在这里详细讨论这个问题,因为它本身就给出了相当长的答案。(有关更多信息,请参阅MSDN和此博客文章)
向后看只是匹配^.*
,所以直到行首的所有内容,但是对于每一次.
我们将一个空捕获推入 stack a
,从而将我们的位置计算X
为堆栈的大小。
然后在使用前瞻中的其余行之后,我们再次匹配 just .*
,但在使用 each 之前.
,我们从堆栈中弹出一个元素a
(这会导致失败,一次a
为空)并将一个空捕获推送到b
(这样我们就不会'不要忘记第三行必须有多少个字符)。
为了确保我们真正清空整个堆栈,我们使用(?(a)(?!))
. (?!)
这是一个条件模式,如果堆栈不为空,则尝试匹配a
(否则简单地跳过)。并且(?!)
是一个空的否定前瞻,它总是失败。因此,这只是编码,“a
不是空的?失败。否则,继续”。
现在知道我们已经在新行中使用了正确数量的字符,我们尝试匹配 aX
和该行的其余部分。然后我们用 stack 再次重复相同的过程b
。现在不需要压入任何新堆栈,因为如果这可行,我们就完成了。在此之后我们检查它b
是否为空,并匹配第三个X
。
最后,一个优化方面的说明:如果所有重复都包含在原子组中(从而模拟所有格量词,.NET 不支持),这种模式仍然有效!这将节省大量的回溯。此外,如果我们至少将堆栈弹出量词放在原子组中,我们可以摆脱这两个(?(...)(?!))
检查(因为这些检查仅在前面的重复必须回溯的情况下才需要)。
完整的 .NET 解决方案
(只有最勇敢的冒险者才能跟随我进入我即将进入的真正黑暗的洞穴......)
正如评论中所讨论的,这个解决方案有一个缺点:它计算重叠匹配。例如
..X..
..X..
..X..
..X..
给出两个匹配项,一个在第一行,一个在第二行。我们想避免这种情况,只报告一场比赛(如果有 6 到 8X
秒,则报告两场,如果有 9 到 11X
秒,则报告三场,依此类推)。此外,我们要报告第 1、4、7、... 的比赛X
。
我们可以通过要求第一个X
前面是X
满足我们要求的 3 个 other 的整数倍来调整上述模式以允许此解决方案。检查 this 的基本思想使用与以前相同的堆栈操作(除了我们在 3 个堆栈之间移动东西,以便在找到三个X
s 后我们结束了我们开始的地方)。为此,我们必须稍微调整一下lookbehind。
不过有一个问题。.NET 的可变长度lookbehind 使用另一个.NET 独有的功能,RightToLeftMode
其中模式是从右到左读取(和匹配)的。通常这不需要打扰我们,但是当我们将其与平衡组结合使用时,我们可能会遇到一些令人不快的惊喜。特别是,在考虑我们的捕获堆栈如何演变时,我们还需要从右到左(或从下到上)构造(并读取)表达式。
因此,当您阅读以下表达式(以及我的注释)时,从最外面的lookbehind 的末尾开始(您必须滚动一点) - 即就在唯一的顶级X
; 然后一直阅读到顶部。然后在后视之后继续。
(?<=
# note that the lookbehind below does NOT affect the state of stack 'a'!
# in fact, negative lookarounds can never change any capturing state.
# this is because they have to fail for the engine to continue matching.
# and if they fail, the engine needs to backtrack out of them, in which
# case the previous capturing state will be restored.
(?<! # if we get here, there is another X on top of the last
# one in the loop, and the pattern fails
^ # make sure we reached the beginning of the line
(?(a)(?!)) # make sure that stack 'a' is empty
(?:(?<-a>).)* # while we can pop an element from stack 'a', and consume
# a character
X.*\n # consume the next line and a potential X
)
# at this point we know that there are less than 3 Xs in the same column
# above this position. but there might still be one or two more. these
# are the cases we now have to eliminate, and we use a nested negative
# lookbehind for this. the lookbehind simply checks the next row and
# asserts that there is no further X in the same column.
# this, together with the loop, below means that the X we are going to match
# is either the topmost in its column or preceded by an integer multiple of 3
# Xs - exactly what we are looking for.
(?:
# at this point we've advanced the lookbehind's "cursor" by exactly 3 Xs
# in the same column, AND we've restored the same amount of captures on
# stack 'a', so we're left in exactly the same state as before and can
# potentially match another 3 Xs upwards this way.
# the fact that stack 'a' is unaffected by a full iteration of this loop is
# also crucial for the later (lookahead) part to work regardless of the
# amount of Xs we've looked at here.
^ # make sure we reached the beginning of the line
(?(c)(?!)) # make sure that stack 'a' is empty
(?:(?<-c>)(?<a>).)* # while we can pop an element from stack 'c', push an
# element onto 'a' and consume a character
X.*\n # consume the next line and a potential X
(?(b)(?!)) # make sure that stack 'b' is empty
(?:(?<-b>)(?<c>).)* # while we can pop an element from stack 'b', push an
# element onto 'c' and consume a character
X.*\n # consume the next line and a potential X
(?(a)(?!)) # make sure that stack 'a' is empty
(?:(?<-a>)(?<b>).)* # while we can pop an element from stack 'a', push an
# element onto 'b' and consume a character
X.*\n # consume the next line and a potential X
)* # this non-capturing group will match exactly 3 leading
# Xs in the same column. we repeat this group 0 or more
# times to match an integer-multiple of 3 occurrences.
^ # make sure we reached the beginning of the line
(?:(?<a>).)* # push an empty capture on the 'a' stack for each
# character in front of X
) # end of lookbehind (or rather beginning)
# the rest is the same as before
X # match X
(?=.*\n # lookahead checks that there are two more Xs right below
(?:(?<-a>)(?<b>).)* # while we can pop an element from stack 'a', push an
# element onto 'b' and consume a character
(?(a)(?!)) # make sure that stack 'a' is empty
X.*\n # match X and the rest of the line
(?:(?<-b>).)* # while we can pop an element from stack 'b', and consume
# a character
(?(b)(?!)) # make sure that stack 'b' is empty
X # match a final X
) # end of lookahead
RegexHero.net 上的工作演示。
这次我把所有的解释都正确地穿插在了模式中。因此,如果您按照我上面推荐的方式阅读该模式,您就会在需要时得到正确的解释......
现在那真是一头野兽。但它现在满足了整个规范,并展示了 .NET 的正则表达式的强大功能。而且,虽然这看起来很可怕,但我认为(一旦你意识到从右到左的东西)这比 PCRE 的可比解决方案(使用递归或其他方式)更容易理解。
正如 Kobi 在下面的评论中提到的那样,如果您接受在单个匹配的多个捕获中找到您的结果(例如,如果您有 7X
的列,您只会得到一个匹配,但使用2 在某个组中捕获)。您可以通过将主要(前瞻)部分重复 1 次或多次并捕获初始部分X
(尽管将所有内容放在前瞻中)来做到这一点。然后后视不需要计算X
s 的三倍,而只需要检查没有前导X
。这可能会将图案的大小减半。
部分 PCRE 解决方案
(如果只有最勇敢的冒险者跟随我完成最后的解决方案,我可能在接下来的旅程中只剩下疯子......)
为了证明我刚才所说的上述解决方案与 PCRE 的比较,让我们看看我们如何甚至可以远程解决 PCRE 中的全部问题。如果没有可变长度的后视和平衡组,我们将不得不更加努力地工作。
Qtax(OP)为他的第一个问题(检查字符串是否包含任何-column)提供了一个绝妙的解决方案,X
使用自引用组进行计数。这是一个非常优雅和紧凑的解决方案。但是因为每个匹配都是从行的X
开头到列的开头,并且匹配不能重叠,所以我们不能在每行得到多个匹配。我们可以尝试将所有内容都放在一个前瞻中(这样实际上没有匹配),但是两个零宽度匹配也永远不会从同一位置开始——所以我们仍然会在每个候选行中得到一个匹配。
然而,至少可以用 PCRE 解决问题 2 的第一部分:计算从每行开始的列数(因此计算X
列的总数)。由于我们无法以单个匹配的形式获得此计数(请参阅上一段),并且我们无法以单个组或捕获的形式获得此计数(因为 PCRE 仅提供固定且有限数量的捕获,而不是 .NET )。我们可以做的是对匹配中的列数进行编码。
方法如下:对于每一行,我们检查是否有一列开始。如果是这样,我们在某个捕获组中包含一个字符。然后,在报告成功匹配之前,我们尝试找到尽可能多的列 - 为每个列添加一个字符到该特定组。通过这样做,我们在特定捕获的长度中对从每行开始的列数进行编码。
实际上,在正则表达式中实现这个概念比听起来要复杂得多(而且听起来已经相当复杂)。无论如何,这里是:
^
(?:(?|
(?(5)(?![\s\S]*+\5))
(?!(?!)()())
(?=
(?:
.
(?=
.*+\n
( \3? . )
.*+\n
( \4? . )
)
)*?
X .*+\n
\3
X .*+\n
\4
)
()
|
(?(5)(?=[\s\S]*+\5)|(?!))
(?:
.
(?=
.*+\n
( \1? .)
.*+\n
( \2? .)
)
)+?
(?=
(?<=X).*+\n
(\1)
(?<=X).*+\n
(\2)
(?<=X)
)
(?=
([\s\S])
[\s\S]*
([\s\S] (?(6)\6))
)
){2})+
(实际上,它比这更容易一些 - 请参阅 Qtax 的答案以了解如何简化此方法。出于学术原因,无论如何我都会将这种方法留在这里,因为可以从中学到一些非常先进和有趣的技术 - 请参阅摘要结尾。)
是的,没有注释。我想,无论如何,没有人会真正阅读它们,所以我将尝试将这个表达式分解为多个部分(我将采用自上而下的方法)。
那么我们来看看来自地狱的洋葱的外层:
^
(?:(?|
checkForNextColumn
|
countAndAdvance
){2})+
因此,我们的匹配再次锚定在行的开头。然后我们有一个(?:...{2})+
表示偶数重复的东西。这就是两个子模式的交替。这些子模式代表了我上面提到的步骤。第一个检查是否有从当前行开始的另一列,第二个注册一个计数并为第一个子模式的另一个应用程序准备引擎的状态。所以控制权交给了第二个模式——第一个只是使用前瞻检查另一列,因此是一个零宽度模式。这就是为什么我不能简单地把所有东西都包起来,+
而是必须做{2})+
thing - 否则零宽度组件将只尝试一次;这是几乎所有引擎都应用的必要优化,以避免像(a*)+
.
还有一个(非常重要的细节):我用于(?|...)
交替。在这种分组中,每个备选方案都以相同的组号开始。因此在/(?|(a)|(b))/
两者中a
和b
都可以被捕获到 group1
中。这是允许子模式之间“通信”的关键技巧,因为它们可以修改相同的组。
无论如何......所以我们有这两个子模式。我们想确保控制在它们之间真正交替。因此,如果它是最后一个匹配的组,则每个组都会失败。我们通过将模式包装在一些分组和引用魔法中来做到这一点:
^(?:(?|
(?(5)(?![\s\S]*+\5)) # if group 5 has matched before make sure that
# it didn't match empty
checkForNextColumn # contains 4 capturing groups
() # this is group 5, match empty
|
(?(5)(?=[\s\S]*+\5)|(?!)) # make sure that group 5 is defined and that it
# matched empty
advanceEngineState # contains 4 capturing groups
(?=
([\s\S]) # this is group 5, match non-empty
[\s\S]* # advance to the end very end of the string
([\s\S] (?(6)\6)) # add a character from the end of the string to
# group 6
)
){2})+
因此,在每个备选方案结束时,我们将使该备选方案的条件无效,甚至开始匹配。在第二个备选方案的最后,我们还将6
使用 Qtax 概述的技术将一个字符包含到 group 中。这是计数步骤。即,组6
将包含与从当前行开始的列一样多的字符。
现在checkForNextColumn
真的只是 Qtax 在前瞻中的解决方案。不过,它需要再做一次修改,为了证明这一点,我们将advanceEngineState
首先研究。
让我们考虑一下我们希望如何修改状态,以使 Qtax 的解决方案匹配行中的第二列。假设我们有输入
..X..X..
..X..X..
..X..X..
我们想找到第二列。这可以通过从第一个之后的位置开始匹配X
并具有组\1
并且\2
已经分别初始化为第..X
2 行和第 3 行的前三个字符 ( ) 来完成(而不是它们为空)。
现在让我们尝试这样做:匹配所有内容,包括X
开始一列的下一个,然后用相应的行前缀填充两个组以在checkForNextColumn
模式中使用。这又是 Qtax 的模式,除了我们计算X
in(而不是在它之前停止),并且我们需要将捕获添加到一个单独的组中。所以这里是advanceEngineState
:
(?:
.
(?=
.*+\n
( \1? .)
.*+\n
( \2? .)
)
)+?
(?=
(?<=X) .*+\n
(\1)
(?<=X) .*+\n
(\2)
(?<=X)
)
请注意我是如何将X
s 变成后视的,以进一步了解一个字符,以及我如何有效地复制\1
into\3
和\2
into的最终内容\4
。
因此,如果我们现在像前瞻一样使用 Qtax 的解决方案checkForNextColumn
,使用组\3
and\4
而不是\1
and \2
,我们应该完成了。
但是我们如何创建这些组\3
而\4
不是\1
and\2
呢?我们可以用 开始模式()()
,它总是匹配,而不影响引擎的光标,但是将组计数增加 2。但是,这是有问题的:这会将组1
和空字符串重置2
为空字符串,如果我们找到第二列,advanceEngineState
将处于不一致的状态(因为引擎的全局位置已经提前,但计数组再次为零)。所以我们想让这两个组进入模式,但不影响他们当前正在捕获的内容。我们可以通过使用我已经提到的 .NET 解决方案来做到这一点:负面环视中的组不会影响捕获的内容(因为引擎需要从环视中回溯才能继续)。因此,我们可以使用(?!(?!)()())
(永远不会导致模式失败的负前瞻)在我们的模式中包含两组从未使用过的括号。这允许我们使用组3
和4
在我们的第一个子模式中,同时保持组1
和2
下一次迭代的第二个子模式保持不变。总之,这是checkForNextColumn
:
(?!(?!)()())
(?=
(?:
.
(?=
.*+\n
( \3? . )
.*+\n
( \4? . )
)
)*?
X .*+\n
\3
X .*+\n
\4
)
其中,在大多数情况下,实际上看起来真的很熟悉。
所以就是这样。针对某些输入运行此命令将为我们提供一个组,该组6
包含一个捕获,每行都有一个以列开头的行 - 捕获的长度将告诉我们从那里开始有多少列。
是的,它确实有效(现场演示)。
请注意,这(就像基本的 .NET 解决方案一样)会多计超过 3X
秒长的列。我想可以通过前瞻来纠正这个计数(以类似于完整.NET解决方案的后视的方式),但这留给读者作为练习。
有点不幸的是,这个解决方案的基本问题已经非常复杂并且使解决方案膨胀(75% 的行大多只是 Qtax 解决方案的副本)。因为周围的框架有一些非常有趣的技术和教训:
- 我们可以有多个子模式来完成特定的匹配/计数任务,并让它们通过相互捕获组“通信”,方法是将它们
(?|...)
交替放置并循环它们。
- 我们可以强制零宽度替代方案一遍又一遍地执行,方法是将它们包装在一个有限量词中,就像
{2}
在将所有内容放入之前一样+
。
- 可以在一个子模式中跳过组号(不影响捕获的内容),方法是将它们放入永不失败的负前瞻中,例如
(?!(?!)())
.
- 控制可以在子模式之间来回传递,方法是在进入交替时检查的某个组中捕获某些东西或什么都没有。
这允许进行一些非常强大的计算(我已经看到 PCRE 实际上是图灵完备的)——尽管这对于生产性使用来说肯定是错误的方法。但是仍然试图理解(并提出)这样的解决方案可能是一个非常具有挑战性的问题,并且在某种程度上是有益的解决问题的练习。