Признательная сила "современных" регулярных выражений - PullRequest
81 голосов
/ 30 января 2011

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

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

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

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

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

1 Ответ

102 голосов
/ 30 января 2011

Pattern Recursion

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

Это хорошо для множества проблем, но как только вы захотите выполнить рекурсивный спуск парсинг , вам нужно вставить группы захвата тут и там, и будет неудобно восстанавливать полную структуру разбора в сюда. Модуль 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;

Как видите, это очень похоже на БНФ. Проблема в том, что это просто матч, а не захват. И вы действительно не хотите просто окружать все это захватом паренов, потому что это не говорит вам, какое производство соответствовало какой части. Используя ранее упомянутый модуль 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
    }
}

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

...