环视是不是会影响正则表达式可以匹配哪些语言?
Posted
技术标签:
【中文标题】环视是不是会影响正则表达式可以匹配哪些语言?【英文标题】:Does lookaround affect which languages can be matched by regular expressions?环视是否会影响正则表达式可以匹配哪些语言? 【发布时间】:2011-02-27 18:54:39 【问题描述】:现代正则表达式引擎中有一些功能允许您匹配没有该功能无法匹配的语言。例如,以下使用反向引用的正则表达式匹配由重复自身的单词组成的所有字符串的语言:(.+)\1
。这种语言不规则,不能被不使用反向引用的正则表达式匹配。
环视是否也会影响正则表达式可以匹配哪些语言? IE。是否有任何语言可以使用环视来匹配,否则无法匹配?如果是这样,这适用于所有类型的环视(负或正前瞻或后视)还是仅适用于其中一些?
【问题讨论】:
regular-expressions.info/lookaround.html 声明“环视允许您创建没有它们就无法创建的正则表达式,或者没有它们会变得非常冗长”。但是在这个方向上唯一的例子是不可能找到并匹配一个q而不是一个u。这并没有说明是否可以判断输入字符串 contains 一个 q 后跟一个 u (而不必只匹配那个q). @ChristianSemrau:这本身可能不是一个编程问题,但要求只是“编程相关”,我认为这是合格的。对我来说,从编程过程中提出的实际角度来看,这个问题实际上很有趣。 @Christian Semrau:我对“编程相关”的主要标准是问题是否出现在类似的会计网站上(有明显的简单替换)。正则表达式是非常严格的编程事物。我个人认为这很切题。 显然CS是否属于***的问题之前已经讨论过:meta.stackexchange.com/questions/26889/…。就我个人而言,我希望在这里看到更多的 CS 问题,或者如果有必要的话,可能是一个姊妹网站。 cs.stackexchange.com/questions/2557/… 【参考方案1】:对于您提出的问题,即是否可以使用通过环视增强的正则表达式识别比常规语言更大的语言类别,答案是否定的。
证明相对简单,但是将包含环视的正则表达式转换为没有环视的正则表达式的算法是混乱的。
首先:请注意,您始终可以否定正则表达式(在有限的字母表上)。给定一个识别由表达式生成的语言的有限状态自动机,您可以简单地将所有接受状态交换为非接受状态,以获得准确识别该语言的否定的 FSA,为此存在一系列等效的正则表达式.
第二:因为正则语言(因此正则表达式)在否定下是封闭的,所以它们在交集下也是封闭的,因为根据德摩根定律 A intersect B = neg ( neg(A) union neg(B))。换句话说,给定两个正则表达式,您可以找到另一个匹配两者的正则表达式。
这允许您模拟环视表达式。例如 u(?=v)w 仅匹配将匹配 uv 和 uw 的表达式。
对于负前瞻,您需要与集合论 A\B 等效的正则表达式,它只是 A 相交 (neg B) 或等效地 neg (neg(A) union B)。因此,对于任何正则表达式 r 和 s,您都可以找到一个正则表达式 r-s,它匹配那些匹配 r 而不匹配 s 的表达式。在否定前瞻术语中:u(?!v)w 仅匹配那些匹配 uw - uv 的表达式。
环视之所以有用有两个原因。
首先,因为正则表达式的否定会导致一些不太整洁的东西。例如q(?!u)=q($|[^u])
。
其次,正则表达式的作用不仅仅是匹配表达式,它们还使用字符串中的字符——或者至少我们喜欢这样思考它们。例如在 python 中,我关心 .start() 和 .end(),因此当然:
>>> re.search('q($|[^u])', 'Iraq!').end()
5
>>> re.search('q(?!u)', 'Iraq!').end()
4
第三,我认为这是一个非常重要的原因,正则表达式的否定并不能很好地提升连接。 neg(a)neg(b) 与 neg(ab) 不同,这意味着您不能将环视转换出您找到它的上下文 - 您必须处理整个字符串。我想这会让人们不愉快地工作,并打破人们对正则表达式的直觉。
我希望我已经回答了你的理论问题(深夜,如果我不清楚,请原谅我)。我同意一位评论员的观点,他说这确实有实际应用。在尝试抓取一些非常复杂的网页时,我遇到了非常相似的问题。
编辑
我很抱歉没有更清楚:我不相信你可以通过结构归纳来证明正则表达式 + 环视的规律性,我的 u(?!v)w 示例就是这样,一个示例,并且一个容易的。结构归纳不起作用的原因是因为环视以非组合方式表现 - 我试图在上面的否定中提出这一点。我怀疑任何直接的正式证明都会有很多混乱的细节。我试图想一个简单的方法来展示它,但我想不出一个。
为了说明使用 Josh 的第一个 ^([^a]|(?=..b))*$
示例,这相当于一个所有状态都接受的 7 状态 DFSA:
A - (a) -> B - (a) -> C --- (a) --------> D
Λ | \ |
| (not a) \ (b)
| | \ |
| v \ v
(b) E - (a) -> F \-(not(a)--> G
| <- (b) - / |
| | |
| (not a) |
| | |
| v |
\--------- H <-------------------(b)-----/
状态 A 的正则表达式如下:
^(a([^a](ab)*[^a]|a(ab|[^a])*b)b)*$
换句话说,通过消除环视获得的任何正则表达式通常会更长更混乱。
回应 Josh 的评论 - 是的,我确实认为证明等效性的最直接方法是通过 FSA。使这个更混乱的是,构造 FSA 的常用方法是通过非确定性机器 - 更容易将 u|v 简单地表示为由 u 和 v 的机器构造的机器,并带有到它们两者的 epsilon 转换。当然,这等效于确定性机器,但存在状态指数爆炸的风险。而通过确定性机器进行否定要容易得多。
一般证明将涉及获取两台机器的笛卡尔积,并在要插入环视的每个点处选择您希望保留的那些状态。上面的例子在一定程度上说明了我的意思。
我很抱歉没有提供建筑。
进一步编辑: 我发现了一个blog post,它描述了一种算法,用于从带有环视功能的正则表达式生成 DFA。它很简洁,因为作者以明显的方式用“标记的 epsilon 转换”扩展了 NFA-e 的想法,然后解释了如何将这样的自动机转换为 DFA。
我认为类似的方法可以做到这一点,但我很高兴有人写了它。想出如此巧妙的东西,我无法做到。
【讨论】:
我同意 Francis 的观点,即环视是有规律的,但我认为证明是不正确的。问题是您不能将环视正则表达式分解为两个正则表达式 A 和 B。弗朗西斯通过将u(?!v)w
转换为uw
和uv
来做到这一点,但我不相信一般来说存在这样的算法。相反,您可以将前瞻或否定(前瞻)附加到原始 DFA 在它发生 epsilon 转换的点。这个细节有点棘手,但我认为它有效。
例如,考虑正则表达式^([^a]|a(?=..b))*$
。换句话说,所有字符都是允许的,但每个“a”后面必须跟三个字符后面的“b”。我不相信您可以将其简化为您通过联合组合的两个正则表达式 A 和 B。我认为您必须在 NFA 构建中做出积极的前瞻性。
@Josh, sepp2k:对于每种正则语言 L,都有一个等效的正则表达式,反之亦然。现在 a(?=..b) 是正则的,它对应于某个表达式,比如 r。现在你有了 ([^a]|r)*,它又是常规的。我相信这已经被 Kleene 证明了。看看这个:coli.uni-saarland.de/projects/milca/courses/coal/html/…。归纳证明确实有效。您似乎对正则表达式和语言的基本事实感兴趣(我在此评论中的第一句话)。
@Moron:您假设前瞻表达式的组成方式与正则表达式的组成方式相同。您假设([^a]|r)*
匹配与([^a]|a(?=..b))
相同的语言,这不正确,即使r
匹配与a(?=..b)
相同的语言。如果您自己进行 DFA 扩展,您会看到。由于前瞻匹配字符而不消耗它们,因此它与正则表达式的组合方式不同。如果您仍然不相信这一点,我稍后会发布实际的 DFA 扩展。
作为一个简短的证明,考虑a(?=..b)
是空语言,因为a ∩ a..b = ϵ
。所以如果我们按照你的推理r = ϵ
和([^a]|a(?=..b))*
相当于([^a]|ϵ)*
或只是[^a]*
。但这显然是错误的,因为aaab
匹配原始的正则表达式,但不是假定的等价的。【参考方案2】:
正如其他答案所声称的那样,环顾四周不会为正则表达式增加任何额外的功能。
我认为我们可以使用以下方法来展示这一点:
One Pebble 2-NFA(参见论文的引言部分)。
1-pebble 2NFA 不处理嵌套的前瞻,但是,我们可以使用 multi-pebble 2NFA 的变体(请参阅下面的部分)。
简介
2-NFA 是一个非确定性的有限自动机,它能够根据输入向左或向右移动。
单鹅卵石机器是机器可以在输入磁带上放置鹅卵石(即用鹅卵石标记特定输入符号)并根据当前输入位置是否有鹅卵石进行可能的不同转换。
众所周知,One Pebble 2-NFA 具有与常规 DFA 相同的功能。
非嵌套前瞻
基本思路如下:
2NFA 允许我们通过在输入磁带中向前或向后移动来回溯(或“前轨”)。因此,对于前瞻,我们可以对前瞻正则表达式进行匹配,然后回溯我们所消耗的内容,以匹配前瞻表达式。为了准确知道何时停止回溯,我们使用了鹅卵石!我们在进入 dfa 之前放下鹅卵石进行前瞻,以标记回溯需要停止的位置。
因此,在我们的字符串通过 pebble 2NFA 运行结束时,我们知道我们是否匹配了前瞻表达式,并且输入 left(即剩下要消耗的内容)正是匹配剩余部分所需的内容。
所以对于 u(?=v)w 形式的前瞻
我们有 u、v 和 w 的 DFA。
从 u 的 DFA 的接受状态(是的,我们可以假设只有一个),我们向 v 的开始状态进行电子转换,用鹅卵石标记输入。
从 v 的接受状态,我们 e 转换到不断向左移动输入的状态,直到找到一个 pebble,然后转换到 w 的开始状态。
从 v 的拒绝状态,我们电子转移到一个不断向左移动直到找到卵石的状态,然后转移到 u 的接受状态(即我们离开的地方)。
用于显示 r1 的常规 NFA 的证明 | r2 或 r* 等用于这些 2nfas 的卵石。请参阅http://www.coli.uni-saarland.de/projects/milca/courses/coal/html/node41.html#regularlanguages.sec.regexptofsa,了解有关如何将组件机器组合在一起为 r* 表达式等提供更大机器的更多信息。
上述 r* 等证明起作用的原因是,当我们进入组件 nfas 进行重复时,回溯确保输入指针始终位于正确的位置。此外,如果正在使用卵石,则它正在由其中一台前瞻组件机器进行处理。由于没有完全回溯和取回卵石,就没有从前瞻机器到前瞻机器的过渡,因此只需要一台卵石机器。
例如考虑 ([^a] | a(?=...b))*
还有字符串abbb。
我们有abbb,它通过peb2nfa for a(?=...b),最后我们处于状态:(bbb,matched)(即输入中的bbb是剩余的,并且已经匹配'a' 后跟 '..b')。现在因为有 *,我们回到开头(参见上面链接中的构造),并为 [^a] 输入 dfa。匹配b,回到开头,再次输入[^a]两次,然后接受。
处理嵌套的前瞻
要处理嵌套的前瞻,我们可以使用此处定义的 k-pebble 2NFA 的受限版本:Complexity Results for Two-Way and Multi-Pebble Automata and their Logics(参见定义 4.1 和定理 4.2)。
一般来说,2个卵石自动机可以接受非正则集,但有以下限制,可以证明k-卵石自动机是正则集(上述论文中的定理4.2)。
如果鹅卵石是 P_1, P_2, ..., P_K
P_i+1 可能不会被放置,除非 P_i 已经在磁带上,并且 P_i 可能不会被拾取,除非 P_i+1 不在磁带上。基本上,鹅卵石需要以 LIFO 方式使用。
在 P_i+1 放置时间和 P_i 被拾取或 P_i+2 放置时间之间,自动机只能遍历位于当前P_i 的位置和位于 P_i+1 方向上的输入词的结尾。此外,在这个子词中,自动机只能作为具有 Pebble P_i+1 的 1-pebble 自动机。尤其是不允许举起、放置甚至感知另一块鹅卵石的存在。
因此,如果 v 是深度为 k 的嵌套前瞻表达式,则 (?=v) 是深度为 k+1 的嵌套前瞻表达式。当我们进入其中的前瞻机器时,我们确切地知道到目前为止必须放置多少个鹅卵石,因此可以准确地确定要放置哪个鹅卵石,当我们退出该机器时,我们知道要举起哪个鹅卵石。通过放置 pebble t 进入深度 t 的所有机器,并通过移除 pebble t 退出(即我们返回到深度 t-1 机器的处理)。整机的任何运行看起来都像是树的递归dfs调用,可以满足多卵石机的上述两个限制。
现在当你组合表达式时,对于 rr1,由于你 concat,r1 的卵石数必须增加 r 的深度。对于 r* 和 r|r1,卵石编号保持不变。
因此,任何具有前瞻的表达式都可以转换为具有上述 pebble 放置限制的等效多 pebble 机器,因此是常规的。
结论
这基本上解决了 Francis 原始证明中的缺点:能够防止前瞻表达式消耗未来匹配所需的任何内容。
由于 Lookbehinds 只是有限字符串(不是真正的正则表达式),我们可以先处理它们,然后再处理前瞻。
很抱歉写的不完整,但完整的证明需要画很多图。
这对我来说是正确的,但我很高兴知道任何错误(我似乎很喜欢:-))。
【讨论】:
我不确定这是否能处理多个前瞻,例如u(?=v)(?=w)(?=x)z
是吗?
当我们退出 pebble 2NFA 进行前瞻时,我们会回到我们进入的输入磁带状态,使用 pebble,并且取决于匹配与否,我们处于一个两种不同的状态(即我们可以判断是否存在匹配)。因此,它似乎可以通过连接自动机(我们添加的带有电子转换的额外状态)来工作,因为我们总是会取回鹅卵石。但我想这取决于您如何解释该表达式。和u(?=vwx)z一样吗?或 ((u(?=v))?=w)... 等等?
该表达式匹配一个 u,该 u 必须后跟(非消耗)所有三个 v、w 和 x(其中 v、w 和 x 都是通用正则表达式)和一个 z。在尝试构建可以解决此问题的东西后,我相当确信您无法通过组合方式(即通过连接解决方案)来实现。
@Francis:如果它必须匹配所有这些,那么串联工作(我认为)。我们将其连接为 dfa(u) -> peb2ndfa(v) -> peb2ndfa(w) -> dfa(x)。如果在匹配 u 之后,我们不匹配 v 或 w,我们回到 u 并从我们离开的地方继续。如果我们匹配 v,那么因为我们在 v 完成后回溯,我们可以再次匹配 w(再次回溯)然后匹配 x。关键是 2NDFA 允许我们回溯,而 pebble 允许我们知道何时停止回溯。
@sepp2k:您有机会阅读此答案吗?如果您有任何问题/澄清/反例,我很乐意回答。【参考方案3】:
我同意环视是常规的其他帖子(这意味着它不会为正则表达式添加任何基本功能),但我有一个论点,即 IMO 比我见过的其他帖子更简单。
我将通过提供 DFA 构造来展示环视是常规的。当且仅当一种语言具有识别它的 DFA 时,它才是正则的。请注意,Perl 实际上并没有在内部使用 DFA(有关详细信息,请参阅本文:http://swtch.com/~rsc/regexp/regexp1.html),但我们构建了一个 DFA 用于证明。
为正则表达式构建 DFA 的传统方法是首先使用 Thompson 算法构建 NFA。给定两个正则表达式片段 r1
和 r2
,Thompson 算法提供了正则表达式的连接 (r1r2
)、交替 (r1|r2
) 和重复 (r1*
) 构造。这允许您逐步构建识别原始正则表达式的 NFA。有关详细信息,请参阅上面的论文。
为了表明正负前瞻是常规的,我将提供一个将正则表达式 u
与正或负前瞻连接的构造:(?=v)
或 (?!v)
。只有串联需要特殊处理;通常的交替和重复结构可以正常工作。
u(?=v) 和 u(?!v) 的构造是:
换句话说,将u
的现有NFA 的每个最终状态都连接到接受状态和 到v
的NFA,但修改如下。函数f(v)
定义为:
aa(v)
成为 NFA v
上的一个函数,它将每个接受状态更改为“反接受状态”。反接受状态定义为如果通过 NFA 的 任何 路径对于给定字符串 s
以该状态结束,则导致匹配失败的状态,即使通过 @987654339 的不同路径也是如此@ for s
以接受状态结束。
让 loop(v)
成为 NFA v
上的一个函数,它在任何接受状态上添加一个自转换。换句话说,一旦路径导致接受状态,无论输入什么,该路径都可以永远保持接受状态。
对于负前瞻,f(v) = aa(loop(v))
。
对于积极的前瞻,f(v) = aa(neg(v))
。
为了提供一个直观的例子来说明为什么会这样,我将使用正则表达式(b|a(?:.b))+
,它是我在弗朗西斯证明的 cmets 中提出的正则表达式的略微简化版本。如果我们将我的构造与传统的 Thompson 构造一起使用,我们最终会得到:
e
s 是 epsilon 转换(可以在不消耗任何输入的情况下进行转换),反接受状态用X
标记。在图表的左半部分,您可以看到(a|b)+
的表示:任何a
或b
将图表置于接受状态,但也允许转换回开始状态,以便我们再次执行此操作。但请注意,每次我们匹配 a
时,我们也会进入图表的右半部分,在此我们处于反接受状态,直到我们匹配 "any" 后跟 b
。
这不是传统的 NFA,因为传统的 NFA 没有反接受状态。但是,我们可以使用传统的 NFA->DFA 算法将其转换为传统的 DFA。该算法像往常一样工作,我们通过使我们的 DFA 状态对应于我们可能处于的 NFA 状态的子集来模拟 NFA 的多次运行。一个转折是我们稍微增加了确定 DFA 状态是否是接受(最终)状态与否。在传统算法中,如果 任何 NFA 状态是接受状态,则 DFA 状态是接受状态。我们将其修改为当且仅当以下情况下,DFA 状态才是接受状态:
0 NFA 状态是反接受状态。= 1 NFA 状态是接受状态,和
该算法将为我们提供一个 DFA,它可以通过前瞻识别正则表达式。因此,前瞻是常规的。请注意,lookbehind 需要单独的证明。
【讨论】:
在你给的机器上,你接受一个。哪个不在 (b | a(?=.b)) 中。反接受状态也是匹配失败的接受状态?那么根据接受状态的定义,没有反接受状态!还是我错过了什么? @Moron:我认为你错过了我的反接受状态的含义。这是相同的图,但有编号的状态:imgur.com/ho4C8.png 我的机器不接受a
,因为在匹配a
之后,我们可以转换到状态 4、3、1 和 5(使用 NFA->DFA 算法)。但是状态 5 是反接受状态,所以按照我的文章底部的规则,状态 4、3、1 和 5 对应的 DFA 状态不是接受状态。跨度>
@Josh:aa(v)
的定义不是依赖于字符串s
吗?即集合aa(v)
可以随s
变化。您还说反接受状态开始是接受状态。那么,如果机器最终处于那种状态,任何匹配怎么会失败呢?对不起,如果我读错了。
@Moron: aa(v)
只是将所有接受状态翻转为反接受状态,因此它不应该依赖于s
。 v
和 aa(v)
都是 NFA,而不是集合。我不听你最后的评论:确实在v
中有接受状态,但aa(v)
没有任何接受状态,而aa(v)
是最终NFA 中的最终结果。
@Josh:您的定义:“让 aa(v) 成为 NFA v 上的一个函数,它将每个 accept 状态更改为反接受状态..”。因此,如果机器 v 在某些运行中以状态 P 结束并且匹配失败,则您将接受状态 P 更改为非接受状态。但是根据接受状态的定义(注意 v 仍然是 NFA),如果机器在那里结束,则匹配已通过!我所说的集合是指 v 中的状态集合,您需要更改为反接受,以使 v 变为 aa(v)。这不取决于字符串s
吗?我希望我现在更清楚了。【参考方案4】:
我感觉这里有两个不同的问题:
是包含更多“环视”的正则表达式引擎 比没有的正则表达式引擎强大吗? 是否“环顾” 赋予正则表达式引擎解析语言的能力 比Chomsky Type 3 - Regular grammar 生成的更复杂?在实际意义上,第一个问题的答案是肯定的。 Lookaround 将提供一个正则表达式引擎 使用此功能从根本上比不使用此功能的功能更强大。这是因为 它为匹配过程提供了一组更丰富的“锚”。 Lookaround 允许您将整个 Regex 定义为可能的锚点(零宽度断言)。你可以 大致了解此功能的强大功能here。
Lookaround 虽然功能强大,但不会使 Regex 引擎超出理论值 第 3 类语法对其施加的限制。例如,您将永远无法可靠地 使用正则表达式引擎解析基于Context Free - Type 2 Grammar 的语言 配备环视。正则表达式引擎仅限于Finite State Automation 这从根本上将他们可以解析的任何语言的表达能力限制在第 3 类语法的水平。不管 有多少“技巧”被添加到您的正则表达式引擎中,通过Context Free Grammar 生成的语言 将永远超出其能力范围。 Parsing Context Free - Type 2 语法需要下推自动化来“记住”它的位置 递归语言结构。任何需要对语法规则进行递归评估的东西都不能使用 正则表达式引擎。
总结一下:Lookaround 为 Regex 引擎提供了一些实际的好处,但不会“改变游戏” 理论水平。
编辑
在类型 3(常规)和类型 2(上下文无关)之间是否存在某种复杂的语法?
我相信答案是否定的。原因是因为没有理论上的限制 放在描述正则语言所需的 NFA/DFA 的大小上。它可能会变得任意大 因此使用(或指定)不切实际。这是诸如“环视”之类的闪避有用的地方。他们 提供一种简写机制来指定否则会导致非常大/复杂的 NFA/DFA 规格。它们不会增加表达能力 常规语言,它们只是使指定它们更实用。一旦你明白了这一点,它就变成了 很明显,有很多“功能”可以添加到正则表达式引擎中,以使它们更多 在实际意义上有用 - 但没有什么能让他们能够超越 正则语言的限制。
Regular 和 Context Free 语言之间的基本区别在于 Regular 语言 不包含递归元素。为了评估递归语言,您需要一个 下推自动化 “记住”你在递归中的位置。 NFA/DFA 不堆叠状态信息,因此不能 处理递归。所以给定一个非递归的语言定义会有一些 NFA/DFA(但是 不一定是实际的正则表达式)来描述它。
【讨论】:
比常规语法更强大的语法一定和上下文无关一样强大吗? IE。是否知道两者之间存在 no 语法? @BlueRaja:正是我的想法:'语法连续统假设':-) @Moron @BlueRaja - 我已经为你编辑了我的答案。希望对您有所帮助。 当然,在正则文法类和上下文无关文法类之间有很多严格的文法类,包括一些琐碎的例子,比如正则文法类和语言的文法。平衡括号。 deterministic context free grammars 是一个更有用的例子。An NFA/DFA does not stack state information so cannot handle the recursion.
是的。所以请停止尝试使用正则表达式解析 HTML!以上是关于环视是不是会影响正则表达式可以匹配哪些语言?的主要内容,如果未能解决你的问题,请参考以下文章