Синтаксический анализ XML
Скорее всего можно было бы написать другую тему для этой статьи. Например: "когда можно применять грамматики, а когда нет" или "не всегда грамматики подходят", или вот такую модную "эти грамматики - они не всегда серебрянные". Но речь пойдет о обработке XML, в том числе с помощью грамматик Perl 5.
Задачей процесса синтаксического анализа текста почти всегда является построение дерева объектов. Каждый из объектов ассоциируется с той или иной сущностью обрабатываемого текста. Например, если производится обработка текста языка программирования, то объектами в итоге становятся операторы, подпрограммы, циклы или арифметические операторы. Поэтому, какие бы не взяли инструменты синтаксического анализа, в каждом присутствует механизм построения деревьев.
У меня возникла ситуация, когда грамматики оказались не самым оптимальным с точки зрения простоты решением. Конечно же важную роль играют исходные условия, а они такие: необходим синтаксический анализ XML. Ниже приведу пример такого XML:
<wd>
<object class="isauth" id="auth_switch">
<auth>
<object class="comp_unauth" id="ExitCP"/>
</auth>
</object>
</wd>
Тэг wd
является корневым, а его содержимое должно в итоге отобразиться в виде некой структуры в памяти:
<object class="isauth" id="auth_switch" >
└── auth
└──<object class="comp_unauth" id="ExitCP">
Решение с помощью грамматик Perl 5
Итоговое решение с помощью грамматик Perl 5 получилось следующее:
use Regexp::Grammars;
my $q = qr{
\A <File> \Z
# Описание блока целиком
# <wd>...</wd>
<rule: File><start_block> <[childs]>+ <end_block>
<rule: start_block>\<wd>
<rule: end_block>\<\/wd>
#Допустимые тэги
<token: childs>
<MATCH=object> |
<MATCH=any_tag>
#objrule - создает объект указанного класса
<objrule: WebDAO::Lexer::object> <tag( tagname=>'object' )>
<objrule: WebDAO::Lexer::any_tag> <tag>
<rule: attribute> <name=([_\w]+)>=['"]<value=(?: ([^'"]+) )>['"]
#argument :tagname
<rule: tag>
<matchpos>
<matchline>
(?{
$ARG{'tagname'} =
defined ($ARG{'tagname'})
? quotemeta $ARG{'tagname'}
: '(\w+)';
# setup defaults
$MATCH{childs} //=[];
$MATCH{attribute} //=[];
})
(?:
< <tagname=:tagname>
<[attribute]>* />
)
|
(?:
< <tagname=:tagname> <[attribute]>* >
<[childs]>*
</ <:tagname> >
)
}xms;
Для описания отдельно взятого тэга в примере используется правило rule:tag
. Грамматики Perl 5 предоставляют возможность передачи параметров для правил. Эта особенность позволяет точно описать допустимые тэги:
#Допустимые тэги
<token: childs>
<MATCH=object> |
<MATCH=any_tag>
# правило для тэга 'object'
<objrule: WebDAO::Lexer::object> <tag( tagname=>'object' )>>
#если параметр tagname не указан, то допускается любой тэг
<objrule: WebDAO::Lexer::any_tag> <tag>
Параметр tagname используется в описании тэга XML для двух допустимых форм:
(?:
# пустой тэг
< <tagname=:tagname> <[attribute]>* />
)
|
(?:
< <tagname=:tagname> <[attribute]>* >
# допускаются вложенные тэги
<[childs]>*
</ <:tagname> >
)
В случае, если правило совпадает, то создается объект указанного класса:
<objrule: WebDAO::Lexer::object> <tag( tagname=>'object' )>
<objrule: WebDAO::Lexer::any_tag> <tag>
Итоговое дерево доступно после применения грамматики к исходному тексту:
if ( $txt =~ $q ) {
#результат в переменной %/
return {%/}->{File}->{childs};
}
Наличие параметров для правил позволяет сократить количество кода и делает грамматики отдельным языком программирования.
Решение с помощью XML::Flow
Так как целью является синтаксическое дерево и простота решения, то здесь пригодилась библиотека XML::FLow для работы с XML [1]:
sub parse {
my $self = shift;
my $txt = shift || return [];
#Соответствие тэгов классам
my %classmap = (
object => 'WebDAO::Lexer::object',
default => 'WebDAO::Lexer::any_tag',
);
our $result = [];
# обработчики тэгов
my %tags = (
# корневой тэг
wd => sub { shift; $result = \@_ },
# * обработчик для любого тэга XML
'*' => sub {
my $name = shift;
my $a = shift;
my $childs = \@_;
my $class = $classmap{$name} || $classmap{'default'};
#создаем экземпляр класса
my $o = $class->new(
name => $name,
attr => $a,
childs => $childs
);
#и возвращаем его как результат обработчика
return $o;
}
);
my $rd = new XML::Flow:: \$txt;
$rd->read( \%tags );
$result;
}
Несмотря на то, что основное предназначение библиотеки - это серилизация структур данных в XML, есть и дополнительная возможность - синтаксическая обработка XML в потоковом режиме.
Принцип работы следующий: встретив закрывающий тэг XML, вызывается обработчик, которому в качестве параметров передаются атрибуты тэга и результаты обработчиков вложенных в него тэгов.
Например, для следующего XML:
<wd>
<object>
<auth />
</object>
</wd>
Схема вызовов будет следующей:
call_wd (
call_object (
call_auth()
)
)
Как видно результаты вызовов вложенный тэгов будут в конечном итоге переданы в обработчик корневого тэга и ему останется только сохранить их в переменной:
# корневой тэг
wd => sub { shift; $result = \@_ },
А так как обработчики тэгов возвращают созданные объекты, то в переменную $result
сохранится уже готовое дерево.
Итоги обоих решений
Решение с помощью XML::Flow
получилось простым главным образом из-за наличия уже готовой библиотеки для работы с XML. Но это не уменьшает значимость грамматик в Perl 5 и вообще средств синтаксического анализа.
Если учесть прекращение развития XHTML и вообще общую тенденцию ухода от XML, то будущее однозначно за грамматиками !
[1] Библиотека XML::Flow. http://search.cpan.org/perldoc?XML::Flow