“现代”正则表达式的识别能力

Posted

技术标签:

【中文标题】“现代”正则表达式的识别能力【英文标题】:The recognizing power of "modern" regexes 【发布时间】:2011-06-17 23:30:47 【问题描述】:

真正的现代正则表达式实际上能识别哪类语言?

只要有一个带有反向引用的无限长度捕获组(例如(.*)_\1),正则表达式现在就会匹配非常规语言。但这本身并不足以匹配 S ::= '(' S ')' | ε 之类的东西——匹配括号对的上下文无关语言。

递归正则表达式(对我来说是新的,但我确信 Perl 和 PCRE 中存在)似乎至少可以识别大多数 CFL。

有人做过或读过这方面的研究吗?这些“现代”正则表达式的局限性是什么?他们对 LL 或 LR 语法的识别比 CFG 严格多还是严格少?或者是否存在两种语言都可以被正则表达式识别但不能被 CFG 相反?

非常感谢相关论文的链接。

【问题讨论】:

我不知道有任何正式的工作涉及可通过递归模式解决的可计算性问题。我知道您上面的递归生成很容易在 PCRE 或 Perl 中编码为递归模式。 这会更适合cstheory.stackexchange.com 吗? @arcain,我真的不认为这是一个“研究级别的问题”,因为它很可能已经被做死了......如果我没有听到,我可能会尝试把它贴在那里什么... @toby - 当然,但它一个理论问题,而且 cstheory 的社区是一个更专业的受众。数量也较低,因此您的问题在更容易回答的问题中迷失的可能性较小。我只是想看到你的问题得到答案。 旧帖,但我已经多次引用此链接:nikic.github.io/2012/06/15/… 【参考方案1】:

模式递归

有了递归模式,你就有了一种递归下降的形式匹配

这对各种问题都很好,但是一旦你想真正做递归下降解析,你需要在这里和那里插入捕获组,恢复完整的解析结构很尴尬这样。 Damian Conway 的用于 Perl 的 Regexp::Grammars 模块将简单模式转换为等效模式,该模式自动将所有命名捕获转换为递归数据结构,从而更容易检索已解析的结构。在这篇文章的最后,我有一个比较这两种方法的示例。

递归限制

问题是递归模式可以匹配哪些类型的语法。好吧,它们肯定是 recursive descent 类型匹配器。唯一想到的是递归模式不能处理left recursion。这限制了你可以应用它们的语法种类。有时您可以重新排序您的产品以消除左递归。

顺便说一句,PCRE 和 Perl 在允许您如何表述递归方面略有不同。请参阅 pcrepattern 手册页中有关“递归模式”和“与 Perl 的递归差异”的部分。例如:Perl 可以处理 ^(.|(.)(?1)\2)$ 而 PCRE 需要 ^((.)(?1)\2|.)$

递归演示

对递归模式的需求出人意料地频繁出现。一个受欢迎的例子是当您需要匹配可以嵌套的东西时,例如平衡括号、引号,甚至是 html/XML 标记。这是平衡括号的匹配:

\((?:[^()]*+|(?0))*\)

由于其紧凑的性质,我发现阅读起来比较棘手。这很容易通过/x 模式解决,使空白不再重要:

\( (?: [^()] *+ | (?0) )* \)

再一次,由于我们使用括号进行递归,一个更清晰的例子是匹配嵌套的单引号:

‘ (?: [^‘’] *+ | (?0) )* ’

您可能希望匹配的另一个递归定义的事物是回文。这个简单的模式适用于 Perl:

^((.)(?1)\2|.?)$

您可以使用以下方式在大多数系统上进行测试:

$ perl -nle 'print if /^((.)(?1)\2|.?)$/i' /usr/share/dict/words

请注意,PCRE 的递归实现需要更精细

^(?:((.)(?1)\2|)|((.)(?3)\4|.))

这是因为 PCRE 递归的工作方式受到限制。

正确解析

对我来说,上面的例子大多是玩具火柴,并不是所有的都很有趣,真的。当它变得有趣时,就是当你有一个真正的语法时,你正在尝试解析。例如,RFC 5322 相当详尽地定义了一个邮件地址。这是一个与之匹配的“语法”模式:

$rfc5322 = qr

   (?(DEFINE)

     (?<address>         (?&mailbox) | (?&group))
     (?<mailbox>         (?&name_addr) | (?&addr_spec))
     (?<name_addr>       (?&display_name)? (?&angle_addr))
     (?<angle_addr>      (?&CFWS)? < (?&addr_spec) > (?&CFWS)?)
     (?<group>           (?&display_name) : (?:(?&mailbox_list) | (?&CFWS))? ; (?&CFWS)?)
     (?<display_name>    (?&phrase))
     (?<mailbox_list>    (?&mailbox) (?: , (?&mailbox))*)

     (?<addr_spec>       (?&local_part) \@ (?&domain))
     (?<local_part>      (?&dot_atom) | (?&quoted_string))
     (?<domain>          (?&dot_atom) | (?&domain_literal))
     (?<domain_literal>  (?&CFWS)? \[ (?: (?&FWS)? (?&dcontent))* (?&FWS)?
                                   \] (?&CFWS)?)
     (?<dcontent>        (?&dtext) | (?&quoted_pair))
     (?<dtext>           (?&NO_WS_CTL) | [\x21-\x5a\x5e-\x7e])

     (?<atext>           (?&ALPHA) | (?&DIGIT) | [!#\$%&'*+-/=?^_`|~])
     (?<atom>            (?&CFWS)? (?&atext)+ (?&CFWS)?)
     (?<dot_atom>        (?&CFWS)? (?&dot_atom_text) (?&CFWS)?)
     (?<dot_atom_text>   (?&atext)+ (?: \. (?&atext)+)*)

     (?<text>            [\x01-\x09\x0b\x0c\x0e-\x7f])
     (?<quoted_pair>     \\ (?&text))

     (?<qtext>           (?&NO_WS_CTL) | [\x21\x23-\x5b\x5d-\x7e])
     (?<qcontent>        (?&qtext) | (?&quoted_pair))
     (?<quoted_string>   (?&CFWS)? (?&DQUOTE) (?:(?&FWS)? (?&qcontent))*
                          (?&FWS)? (?&DQUOTE) (?&CFWS)?)

     (?<word>            (?&atom) | (?&quoted_string))
     (?<phrase>          (?&word)+)

     # Folding white space
     (?<FWS>             (?: (?&WSP)* (?&CRLF))? (?&WSP)+)
     (?<ctext>           (?&NO_WS_CTL) | [\x21-\x27\x2a-\x5b\x5d-\x7e])
     (?<ccontent>        (?&ctext) | (?&quoted_pair) | (?&comment))
     (?<comment>         \( (?: (?&FWS)? (?&ccontent))* (?&FWS)? \) )
     (?<CFWS>            (?: (?&FWS)? (?&comment))*
                         (?: (?:(?&FWS)? (?&comment)) | (?&FWS)))

     # No whitespace control
     (?<NO_WS_CTL>       [\x01-\x08\x0b\x0c\x0e-\x1f\x7f])

     (?<ALPHA>           [A-Za-z])
     (?<DIGIT>           [0-9])
     (?<CRLF>            \x0d \x0a)
     (?<DQUOTE>          ")
     (?<WSP>             [\x20\x09])
   )

   (?&address)

x;

如您所见,这非常类似于 BNF。问题是它只是匹配,而不是捕获。你真的不想用捕获括号包围整个事情,因为这并不能告诉你哪个产品与哪个部分匹配。使用前面提到的 Regexp::Grammars 模块,我们可以。

#!/usr/bin/env perl

use strict;
use warnings;
use 5.010;
use Data::Dumper "Dumper";

my $rfc5322 = do 
    use Regexp::Grammars;    # ...the magic is lexically scoped
    qr

    # Keep the big stick handy, just in case...
    # <debug:on>

    # Match this...
    <address>

    # As defined by these...
    <token: address>         <mailbox> | <group>
    <token: mailbox>         <name_addr> | <addr_spec>
    <token: name_addr>       <display_name>? <angle_addr>
    <token: angle_addr>      <CFWS>? \< <addr_spec> \> <CFWS>?
    <token: group>           <display_name> : (?:<mailbox_list> | <CFWS>)? ; <CFWS>?
    <token: display_name>    <phrase>
    <token: mailbox_list>    <[mailbox]> ** (,)

    <token: addr_spec>       <local_part> \@ <domain>
    <token: local_part>      <dot_atom> | <quoted_string>
    <token: domain>          <dot_atom> | <domain_literal>
    <token: domain_literal>  <CFWS>? \[ (?: <FWS>? <[dcontent]>)* <FWS>?

    <token: dcontent>        <dtext> | <quoted_pair>
    <token: dtext>           <.NO_WS_CTL> | [\x21-\x5a\x5e-\x7e]

    <token: atext>           <.ALPHA> | <.DIGIT> | [!#\$%&'*+-/=?^_`|~]
    <token: atom>            <.CFWS>? <.atext>+ <.CFWS>?
    <token: dot_atom>        <.CFWS>? <.dot_atom_text> <.CFWS>?
    <token: dot_atom_text>   <.atext>+ (?: \. <.atext>+)*

    <token: text>            [\x01-\x09\x0b\x0c\x0e-\x7f]
    <token: quoted_pair>     \\ <.text>

    <token: qtext>           <.NO_WS_CTL> | [\x21\x23-\x5b\x5d-\x7e]
    <token: qcontent>        <.qtext> | <.quoted_pair>
    <token: quoted_string>   <.CFWS>? <.DQUOTE> (?:<.FWS>? <.qcontent>)*
                             <.FWS>? <.DQUOTE> <.CFWS>?

    <token: word>            <.atom> | <.quoted_string>
    <token: phrase>          <.word>+

    # Folding white space
    <token: FWS>             (?: <.WSP>* <.CRLF>)? <.WSP>+
    <token: ctext>           <.NO_WS_CTL> | [\x21-\x27\x2a-\x5b\x5d-\x7e]
    <token: ccontent>        <.ctext> | <.quoted_pair> | <.comment>
    <token: comment>         \( (?: <.FWS>? <.ccontent>)* <.FWS>? \)
    <token: CFWS>            (?: <.FWS>? <.comment>)*
                             (?: (?:<.FWS>? <.comment>) | <.FWS>)

    # No whitespace control
    <token: NO_WS_CTL>       [\x01-\x08\x0b\x0c\x0e-\x1f\x7f]
    <token: ALPHA>           [A-Za-z]
    <token: DIGIT>           [0-9]
    <token: CRLF>            \x0d \x0a
    <token: DQUOTE>          "
    <token: WSP>             [\x20\x09]
    x;
;

while (my $input = <>) 
    if ($input =~ $rfc5322) 
        say Dumper \%/;       # ...the parse tree of any successful match
                              # appears in this punctuation variable
    

如您所见,通过在模式中使用一个非常不同的符号,您现在可以在 %/ 变量中为您存储整个解析树,并且所有内容都被整齐地标记。正如=~ 运算符所见,转换的结果仍然是一个模式。就是有点神奇。

【讨论】:

左递归的限制绝对值得了解,但如果我没记错的话,严格来说它对“识别能力”没有影响,因为对于任何左递归语法,都有一个匹配相同语言的右递归语法——它可能会更麻烦。 @tobyodavies:我本可以进一步解释 PCRE 限制;它们与组的原子性有关:您不能在 PCRE 中对尚未完成的组调用递归,但在 Perl 中可以。语法 RFC 5322 模式在 PCRE 中应该同样适用;整个((DEFINE)…) 想法非常强大且有用,允许将声明(及其排序)与执行分离,就像所有自上而下的编程一样。我不记得还有哪些其他语言有组递归;它可能是像 C♯ 之类的异国情调的东西。

以上是关于“现代”正则表达式的识别能力的主要内容,如果未能解决你的问题,请参考以下文章

正则表达式中无法识别撇号 (')

正则表达式识别商店信用卡号

如何使用正则表达式识别特定行? C#

iOS实现简书的账号识别方式(正则表达式)

如何通过正则表达式识别文本中的段落?

复杂的正则表达式仅识别带括号的参数