外观是否影响正则表达式可以匹配哪些语言?

时间:2010-06-04 12:44:20

标签: regex lookbehind lookahead lookaround

现代正则表达式引擎中有一些功能允许您匹配没有该功能时无法匹配的语言。例如,使用反向引用的以下正则表达式匹配由重复自身的单词组成的所有字符串的语言:(.+)\1。这种语言不规则,不能与不使用反向引用的正则表达式匹配。

环视还会影响正则表达式可以匹配的语言吗?即是否有任何语言可以使用无法匹配的外观匹配?如果是这样,对于所有类型的环视(负面或正向前瞻或后观)或仅仅针对其中一些而言,这是真的吗?

4 个答案:

答案 0 :(得分:25)

你问的问题的答案是,是否可以用正则表达式识别比常规语言更大的语言类别,这是正确的。

证明相对简单,但是将包含外观的正则表达式转换为一个的算法很麻烦。

首先:请注意,您总是可以否定正则表达式(通过有限字母表)。给定一个识别表达式生成的语言的有限状态自动机,您可以简单地将所有接受状态交换为非接受状态,以获得能够准确识别该语言否定的FSA,其中有一系列等效正则表达式。

第二:因为正定语言(以及因此正则表达式)在否定时被关闭,所以它们也在交叉点下闭合,因为A交叉B = neg(neg(A)union neg(B))de Morgan定律。换句话说,给定两个正则表达式,您可以找到另一个匹配两者的正则表达式。

这允许您模拟环视表达式。例如,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。

我认为这样的事情会是一种方法,但我很高兴有人写了这篇文章。我不可能想出一些如此整洁的东西。

答案 1 :(得分:23)

答案 2 :(得分:9)

我同意其他帖子看起来是常规的(这意味着它没有为正则表达式添加任何基本功能),但我有一个论点,它比我见过的其他更简单的IMO。

我将通过提供DFA构造来证明环视是常规的。当且仅当语言具有可识别它的DFA时,该语言才是常规语言。请注意,Perl实际上并未在内部使用DFA(有关详细信息,请参阅此文章:http://swtch.com/~rsc/regexp/regexp1.html)但我们为了证明而构建DFA。

为正则表达式构造DFA的传统方法是首先使用Thompson算法构建NFA。给定两个正则表达式片段r1r2,Thompson的算法提供了连接(r1r2),交替(r1|r2)和重复(r1*)的构造。常用表达。这允许您逐位构建NFA,以识别原始正则表达式。有关详细信息,请参阅上面的文章。

为了表明正面和负面的前瞻是常规的,我将提供一个结构,用于连接具有正面或负面前瞻的正则表达式u(?=v)(?!v)。只有连接才需要特殊处理;通常的交替和重复建设工作正常。

u(?= v)和u(?!v)的构造是:

http://imgur.com/ClQpz.png

换句话说,将u的现有NFA的每个最终状态连接到接受状态连接到v的NFA,但修改如下。函数f(v)定义为:

  • aa(v)成为NFA v上的一项功能,将每个接受状态更改为“反接受状态”。如果通过NFA的任何路径在给定字符串s的此状态下结束,则将接受状态定义为导致匹配失败的状态,即使通过不同的路径v的{​​{1}}以接受状态结束。
  • s成为NFA loop(v)上的一个函数,它会在任何接受状态上添加自转换。换句话说,一旦一条路径通向一个接受状态,无论接下来是什么输入,该路径都可以永远保持在接受状态。
  • 对于否定前瞻,v
  • 积极向前看,f(v) = aa(loop(v))

为了提供一个直观的例子,为什么会这样,我将使用正则表达式f(v) = aa(neg(v)),这是我在弗朗西斯证明的评论中提出的正则表达式的略微简化版本。如果我们将我的构造与传统的Thompson结构一起使用,我们最终得到:

alt text

(b|a(?:.b))+是epsilon过渡(可以在不消耗任何输入的情况下进行过渡),反接受状态用e标记。在图表的左半部分,您会看到X的表示形式:任何(a|b)+a将图形置于接受状态,但也允许转换回到开始状态,因此我们可以再做一次。但请注意,每次我们匹配b时,我们也会输入图表的右半部分,我们处于反接受状态,直到我们匹配“任意”后跟a

这不是传统的NFA,因为传统的NFA没有反接受状态。但是,我们可以使用传统的NFA-&gt; DFA算法将其转换为传统的DFA。该算法像往常一样工作,我们通过使我们的DFA状态对应于我们可能处于的NFA状态的子集来模拟NFA的多次运行。一个转折点是我们稍微增加了用于确定DFA状态是否为接受(最终)状态。在传统算法中,如果NFA状态的任何是接受状态,则DFA状态是接受状态。我们修改这个以说DFA状态是一个接受状态当且仅当:

  •   

    = 1 NFA状态是接受状态,

  • 0 NFA州是反接受州。

此算法将为我们提供一个DFA,可以通过前瞻识别正则表达式。因此,前瞻是常规的。请注意,lookbehind需要单独的证明。

答案 3 :(得分:2)

我觉得这里有两个不同的问题:

  • Regex引擎是否会更加“环顾” 比Regex引擎更强大吗?
  • “环顾四周” 使正则表达式引擎能够解析语言 比Chomsky Type 3 - Regular grammar生成的更复杂?

实际意义上的第一个问题的答案是肯定的。 Lookaround将提供一个正则表达式引擎 使用此功能基本上比没有功能的功能更强大。这是因为 它为匹配过程提供了更丰富的“锚点”。 Lookaround允许您将整个正则表达式定义为可能的锚点(零宽度断言)。您可以 对此功能的强大功能进行了很好的概述here

看起来虽然功能强大,但并没有将Regex引擎超越理论范围 3型语法对其施加的限制。例如,您永远无法可靠 使用Regex引擎解析基于Context Free - Type 2 Grammar的语言 配备外观。正则表达式引擎仅限于Finite State Automation的强大功能 这从根本上限制了他们可以解析为3类语法水平的任何语言的表达能力。不管 您的Regex引擎添加了多少“技巧”,通过Context Free Grammar生成的语言 将始终超越其能力。解析上下文自由 - 类型2语法要求下推自动化“记住”它所在的位置 递归语言构造。任何需要递归评估语法规则的内容都无法使用进行解析 正则表达式引擎。

总结一下:Lookaround为Regex引擎提供了一些实际的好处,但没有“改变游戏” 理论水平。

修改

在Type 3(Regular)和Type 2(Context Free)之间是否有一些复杂的语法?

我相信答案是否定的。原因是因为没有理论上的限制 放在描述常规语言所需的NFA / DFA大小上。它可能变得任意大 因此使用(或指定)是不切实际的。这就是“环视”这样的避难所很有用的地方。他们 提供一种简便的机制来指定否则会导致非常大/复杂的NFA / DFA 规格。他们不会增加表现力 常规语言,他们只是使它们更实用。一旦你明白这一点,就会变成这样 清楚的是,有许多“功能”可以添加到Regex引擎中以使它们更多 在实际意义上有用 - 但没有什么能使它们超越 常规语言的限制。

Regular和Context Free语言之间的基本区别在于常规语言 不包含递归元素。为了评估递归语言,您需要一个 推倒自动化 “记住”你在递归中的位置。 NFA / DFA不会堆叠状态信息,因此不能 处理递归。因此,给定非递归语言定义会有一些NFA / DFA(但是 不一定是一个实用的正则表达式来描述它。