Porównanie parserów XML-a OSIOŁKOWI W ŻŁOBY DANO... XML jest obecnie najpopularniejszym formatem wymiany danych. Język posiada szeroki wachlarz metod jego obsługi. W tym miesiącu w dziale a omówimy zalety i wady najpopularniejszych modułów XML, aby pomóc podjąć decyzję wyboru najlepiej dopasowanego do potrzeb użytkownika. Wdziedzinie przetwarzania dokumentów XML z pewnością trzyma się swojego motta: jest więcej niż jeden sposób, aby to zrobić. Mamy naprawdę duży wybór modułów do obsługi XML-a. Przeanalizujemy różne metody podejścia tych modułów, wykorzystując przykład przedstawiony na Rysunku 1. Ten plik zawiera dwa rekordy typu <cd> zagnieżdżone w znaczniku <result>. Każdy z tych rekordów składa się ze znaczników <artists> i <title>, reprezentujących odpowiednio nazwy artystów i tytuł płyty CD. W polu <artists> może być więcej niż jeden znacznik <artist>. Siła prostoty Najprostszy sposób na załadowanie struktury plików XML w u polega na wykorzystaniu modułu XML::Simple dostępnego w archiwum CPAN. Moduł ten udostępnia funkcję XMLin, która wczytuje plik lub łańcuch znaków zawierający kod XML i zapisuje je w strukturze obiektów a: w zależności od liczby artystów w znacznikach <artists> wynikowa struktura zawierająca nazwy artystów może być skalarem lub macierzą. To znacznie utrudnia pracę use XML::Simple; my $ref = XMLin ("data.xml"); Rysunek 1: Przykładowe dane XML reprezentujące bazę muzycznych płyt CD. Przykład struktury zmiennej $ref po załadowaniu naszego pliku przedstawia Rysunek 2. Warto zauważyć dwie rzeczy: Rysunek 2: Struktura danych zastosowana przez XML::Simple do przechowywania danych z Rysunku 1. 70 NUMER 20 PAŹDZIERNIK 2005 WWW.LINUX-MAGAZINE.PL
PROGRAMOWANIE z tą strukturą. Można jednak wymusić, aby parser zawsze stosował macierze dla określonego pola struktury. Służy do tego opcja ForceArray. Wywołanie XMLin ( data.xml, ForceArray => ['artist']); powoduje, że $ref->{cd}->[0]->{artists}->{artist} Rysunek 3: Struktura XML uproszczona za pomocą opcji GroupTags modułu XML::Simple. zawsze zwróci referencję macierzy, nawet gdy w źródle danych występuje tylko jeden artysta. Po drugie, składnia ->{artists}- >{artist} jest nieco niewygodna, ponieważ ->{artists} nie zawiera innych elementów niż ->{artist}. XML::Simple obsługuje Listing 1: xptitles 03 use XML::LibXML; 05 my $x = XML::LibXML->new() 06 or die "new failed"; 07 08 my $d = 09 $x->parse_file("data.xml") 10 or die "parse failed"; 12 my $titles = 13 "/result/cd/title/text()"; 15 for my $title ( 16 $d->findnodes($titles) ) { 17 print $title->tostring(), 18 "\n"; 19 } opcję GroupTags, pozwalającą programistom nieco uprościć wygenerowaną strukturę. Poniższy kod wygeneruje strukturę przedstawioną na Rysunku 3, która jest już znacznie prostsza w obsłudze. XMLin("data.xml", ForceArray => ['artist'], GroupTags => {'artists' => 'artist'}); W tej strukturze możemy zastosować na przykład prostą pętlę for wyszukującą numery seryjne płyt: for my $cd (<\@>{$ref->{cd}}) { print $cd-> {serial}, "<\\>n"; } XML::Simple ładuje cały plik XML do pamięci, co jest bardzo wygodne przy mniejszych plikach. Jeśli jednak mamy do czynienia z dość dużym plikiem XML, to podejście okaże się nieoptymalne, ponieważ może spowodować przepełnienie pamięci programu. Pokrętne ścieżki Wielbiciele zawiłych składni pokochają XPath. Moduł XML::LibXML z archiwum CPAN opiera się na bibliotece libxml2 związanej z projektem Gnome. Moduł ten pozwala zastosować znaną z XPath notację findnodes, stosowaną do wyszukiwania elementów Notacja Xpath wyszukująca zawartość tekstową wszystkich elementów <title> jest następująca /result/cd/title/text(): rozpoczynamy od korzenia dokumentu /, wspinamy się na gałąź <results>, <cd> i <title>, aby na końcu wywołać text(), co zwróci zawartość tekstową elementu. Można też alternatywnie zastosować składnię //title/text(), informującą XPath, że ma wykryć wszystkie elementy <title> niezależnie od tego, w którym miejscu hierarchii XML się znajdują. Skrypt xptitles z Listingu 1 demonstruje, że metoda findnodes() zwraca listę obiektów tekstowych, z których metoda tostring() w końcu pozwala odczytać poszukiwane wartości tekstowe. XPath potrafi nieźle rozwiązywać również trudniejsze zadania. Listing 2 prezentuje sposób odczytania wszystkich numerów seryjnych dysków CD, które w polach <artist> zawierają tekst Foo Fighters. Zastosowana w tym celu ścieżka /result/cd/artists/artist[.="foo Fighters"]/../../<\@>serial powoduje, że najpierw wspinamy się do znaczników <artist>, które są sprawdzane na obecność poszukiwanego tekstu za pomocą predykatu [.= Foo Fighters ]. Kropka określa bieżący węzeł w ścieżce. Jeśli w tym węźle zostanie odnaleziony poszukiwany tekst Foo Fighters, XPath przechodzi dwa poziomy wyżej w hierarchii../... Tutaj znajdują się węzły <cd>. Za pomocą <\@>serial pobierany jest obiekt atrybutu serial i zwracany jako wynik wywołania XPath. Listing 2 (xpserial) przedstawia cały skrypt, który z obiektu wynikowego wyciąga jego wartość (numer seryjny płyty CD) za pomocą metody value(). XPath umożliwia również zastosowanie notacji uproszczonej, lecz jeśli wystąpi problem z plikiem źródłowym, wyszukiwanie przyczyny może być dość utrudnione. Można jednak stwierdzić, że połączenie a i XPath pomimo wad jest z pewnością warte zastosowania, ponieważ daje dostęp do skutecznych technik XPath konstruowania solidnej logiki programu oraz zapenia doskonałe możliwości wyszukiwania błędów. W porównaniu z tym zastosowanie najprostszego nawet procesora XSLT wiąże się ze Listing 2: xpserial 03 use XML::LibXML; 05 my $x = XML::LibXML->new() 06 or die "new failed"; 07 08 my $d = 09 $x->parse_file("data.xml") 10 or die "parse failed"; 12 my $serials = q{ 13 /result/cd/artists/ artist[.="foo Fighters"]/ 15../../@serial 16 }; 17 18 for my $serial ( 19 $d->findnodes($serials) ) { 20 print $serial->value(), 21 "\n"; 22 } WWW.LINUX-MAGAZINE.PL NUMER 20 PAŹDZIERNIK 2005 71
moduł XML::Parser, może zainstalować moduł XML::SAX::Pure, który również można znaleźć w repozytorium CPAN. Warto pamiętać, że to rozwiązanie nie należy do najszybszych, lecz można je zainstalować bez konieczności posiadania kompilatora języka C. Instalacja modułu XML::Parser zajmuje dłuższą chwilę, poznacznie większymi problemami. XML::Parser Moduł XML::Parser implementuje bardziej klasyczny parser. Przekopuje się przez dokument XML znacznik po znaczniku i, gdy są spełnione określone warunki, wywołuje zdefiniowane przez użytkownika funkcje zwrotne (callback). Aby wyszukać numery seryjne płyt CD, w których nazwa artysty zawiera tekst Foo Fighters, należy na bieżąco kontrolować stan parsera już na etapie analizy drzewa Listing 3 xmlparse zawiera w wywołaniu Listing 3: xmlparse 03 use XML::Parser; 05 my $p = XML::Parser->new(); 06 $p->sethandlers( 07 Start => \&start, 08 Char => \&text, 09 ); 10 $p->parsefile("data.xml"); 12 my $serial; 13 my $is_artist; 15 ############################# 16 sub start { 17 ############################# 18 my ($p, $tag, %attrs) = @_; 19 20 if ( $tag eq "cd" ) { 21 $serial = $attrs{serial}; 22 } 23 24 $is_artist = 25 ( $tag eq "artist" ); 26 } 27 28 ############################# 29 sub text { 30 ############################# 31 my ( $p, $text ) = @_; 32 33 if ( $is_artist and 34 $text eq 35 "Foo Fighters" ) { 36 print "$serial\n"; 37 } 38 } konstruktora new () XML::Parser wskazanie funkcji zwrotnych dla parsera dla zdarzeń Start (gdy parser napotka otwierający znacznik XML) oraz Char (gdy parser napotka tekst pomiędzy znacznikami). Gdy parser napotka znacznik otwierający, jak <cd serial= 001 >, wywoła funkcję zwrotną start(), przekazując jej referencję parsera, nazwę znacznika oraz listę atrybutów w postaci par kluczy i wartości. W naszym przykładzie funkcji start () w drugim parametrze przekazywany jest ciąg znaków cd. Trzeci i czwarty parametr to odpowiednio ciągi znaków serial i 001. Funkcja zwrotna text () jest zdefiniowana w wierszu 29. Gdy parser znajdzie wartość tekstową, wywołuję tę funkcję z dwoma parametrami: referencją do parsera i ciągiem znaków reprezentującym znaleziony tekst. Aby parser wiedział, czy znaleziony tekst zawiera nazwę artysty (a nie inny ciąg znaków), musi śledzić swój stan, a w szczególności sprawdzić, czy przetwarzanie znajduje się wewnątrz znacznika <artist>. Jedyny sposób, aby parser mógł to stwierdzić, polega na zastosowaniu zmiennej globalnej $is_artist, której przypisywana jest wartość prawdziwa w przypadku, gdy otwierany jest znacznik <artist>. Zmienna globalna $serial wykorzystuje to samo podejście: zapisuje wartość numeru seryjnego w przypadku, gdy funkcja start() znajdzie atrybut serial znacznika <cd>. Dzięki temu funkcja print() w funkcji zwrotnej text() wypisuje prawidłowy numer seryjny aktualnie przetwarzanej płyty CD. To podejście zakłada, że każda płyta CD ma zdefiniowany atrybut <serial>, lecz tego możemy dopilnować, stosując kontrole poprawności składni bazy, na przykład za pomocą DTD. Modułu XML::Parser z reguły nie stosuje się bezpośrednio, lecz jako klasę bazową dla własnej klasy użytkownika. W rzeczywistości omówiony wcześniej XML::Simple może niejawnie wykorzystywać XML::Parser, jest to uzależnione od środowiska instalacji. Jeśli XML::Parser jest zainstalowany, lecz XML::Simple go nie wykorzystuje, można go do tego nakłonić, umieszczając w skrypcie klauzulę $XML::Simple::PREFERRED_PARSER = XML::Parser ;. W przypadku, gdy użytkownik pracuje na platformie, dla której nie jest dostępny Listing 4: htmlparse 03 use HTML::Parser; 05 my $p = HTML::Parser->new( 06 api_version => 3, 07 start_h => [ 08 \&start, "tagname, attr" 09 ], 10 text_h => [ \&text, "dtext" ], 12 xml_mode => 1, 13 ); 15 $p->parse_file("data.xml") 16 or die "Nie można przetworzyć"; 17 18 my $serial; 19 my $artist; 20 21 ############################# 22 sub start { 23 ############################# 24 my ( $tag, $attrs ) = @_; 25 26 if ( $tag eq "cd" ) { 27 $serial = 28 $attrs->{serial}; 29 } 30 31 $artist = 32 ( $tag eq "artist" ); 33 } 34 35 ############################# 36 sub text { 37 ############################# 38 my ($text) = @_; 39 40 if ($artist and 41 $text eq 42 "Foo Fighters" ) { 43 print "$serial\n"; 44 } 45 } 72 NUMER 20 PAŹDZIERNIK 2005 WWW.LINUX-MAGAZINE.PL
PROGRAMOWANIE nieważ do pracy potrzebuje działającej instalacji biblioteki expat. Aby uniknąć konieczności instalowania tych wszystkich bibliotek, można do pracy wykorzystać HTML::Parser lub inną bibliotekę dostępną w archiwum CPAN. Warunki są dwa: składnia nie może wiele się różnić i musi istnieć możliwość modyfikacji trybu xml_mode z nierestrykcyjnej analizy kodu HTML na bardziej wymagający tryb niezbędny w przypadku Kiepskie narzędzia, poprawne wyniki Jeśli przyjrzeć się skryptowi htmlparse z Listingu 4, można zauważyć, że konstruktor HTML::Parser oczekuje nieco innej składni niż jego odpowiednik w XML::Parser. Po zdefiniowaniu wersji API należy podać Listing 5: twig 03 use XML::Twig; 05 my $twig = XML::Twig->new( 06 TwigHandlers => { 07 "/result/cd/artists/artist" 08 => \&artist 09 } 10 ); 12 $twig->parsefile("data.xml"); 13 ############################# 15 sub artist { 16 ############################# 17 my ( $t, $artist ) = @_; 18 19 if ( $artist->text() eq 20 "Foo Fighters" ) { 21 my $cd = 22 $artist->parent() 23 ->parent(); 24 25 print $cd->att('serial'), 26 "\n"; 27 } 28 29 # Zwolnienie pamięci zajętej przez 30 # przetworzone drzewo 31 $t->purge(); 32 } parametry start_h i text_h, które definiują funkcje zwrotne dla znacznika otwierającego element oraz dla tekstu poza znacznikami Konstruktor określa również parametry parsera, które mają być obsłużone Rysunek 4: Wynik działania skryptu twigfilter (zmodyfikowana struktora XML) przez funkcje zwrotne: start () otrzyma nazwę otwierającego znacznika i listę atrybutów (w postaci referencji do tablicy), natomiast funkcja text () otrzyma po prostu odnaleziony tekst. Naginanie gałązki Moduł XML::Twig autorstwa Michela Rodriguez stanowi niezwykle efektywny sposób przeprowadzenia odwzorowania struktur danych XML na struktury danych a. Potrafi przetworzyć dokumenty XML tak monstrualnych rozmiarów, przy których XML::Simple po prostu nie daje rady. Dzieje się tak dzięki temu, że zamiast ładować cały plik do pamięci, XML::Twig przetwarza go małymi kawałkami. XML::Twig posiada tak wiele metod nawigacji, że określenie najwygodniejszej dla danego problemu może okazać się trudnym zadaniem. Skrypt twig z Listingu 5 wywołuje konstruktor XML::Twig::new() z parametrem Twighandlers, który powoduje, że gałąź struktury XML /result/cd/artists/artist jest odwzorowywana na funkcję obsługi artist() zdefiniowaną w wierszu 15. Gdy parser XML::Twig napotka znacznik <artist>, wywoła funkcję artist z dwoma parametrami. Pierwszym z nich jest obiekt klasy XML::Twig, drugim jest obiekt XML::Twig::Elt (najwyraźniej Elt to skrót od element ). Ten drugi parametr reprezentuje węzeł w drzewie XML, bezpośrednio do którego jest zaczepiony znacznik <artist>. Metoda text () obiektu XML::Twig::Elt zwraca tekst znajdujący się pomiędzy początkowym a końcowym znacznikiem <artist>. Jeśli tekst ten zawiera ciąg znaków Foo Fighters, wiersze 23 i 24 przejdą w hierarchii dwa poziomy w górę, wywołując dwukrotnie metodę parent(). Odszukany w ten sposób obiekt informacji o płycie CD jest następnie za pomocą metody att() odpytany o dostępność atrybutu serial, którego wartość jest następnie wypisywana. Po przetworzeniu znacznika artist w wierszu 31 jest wywoływana metoda purge(), która zwalnia pamięć wykorzystywaną przez drzewo XML do gałęzi, w której obiekt aktualnie się znajduje. XML::Twig jest wystarczająco inteligentny, aby nie usuwać bezpośrednich przodków bieżącego węzła, lecz usunie rodzeństwo, które zostało już w pełni przetworzone. Ten typ zarządzania pamięcią nie ma większego sensu przy tak małym drzewie XML jak przykładowe, lecz przy gigantycznych dokumentach może być kwestią życia lub śmierci. XML::Twig cechuje się nie tylko eleganckimi funkcjami nawigacyjnymi, skrypt może też zmieniać nazwy znaczników, wywoływać metody dynamicznie zmieniające drzewo, a nawet odrzucające jego fragmenty dla oszczędności pamięci. Weźmy na przykład skrypt twigfilter zli- Listing 6: twigfilter 03 use XML::Twig; 05 my $twig = 06 XML::Twig->new( 07 PrettyPrint => "indented"); 08 09 $twig->parsefile("data.xml") 10 or die "Błąd parsowania"; 12 my $root = $twig->root(); 13 for my $cd ( 15 $root->children('cd') ) { 16 $cd->att_to_field( 17 'serial', 'id' ); 18 $cd->first_child('artists') 19 ->delete(); 20 $cd->set_gi("compactdisc"); 21 } 22 23 $root->print(); WWW.LINUX-MAGAZINE.PL NUMER 20 PAŹDZIERNIK 2005 73
Rysunek 5: Zapytania XPath w interaktywnej powłoce xsh. stingu 6, który zastępuje składnię atrybutów serial='xxx' znacznika cd z postaci <cd serial= xxx >... </cd> na czytelniejszą postać <cd><id>xxx</id>... </cd>, usuwając przy okazji informacje o artyście. W tym celu skrypt wykorzystuje metodę root() odczytującą obiekt korzenia (<results>). Następnie metoda children() zwraca wszystkich potomków obiektu korzenia, to znaczy elementy cd. Metoda att_to_field() przekształca atrybuty serial elementów cd na samodzielne elementy id. W tym momencie metoda first_child() zwraca już tylko jeden element artist. Metoda delete() tego elementu unicestwia węzeł i usuwa go z drzewa. Na końcu metoda set_gi() (gi to skrót od generic identifier) zmienia nazwę obiektu cd powstałego w wy- niku parsowania znacznika <cd> na nazwę <CompactDisc>. Rysunek 4 przedstawia wynik działania tego skryptu. Parametr PrettyPrint konstruktora o wartości indented powoduje, że funkcja print () wywoływana w wierszu 23 wypisze reprezentację drzewa XML w estetycznie sformatowanej postaci. Moduł XML::Twig daje programistom możliwość pisania niezwykle zwartych programów. Należy jedynie poćwiczyć chwilę, aby nauczyć się odpowiednich technik pracy z tym modułem. XML::XSH Zwolennicy rozwiązań interaktywnych zainteresują się zapewne trybem powłoki xsh modułu XML::XSH. Wywołanie xsh otwiera interpreter tekstowy, w którym można wczytywać dokumenty zapisane na dysku twardym, a nawet odczytywać je bezpośrednio z WWW. Następnie można na tych załadowanych strukturach wykonywać dowolne żądania XPath. Wyniki są wypisywane na ekranie, można więc na bieżąco korygować wywoływane zapytania. Rysunek 5 przedstawia przykładową sesję powłoki: załadowanie dokumentu z dysku twardego (poleceniem open doca = data.xml ), po czym następuje wywołanie ls uruchamiające zapytanie XPath. Wynikiem tego zapytania jest jeden numer seryjny serial='002'. Przedstawiłem tu zaledwie kilka wybranych przykładów z ogromnej kolekcji modułów do obróbki formatu XML dostępnych w archiwum CPAN. XML::XPath, XML::DOM, XML::Mini, XML::SAX i XML::Grove są zaledwie przykładami nieskończonych możliwości programistów języka dotyczących obróbki dokumentów INFO [1] Listingi dla tego artykułu: http://www.linux-magazine.com/magazine/downloads/58/ [2] Podręcznik modułu XML::Twig: http://www.xmltwig.com/xmltwig/tutorial/index.html 74 NUMER 20 PAŹDZIERNIK 2005 WWW.LINUX-MAGAZINE.PL