Как я могу изменить сложный XML-документ в Perl, чтобы добавить дополнительную разметку к текстовым узлам? - PullRequest
3 голосов
/ 12 мая 2011

У меня есть XML-документ, подобный этому:

<article>
  <author>Smith</author>
  <date>2011-10-10</date>
  <description>Article about <b>frobnitz</b>, crulps and furtikurty's. Mainly frobnitz</description>
</article>

Мне нужно проанализировать это в Perl, а затем добавить новые теги вокруг некоторых слов или фраз (например, для ссылки на определения). Я хочу пометить только первый экземпляр целевого слова и сузить область поиска только тем, что содержится в данном теге (например, только тег описания).

Я могу разобрать с XML :: Twig и установить «twig_handler» для тега описания. Но когда я вызываю $ node-> text , я получаю текст с удаленными тегами. На самом деле я хочу пройти по (очень маленькому) дереву, чтобы существующие теги были сохранены и не сломаны. Окончательный вывод XML должен выглядеть следующим образом:

<article>
  <author>Smith</author>
  <date>2011-10-10</date>
  <description>Article about <b><a href="dictionary.html#frobnitz">frobnitz</a></b>, <a href="dictionary.html#crulps">crulps</a> and <a href="dictionary.html#furtikurty">furtikurty</a>'s. Mainly frobnitz</description>
</article>

У меня также есть XML :: LibXML , доступный в целевой среде, но я не уверен, с чего начать ...

Вот мой минимальный тестовый пример. Цени любую помощь!

#!/usr/bin/perl
use strict;
use warnings;

use XML::Twig;

my %dictionary = (
    frobnitz    => 'dictionary.html#frobnitz',
    crulps      => 'dictionary.html#crulps',
    furtykurty  => 'dictionary.html#furtykurty',
    );

sub markup_plain_text { 
    my ( $text ) = @_;

    foreach my $k ( keys %dictionary ) {
        $text =~ s/(^|\W)($k)(\W|$)}/$1<a href="$dictionary{$k}">$2<\/a>$3/si;
    }

    return $text;
}

sub convert {
    my( $t, $node ) = @_;
    warn "convert: TEXT=[" . $node->text . "]\n";
    $node->set_text( markup_plain_text($node->text) );
    return 1;
}

sub markup {
    my ( $text ) = @_;

    my $t = XML::Twig->new(
        twig_handlers => { description => \&convert },
        pretty_print  => 'indented',
        );
    $t->parse( $text );

    return $t->flush;
}


my $orig = <<END_XML;
<article>
    <author>Smith</author>
    <date>2011-10-10</date>
    <description>Article about <b>frobnitz</b>, crulps and furtikurty's. Mainly frobnitz's</description>
</article>
END_XML
;

markup($orig);

1 Ответ

3 голосов
/ 12 мая 2011

Это немного сложнее, но XML :: Twig предназначен (и я активно его использую) для такого рода обработки. Таким образом, существует специальный метод, называемый mark, который принимает регулярное выражение и помечает совпадения.

В этом случае регулярное выражение, вероятно, будет довольно большим. Я использовал Regexp :: Assempble для его сборки, чтобы он был оптимизирован. Тогда другая проблема заключается в том, что mark не позволяет вам использовать текст соответствия для установки атрибута (я мог бы поработать над этим в следующей версии модуля, что было бы полезно), поэтому я должен отметить сначала , затем вернитесь назад и установите атрибут href во втором проходе (в любом случае второй проход необходим для «отмены связывания» слов, которые уже были связаны).

И последнее слово: я почти отказался от написания решения, потому что в данных вашего примера есть несколько опечаток. Нет ничего хуже, чем правильно составить код, просто увидеть, что тест по-прежнему не работает, потому что вы используете «словарь» в коде и «определения» в данных, или «furtykurtle», «furtikurty» и «furtijurty», где все должно быть быть тем же словом. Поэтому, пожалуйста, перед публикацией убедитесь, что ваши данные верны. К счастью, я писал код в качестве теста.

#!/usr/bin/perl 

use strict;
use warnings;

use XML::Twig;
use Regexp::Assemble;

use Test::More tests => 1; 
use autodie qw(open);

my %dictionary = (
    frobnitz    => 'definitions.html#frobnitz',
    crulps      => 'definitions.html#crulps',
    furtikurty  => 'definitions.html#furtikurty',
    );

my $match_defs= Regexp::Assemble->new()
                                ->add( keys %dictionary)
                                ->anchor_word
                                ->as_string;
# I am not familiar enough with Regexp::Assemble to know a cleaner
# way to get get the capturing braces in the regexp
$match_defs= qr/($match_defs)/; 

my $in       = data_para(); 
my $expected = data_para();
my $out;
open( my $out_fh, '>', \$out);


XML::Twig->new( twig_roots => { 'description' => sub { tag_defs( @_, $out_fh, $match_defs, \%dictionary); } },
                twig_print_outside_roots => $out_fh, 
              )
         ->parse( $in);

is( $out, $expected, 'base test');
exit;

sub tag_defs
  { my( $t, $description, $out_fh, $match_defs, $dictionary)= @_;

    my @a= $description->mark( $match_defs, 'a' );

    # word => 1 when already used in this description
    # this might need to have a different scope if you need to tag
    # only the first time the word appears in a section or whatever
    my $tagged_in_description; 

    foreach my $a (@a) 
      { my $word= $a->text;
        warn "checking a: ", $a->sprint, "\n";

        if( $tagged_in_description->{$word})
          { $a->erase; } # we did not need to tag it after all
        else
          { $a->set_att( href => $dictionary->{$word}); }
        $tagged_in_description->{$word}++;
      }

    $t->flush( $out_fh); }


sub def_href
  { my( $word)= @_;
    return $dictionary{word};
  }

sub data_para
  { local $/="\n\n";
    my $para= <DATA>;
    return $para;
  }

__DATA__
<article>
  <author>Smith</author>
  <date>2011-10-10</date>
  <description>Article about <b>frobnitz</b>, crulps and furtikurty's. Mainly frobnitz</description>
</article>

<article>
  <author>Smith</author>
  <date>2011-10-10</date>
  <description>Article about <b><a href="definitions.html#frobnitz">frobnitz</a></b>, <a href="definitions.html#crulps">crulps</a> and <a href="definitions.html#furtikurty">furtikurty</a>'s. Mainly frobnitz</description>
</article>
...