Признавательная способность "современных" регулярных выражений

Какой класс языков действительно распознает реальные современные регулярные выражения?

Всякий раз, когда существует группа захвата неограниченной длины с обратной ссылкой (например, (.*)_\1), регулярное выражение теперь соответствует нерегулярному языку. Но этого само по себе недостаточно, чтобы сопоставить что-то вроде S ::= '(' S ')' | ε - контекстно-свободный язык совпадающих пар паренов.

Рекурсивные регулярные выражения (которые для меня новы, но я уверен, существуют в Perl и PCRE), по-видимому, распознают как минимум большинство CFL.

Кто-нибудь сделал или прочитал какие-либо исследования в этой области? Каковы ограничения этих "современных" регулярных выражений? Знают ли они строго больше или строго меньше, чем CFG, грамматик LL или LR? Или существуют ли оба языка, которые могут быть распознаны регулярным выражением, но не CFG и наоборот?

Ссылки на соответствующие документы будут высоко оценены.

Ответ 1

Рекурсия шаблона

С рекурсивными шаблонами у вас есть форма рекурсивного сопоставления спуска.

Это хорошо для множества проблем, но как только вы захотите провести рекурсивный анализ спуска, вам нужно вставить группы захвата здесь и там, и неудобно восстанавливать всю структуру разбора таким образом. Damian Conways Модуль Regexp:: Grammars для Perl преобразует простой шаблон в эквивалентный, который автоматически делает все, что назвал захват, в рекурсивную структуру данных, делая для гораздо более легкого поиска анализируемой структуры. У меня есть образец, сравнивающий эти два подхода в конце этой публикации.

Ограничения на рекурсию

Вопрос в том, какие типы грамматик могут соответствовать рекурсивным шаблонам. Ну, theyre, конечно, рекурсивный спуск. Единственное, что приходит в голову, это то, что рекурсивные шаблоны не могут обрабатывать левую рекурсию.. Это ставит ограничение на типы грамматики, к которым вы можете применить их. Иногда вы можете изменить порядок своих постановок, чтобы исключить левую рекурсию.

BTW, PCRE и Perl немного отличаются от того, как вам разрешено выражать рекурсию. См. Разделы "RECURSIVE PATTERNS" и "Recursion difference from Perl" в man-странице pcrepattern. например: Perl может обрабатывать ^(.|(.)(?1)\2)$, где PCRE требует ^((.)(?1)\2|.)$.

Демонстрации рекурсии

Потребность в рекурсивных шаблонах возникает неожиданно часто. Один пример, который вы посещаете, - это когда вам нужно сопоставить что-то, что можно вложить, например, сбалансированные круглые скобки, кавычки или даже теги HTML/XML. Heres соответствует для balenced parens:

\((?:[^()]*+|(?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 довольно точно определяет почтовый адрес. Heres "грамматический" шаблон, чтобы соответствовать ему:

$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
    }
}

Как вы видите, используя очень немного отличающуюся нотацию в шаблоне, теперь вы получаете то, что хранит все дерево синтаксического анализа для вас в переменной %/ со всем, что аккуратно помечено. Результат преобразования по-прежнему является шаблоном, как вы можете видеть оператором =~. Его просто немного волшебный.