it-swarm.com.ru

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

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

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

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

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

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

81
tobyodavies

Шаблон рекурсии

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

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

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

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

Кстати, PCRE и Perl немного отличаются в том, как вам разрешено формулировать рекурсию. См. Разделы "Рекурсивные шаблоны" и "Отличие рекурсии от Perl" в справочной странице pcrepattern . Например: 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
    }
}

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

102
tchrist