r 23 01 EQ72I5FHOBQHWQUKTDL3FPVCF7E6AI26E7IE3CI


23. XML i libxml
Ostatnio najmodniejszym tematem jest XML, czyli eXtensible Markup Language. Jeżeli
spojrzymy do którejkolwiek gazety  komputerowej , to znajdziemy wzmiankę o XML, często w
połączeniu ze skrótami SAX, XSLT, DOM, DTD i innymi. Przeglądając katalogi książek również
można natrafić na bardzo wiele pozycji poświęconych XML i tematom pokrewnym.
Podczas wstępnych rozmów z wydawnictwem Wrox omawialiśmy koncepcję i zawartość tej
książki oraz przykładową aplikację, która miała posłużyć jako szkielet pomagający pokazać
metody omawiane w każdym z rozdziałów. Aby ta aplikacja działała, potrzebne były jakieś
przykładowe dane do katalogu wypożyczalni płyt DVD. Wydawnictwo natychmiast wysłało nam
dane (podziękowania dla DanM!) w formacie XML. Oto początkowy fragment tego pliku,
pokazany dla oddania atmosfery (przy okazji: ceny nie są poprawne):



asin CDATA #REQUIRED >






]>


Grand Illusion
29.99
Jean Renoir

Jean Gabin

1938


Seven Samurai
27.99
Akira Kurosawa

Takashi Shimura
Toshiro Mifune

1954

...

Były to rzeczywiście dobre dane przykładowe i nie byliśmy zbytnio zdumieni formatem przekazu,
ale w jaki sposób mieliśmy je przekształcić na postać użyteczną w naszej bazie danych?
Początkowo zakładaliśmy, że dane będą dostarczone w postaci pliku z polami oddzielonymi za
pomocą przecinków (tzw. format CSV) lub w innym formacie, łatwo przyswajalnym przez bazę
danych.
Oczywiście, można było napisać nowy program w języku C (lub w języku Python, Perl... itd.),
który zmieniałby format zapisu danych. Myśleliśmy także o zastosowaniu programów flex i
bison wspomagających obsługę składni. Zastanawialiśmy się nad napisaniem programu
przekształcającego w postaci arkusza stylu (XSL), próbując rozgryzć ten plik za pomocą programu
awk lub wyrażeń regularnych języka Perl...
Doszliśmy w końcu do wniosku, że... dokonanie  poprawnego rozbioru XML będzie możliwe za
pomocą parsera XML. Po cóż zaś budować własny parser, jeśli istnieje kilka eleganckich i
bezpłatnych programów? Ich zastosowaniem zajmiemy się w tym rozdziale.
Opiszemy tutaj następujące zagadnienia:
Krótki przegląd dokumentów XML i sposoby ich definiowania.
Przegląd zastosowań niektórych programów wspomagających przetwarzanie
dokumentów XML w systemie Linux.
Omówienie wywołań zwrotnych programu SAX zastosowanych do pobierania danych z
dokumentu XML.
Struktura dokumentu XML
Zanim zajmiemy się problemami odzysku danych z naszego dokumentu dvdcatalog.xml,
powinniśmy dokładnie poznać czym jest XML i jak są tworzone dokumenty XML.
Składnia XML
Na pierwszy rzut oka dokumenty XML wyglądają bardzo podobnie do dokumentów HTML,
ponieważ zawierają znaczniki, atrybuty znaczników oraz dane między znacznikami. Takie
podobieństwo wynika z tego, że zarówno HTML, jak i XML, wywodzą się ze wspólnego zródła,
czyli z SGML (Standard Generalized Markup Language). XML jest podzbiorem SGML i
mimo podobieństwa do HTML, występują między nimi bardzo ważne różnice:
HTML jest używany głównie dla celów zobrazowania informacji.
Niezależnie od tego, że pierwotne wersje HTML koncentrowały się wokół opisu elementów
dokumentu (np.  to jest nagłówek ), to pózniej zaczęto stosować wiele znaczników niosących
informację o sposobie wyświetlania danych, nie rozszerzając jednak ich znaczenia na definicje
faktycznej zawartości dokumentu. Znaczniki w XML nie zawierają zaś informacji o sposobie
prezentacji dokumentu  natomiast mówią o tym, czym są w istocie przekazywane w nim dane.
Oczywiście, możemy użyć znaczenia danych do określenia sposobu ich prezentacji, ale mimo
wszystko jest to bardzo istotne rozróżnienie. Spójrzmy na pierwszy film opisany w naszym
przykładowym dokumencie. Na stronie HTML opisującej ten film moglibyśmy widzieć
wyróżniony tekst  Jean Renoir i  Jean Gabin , oznaczający osoby. Bez informacji kontekstowej
nie można byłoby jednak powiedzieć, kto jest aktorem, a kto reżyserem. W XML możemy
oznaczyć te pola właśnie odpowiednio jako reżysera i jako aktora, czyli przekazać informację o
ich znaczeniu.
Dokumenty HTML nie są spełniają wymagań XML.
Odnosi się to nawet do tych dokumentów, które są zgodne z definicjami HTML w wersji 4.
Została zdefiniowana nowa wersja HTML oznaczana skrótem XHTML, która zapewnia
równoczesną zgodność definicji XHTML i XML.
W znacznikach XML jest brana pod uwagę wielkość liter.
W HTML znacznik

jest traktowany tak samo jak znacznik

, lecz w XML są to
całkowicie różne znaczniki. W języku angielskim nieco dziwna wydaje się zamiana wielkich liter
na małe, ale w innych językach uzależnienie XML od wielkości liter jest przyjmowane w sposób
naturalny i pozwala uniknąć wielu pułapek spotykanych przy automatycznej konwersji. Dane
XML nie ograniczają się tylko do zestawu znaków ASCII, można w nich stosować pełny zestaw
UNICODE jeżeli tylko jest to potrzebne. Nie wolno tylko używać znaczników, których nazwy
rozpoczynają się od xml lub xsl, niezależnie od wielkości użytych liter. Wszystkie nazwy, które
tak się rozpoczynają, są zarezerwowane przez World Wide Web Consortium (W3C), czyli przez
komitet normujący XML.
Dobrze sformatowany dokument XML
Podobnie jak najnowsze wersje standardu HTML, również i standardy dla dokumentów XML są
ściśle określone za pomocą reguł składniowych opracowanych przez World Wide Web
Consortium (dalej nazywane W3C). Można się z nimi zapoznać w materiałach zródłowych
wymienionych na końcu tego rozdziału. Dokument XML musi spełniać te wymagania, aby można
było go nazwać dokumentem  dobrze sformatowanym . Jeżeli te reguły nie są spełnione, to nie
jest to dokument XML.
W tym rozdziale omówimy skrótowo reguły składni XML, których należy przestrzegać.
Sekcje
Każdy dokument XML składa się z trzech sekcji (a nie tak jak HTML  z dwóch). Sekcje te
zostały nazwane: prolog, treść i epilog (faktycznie w standardzie nie użyto nazwy epilog).
Jedynie sekcja treść jest obowiązkowa, pozostałe dwie nie muszą występować w dokumencie.
Prolog
Pierwsza sekcja dokumentu XML stanowiąca prolog  może i powinna rozpoczynać się od
deklaracji XML (to cytat wzięty z oficjalnej normy). Pomimo tego, że przed chwilą
wspomnieliśmy o braku przymusu użycia tej sekcji, to dokument normatywny zaleca użycie jej
przynajmniej w minimalnej postaci we wszystkich dokumentach XML. Deklaracja XML wygląda
następująco:

Jak łatwo odgadnąć, deklaracja ta zawiera nie tylko informację, że dokument jest dokumentem
XML, ale także, że spełnia określoną wersję specyfikacji XML (w tym przypadku 1.0). Można
także podać w tej deklaracji specyfikację języka oraz informacje dodatkowe, mówiące
czytelnikowi (człowiekowi lub komputerowi), czy do interpretacji XML wymagane są jakieś
dokumenty zewnętrzne. W naszym przykładzie wygląda to następująco:

Mamy tu informację, że w dokumencie użyto znaków ośmiobitowych zdefiniowanych w zestawie
Unicode UTF-8 (czyli ISO-LATIN1) i że nie są wymagane żadne dokumenty zewnętrzne. W
prologu można także określić typ dokumentu, rozpoczynając od oznaczenia naszym przykładzie). Ponieważ jednak nie omawialiśmy tego oznaczenia, to powrócimy do niego
przy okazji skrótowego omawiania definicji typów dokumentu. Prolog może także zawierać
komentarze.
Treść
W treści dokumentu XML znajdują się właściwe dane. Zawiera ona tylko jeden element, objęty
parą znaczników w taki sposób, jak dokument HTML jest objęty znacznikami
.... W XML każdy element może zawierać elementy zagnieżdżone dowolnego
poziomu. W naszym przykładzie pojedynczym elementem jest catalog, w którym są
zagnieżdżone inne elementy, zawierające także zagnieżdżone elementy itd. Dość trudną definicję
 elementu odłożymy do następnego podrozdziału.
W treści można wstawiać komentarze.
Epilog
Epilog jest często pomijany. Może on zawierać instrukcje dotyczące przetwarzania i opisy
zaawansowanych zagadnień, którymi nie musimy się tutaj zajmować.
Elementy
Definiując treść dokumentu XML dla wygody pominęliśmy definicję samego elementu. Ponieważ
element jest podstawowym kontenerem do przechowywania danych w dokumencie XML, to jego
znaczenie jest bardzo ważne. Musimy więc omówić go oddzielnie w tym podrozdziale.
Elementy są pojemnikami zawierającymi dane, atrybuty, inne elementy lub kombinacje tych
wszystkich składników. Elementy są ograniczane za pomocą znaczników w nawiasach
trójkątnych, podobnych do znaczników HTML. W odróżnieniu od HTML, w XML nie wolno
pominąć znacznika końcowego (w HTML często pomijany jest np. znacznik końcowy

).
Oprócz tego, o czym już wspomniano, w znacznikach ważna jest wielkość liter.
Znacznik początkowy składa się z otwierającego nawiasu trójkątnego, nazwy i opcjonalnego
zestawu atrybutów oraz z końcowego nawiasu trójkątnego. Znacznik końcowy zawiera dodatkowo
ukośnik umieszczony za nawiasem otwierającym. Poprawnie zapisany znacznik XML ma więc
następującą postać:
The data content goes here
W pełni dozwolony jest brak zawartości między parą znaczników. Oznacza to, że zamiast pisać:

możemy użyć skróconego zapisu:

Taki pusty znacznik może wyglądać nieco dziwnie, ale tylko dlatego, że nie omówiliśmy jeszcze
atrybutów znacznika. Pozwalają one zawrzeć w znaczniku informację ilościową prawie w taki sam
sposób, jak w znacznikach HTML. Jako przykład można podać specyfikację tabeli zawierającą
szerokości marginesu i wypełnienia:

...

W XML dodajemy atrybuty z wartościami w bardzo podobny sposób, na przykład zapis:
The data content goes
here

definiuje znacznik z atrybutem text_type o wartości example.
Zasady dodawania atrybutów do znaczników XML są bardziej ścisłe niż przy znacznikach HTML:
W XML wszystkie wartości atrybutów muszą być ujęte w cudzysłów lub w apostrofy. Nie
można więc np. użyć znacznika , który w HTML jest poprawny.
W HTML jest możliwe, chociaż czasami powoduje błędy, kilkakrotne użycie tej samej
nazwy atrybutu w ramach jednego znacznika. W XML takie powtórzenia nie są
dozwolone.
W wartościach atrybutów nie mogą występować dwa znaki specjalne < i &. Zamiast nich
trzeba stosować znane także z HTML skróty < i %amp;.
Jeżeli wewnątrz atrybutu muszą występować cudzysłowy tego samego rodzaju co
cudzysłowy ograniczające wartość atrybutu, to zamiast nich należy użyć skrótów '
(dla oznaczenia apostrofu) lub " (dla oznaczenia pojedynczego znaku
cudzysłowu).
Jasne są kryteria wyboru informacji przekazywanej w części znacznika zawierającej atrybuty oraz
w postaci danych zawartych między parą znaczników. Ogólnie mówiąc, jeżeli dane nie zmieniają
znaczenia, a zwłaszcza nie zmieniają wartości, to należy zastosować atrybuty. Jeżeli informacja
nie zależy od jakichś czynników, to należy ją przekazać jako dane. Jako przykład można podać
dokument XML opisujący samochód. Kolor samochodu może występować jako atrybut, ponieważ
nie zmienia istoty samego samochodu, stanowiąc tylko szczegół jego wyglądu. Pojemność silnika
powinna być jednak przekazywana jako dane, ponieważ istotnie wpływa na sam samochód. Jeśli
nie mamy pewności, jak rozdzielić taką informację, to zawsze bezpieczniejsze będzie przekazanie
jej jako dane, a nie jako atrybut.
Zagnieżdżanie elementów
Dokument XML nie byłby wiele wart, gdybyśmy użyli w nim tylko jednego znacznika. Znakomita
większość użytecznych cech XML wynika z tego, że znaczniki można w nim zagnieżdżać. W
naszym przykładzie pokazanym na początku rozdziału mieliśmy znacznik catalog, wewnątrz
którego był umieszczony znacznik dvd, zaś wewnątrz dvd umieszczone były kolejne znaczniki
np. title i actors. Ponieważ do dokumentu można wstawić ten sam znacznik wielokrotnie, to
wewnątrz znacznika dvd o atrybucie  Seven Samurai znajdują się dwa znaczniki opisujące
aktorów. Widzimy więc, że dokument XML opisuje pewną strukturę drzewiastą. Jeśli narysujemy
schemat tej struktury, to zobaczymy, że catalog zawiera wielokrotne wpisy dvd, dvd zawiera
elementy title, price, director, actors i year_made, zaś actors zawiera jeden lub więcej
elementów actor.
rysunek ze strony 820  napisy nie są tu przetłumaczone, ponieważ muszą one ściśle odpowiadać
znacznikom użytym w listingu w tym rozdziale
W XML należy koniecznie przestrzegać poprawności zagnieżdżania sekwencji znaczników.
Wszystkie znaczniki muszą być dokładnie uporządkowane. W HTML konstrukcja taka jak w
poniższym przykładzie jest wprawdzie niedozwolona, ponieważ zawiera niepoprawnie
zagnieżdżone znaczniki, ale przeglądarki interpretują ją zazwyczaj w  rozsądny sposób:
HelloWord
W XML tego rodzaju sekwencja jest traktowana jako poważny błąd i powoduje, że cały dokument
staje się niepoprawny.
Komentarze
Komentarza w XML są bardzo podobne do komentarzy stosowanych w HTML. Komentarz
rozpoczyna się od znaków .
Wewnątrz komentarza nie wolno wstawić dwóch minusów (--) oraz nie wolno kończyć treści
komentarza minusem (-).
W odróżnieniu od HTML, parsery XML nie mają obowiązku przeglądać treści komentarza, a więc
znana sztuczka z ukrywaniem skryptów wewnątrz komentarza nie może być tu stosowana. Na
szczęście tego rodzaju sztuczki nie są potrzebne, ponieważ w XML określono sposób dołączania
instrukcji przetwarzania dokumentu.
Poprawność XML
W poprzednim podrozdziale omówiono reguły składni XML, które zawsze musi spełniać
dokument, aby można było go nazwać dokumentem XML. Reguły te nic nie mówią o zawartości
dokumentu lub sekwencjach znaczników w XML  nakazują tylko zgodność ich składni z XML.
Zazwyczaj nie wystarcza to do określenia formatu dokumentu, który ma być przetwarzany.
Załóżmy, że nasz katalog płyt DVD zawiera następujące dane:

Grand Illusion
29.99
Jean Renoir

Jean Gabin

1938

Jean Renoir
Black Adder
1954

Seven Samurai
27.99
Akira Kurosawa

Takashi Shimura
Toshiro Mifune

1954

Co oznaczają np. poniższe elementy?
Jean Renoir
Black Adder
1954
Ponieważ są one umieszczone na zewnątrz jakiegoś znacznika dvd, to nie ma możliwości
dokładnego określenia, do którego znacznika dvd należy je przypisać. Oprócz tego nie można
określić, czego dotyczy znacznik wibble. Widzimy więc, że powyższy fragment dokumentu XML
jest dobrze sformatowany i ma poprawną składnię, ale jest bezużyteczny ze względu na swoją
niepoprawność semantyczną. Musimy więc znalezć sposób takiego definiowania struktury
dokumentu XML, aby oprócz składni można było zdefiniować dokładnie znaczniki i ich
sekwencje, które mogą występować w danym dokumencie. Ten problem można rozwiązać między
innymi za pomocą definicji typu dokumentu określanej skrótem DTD (od Document Type
Definition).
Definicja typu dokumentu (DTD)
DTD jest dokładną specyfikacją tego, co może się pojawić w danym dokumencie XML, a więc
narzuca pewnego rodzaju ograniczenia na strukturę dokumentu w postaci określonego zestawu i
sekwencji znaczników. Dokumenty XML, z którymi jest związana DTD są klasyfikowane jako
 poprawne . Jest to dodatkowe wymaganie, niezależne od  dobrego sformatowania , a więc
dokument XML nie może być  poprawny jeśli nie jest także  dobrze sformatowany .
Aatwo się przekonać o konieczności stosowania odpowiednio zdefiniowanej struktury dokumentu
XML, ponieważ będziemy się nim posługiwali głównie do przenoszenia informacji. W XML, w
odróżnieniu od HTML, nie występuje coś takiego jak znaczenie wynikające z samego dokumentu,
ani wstępnie zdefiniowane znaczniki. Bez odpowiedniego  słownika nie można więc określić
znaczenia dokumentu XML  czyli zanim rozpocznie się jego rozpowszechnianie, należy
uzgodnić jego strukturę. Wszystko to można osiągnąć stosując DTD.
Tworzenie DTD
Definicja typu dokumentu (DTD) stanowi szkielet XML. W tym rozdziale zbrakłoby miejsca na
pełne omówienie wszystkich zagadnień związanych z DTD, a więc pokażemy tylko zagadnienia
podstawowe. Czytelnicy chcący uzyskać więcej informacji na ten temat powinni się zapoznać z
materiałami zródłowymi wskazanymi na końcu tego rozdziału. Podstawę DTD stanowi deklaracja
ELEMENT, która ma następującą postać:

Deklaracja ta oznacza, że  mytagname jest znacznikiem w strukturze dokumentu XML. Za
nazwą definiowanego znacznika można wymienić zawarte w nim elementy podrzędne.
Obowiązują tu pewne reguły dotyczące sposobu dodawania elementów podrzędnych, definiowania
listy tych elementów, sposobu ich wyboru oraz ich dopuszczalnej liczby. Reguły te są bardzo
proste:
Operator Znaczenie
,
Używany w liście do rozdzielenia elementów podrzędnych, które muszą
pojawiać się w wymienionym porządku
|
Wybór spośród elementów podrzędnych
?
Opcjonalny element podrzędny
*
Dowolna liczba wystąpień elementu podrzędnego (zero lub więcej razy)
+
Co najmniej jedno wystąpienie elementu podrzędnego
( ... )
Grupowanie elementów podrzędnych
Operatory *, ? oraz + następują za elementem, do którego się odnoszą.
Pokażemy to na przykładzie opisu zwykłej kanapki. Załóżmy, że chcemy zdefiniować element
sandwich, zawierający parę elementów bread, między którymi będzie występował jeden
element honey albo jelly. Możemy to zapisać następująco:

Fragment XML spełniający tę specyfikację powinien mieć postać:
/>
Załóżmy teraz, że wypełnienie kanapki ma być opcjonalne. Dodajemy więc odpowiedni
kwantyfikator:

Mamy teraz zapis oznaczający opcjonalność. Jeżeli trzeba, to możemy zastosować dowolnie dużo
nawiasów powiększając stopień złożoności naszej definicji.
Wróćmy jednak do naszego początkowego przykładu z katalogiem płyt DVD. Wymagamy, aby
element catalog składał się z pewnej liczby elementów dvd. Zawsze musi tu występować co
najmniej jeden element dvd. To wymaganie zapisujemy w następujący sposób:

Musimy zapisać, że element dvd zawiera elementy podrzędne, czyli title, price, director,
actors i year_made. Deklaracja dvd jest więc następująca:

Na najniższym poziomie zagnieżdżania musimy wskazać, że w elemencie actors musi
występować przynajmniej jeden element actor:

Zdefiniowaliśmy w ten sposób strukturę znaczników, ale nie mamy jeszcze żadnych informacji o
dopuszczalnych atrybutach tych znaczników. Wiemy, że nasz element dvd musi mieć numer
asin. Definiujemy to dodając do naszej DTD element ATTLIST:
asin CDATA #REQUIRED >
Taki zapis oznacza, że element dvd charakteryzuje się atrybutem asin, który zawiera dane
znakowe (napis) i jest atrybutem obowiązkowym. Ogólna postać znacznika ATTLIST jest
następująca:
kwalifikator>
Ciąg nazwa_znacznika nazwa_atrybutu typ_danych_atrybutu kwalifikator może
być powtarzany wielokrotnie, pod warunkiem, że żadne atrybuty się nie powtórzą. Dozwolone są
tu następujące typy danych:
Typ danych Znaczenie
CDATA
Napis
ID
Nazwa unikatowa w dokumencie XML
IDREF
Odwołanie do innego elementu za pomocą
podanego ID
IDREFS
Odwołanie do listy innych elementów za
pomocą podanych ID
ENTITY
Nazwa zewnętrznej jednostki
ENTITIES
Lista nazw zewnętrznych jednostek
NMTOKEN
Nazwa
NMTOKENS
Lista nazw
NOTATION
Zdefiniowana na zewnątrz notacja, np. TEX lub
PNG
Explicit value
Ciąg jawnie zdefiniowanych wartości
Omówienie wszystkich wymienionych tu typów wykracza znacznie poza zakres tego rozdziału.
Kwalifikator występujący w elemencie ATTLIST może mieć następujące wartości:
Wartość Znaczenie
#REQUIRED
Atrybut musi się pojawić
#IMPLIED
Atrybut jest opcjonalny
#FIXED
Atrybut musi mieć podaną wartość
Jeśli atrybutowi nie nadano wartości, to
automatycznie przybiera on podaną wartość
domyślną
Ponieważ nasz element dvd ma tylko jeden atrybut, to w specyfikacji DTD tego elementu trzeba
użyć tylko jednej deklaracji ATTLIST.
Musimy także określić typ danych, które mogą występować w elementach między ich
znacznikami początkowymi i końcowymi. Na najniższym poziomie wszystkich danych znajdują
się przetwarzalne dane znakowe (Parseable Character Data), co w XML zapisujemy jako
(#PCDATA). Zakończymy więc naszą specyfikację DTD wpisem, który to definiuje:





Podsumujmy nasze rozważania podając specyfikację DTD w całości:


asin CDATA #REQUIRED >


<1ELEMENT director (#PCDATA)>



Można to wyrazić opisowo: element catalog zawiera jeden lub więcej elementów dvd. Każdy
element dvd musi mieć atrybut asin, który zawiera pewne dane. Element dvd zawiera elementy
title, price, directory, actors oraz year_made. Element actors zawiera przynajmniej
jeden element actor. Wszystkie elementy najniższego poziomu muszą zawierać dane znakowe.
Musimy się zgodzić, że specyfikację DTD łatwiej zrozumieć niż taki opis (pod warunkiem, że
rozumie się podstawy DTD).
Schematy
Pomimo tego, że DTD zawierają dokładne definicje struktury dokumentu, to dość trudno jest się
nimi posługiwać. Z tego właśnie powodu W3C opracowuje bardziej rygorystyczną i zarazem
bardziej elastyczną metodę, czyli tzw. schematy. Będą się one lepiej nadawały do definiowania
sposobów przetwarzanie plików XML i ułatwią aplikacjom wymianę danych w tym formacie.
W czasie pisania tej książki rozważano kilka zgłoszonych propozycji, które stanowią główny
przedmiot zainteresowania w światku XML. Obserwuje się konkurencyjną walkę różnych firm na
tym polu i próby wymuszania uznania własnego rozwiązania za standard, powiązane z bezpłatnym
udostępnianiem narzędzi i stron w Internecie wspierających takie rozwiązania. Na szczęście
zostanie to wkrótce rozwiązane i powstanie uzgodniony oficjalny standard. Ponieważ podczas
pisania tych słów nie istniał jeszcze ostateczny schemat definiowania XML, to w pozostałych
częściach tego rozdziału będziemy omawiać tylko DTD, nie zważając na to, że prawdopodobnie
specyfikacja ta może być w przyszłości zastąpiona bardziej złożonym dokumentem.
Powiązania DTD z dokumentem XML
Po zdefiniowaniu specyfikacji DTD musimy powiązać ją z dokumentami XML, których strukturę
ona definiuje. Dla potrzeb naszego katalogu płyt DVD wystarczy po prostu wstawienie tej
specyfikacji do dokumentu. Pamiętajmy jednak, że w ogólnym przypadku takie rozwiązanie nie
jest dobre: jeżeli dwie instytucje chcą wymieniać dokumenty w formacie XML, to niezbyt
wygodne jest, aby każda wiadomość zawierała własną specyfikację. Potrzebny jest więc
uzgodniony standard zewnętrzny, z którym będą zgodne wszystkie wymieniane dokumenty XML.
Schematy XML zapewniające taką zgodność są dopiero opracowywane.
Specyfikacja DTD w naszym przykładowym dokumencie XML jest włączona w dokument za
pomocą znacznika

asin CDATA #REQUIRED >






]>
Rozbiór XML
Jeśli zrozumieliśmy już sposób przekazywania danych katalogowych w naszym przykładowym
dokumencie XML, to musimy dokonać rozbioru tego dokumentu. Czynność ta musi poprzedzić
przetwarzanie danych zawartych w dokumencie. W tym momencie mamy poważny dylemat: jaki
parser zastosować? Stosowane są dwa odmienne modele rozbioru dokumentów XML: model
obiektowy dokumentu (oznaczany skrótem DOM od słów Document Object Model) oraz model,
w którym wykorzystuje się prosty interfejs programowy dla XML (skrótowo nazywany SAX od
słów Simple API for XML). Przed dokonaniem wyboru i rozpoczęciem pracy nad kodem
omówimy skrótowo obydwa z nich.
DOM
Konsorcjum W3C wydało standardową specyfikację dla modelu obiektowego (DOM), która
określa dostęp do wewnętrznych elementów dokumentu w sposób unormowany i niezależny od
użytego języka programowania. W modelu DOM dokument jest pobierany i dokonywany jest jego
rozbiór. Od tego momentu staje się on dostępny dla programu, który może go modyfikować. Po
zakończeniu modyfikacji modelu obiektowego można go ponownie zapisać jako dokument XML.
Istnieje jednak poważna wada modelu obiektowego: cały dokument przed przetworzeniem musi
być przetrzymywany w pamięci i może to sprawiać kłopoty przy dużych plikach XML. Dlatego też
powszechnie używany jest mniej oficjalny standard, czyli SAX, nie stwarzający takich ograniczeń.
SAX
SAX został pierwotnie napisany w języku Java. Ostatnio projektem tej specyfikacji zarządzał
David Meggison i na jego stronie internetowej można znalezć najświeższe informacje (adres jest
podany w wykazie zródeł na końcu rozdziału).
Specyfikacja jest bardzo prosta i została wykorzystana w sposób prawie uniwersalny w parserze
XML napisanym w języku Java. Istnieją także wersje dla języków C i C++ (patrz wykaz
materiałów zródłowych).
Według modelu SAX dokument XML nie jest ładowany do pamięci w całości, ale odczytywany
częściami. Udostępniane są tu wywołania zwrotne do własnego kodu użytkownika, oznaczające
np. początek znacznika, znalezienie komentarza lub wykrycie końca dokumentu. Zmusza to
programistę do nieco większego wysiłku, ponieważ musi on przyjmować dokument XML w
kolejności zgodnej z dokonywanym rozbiorem, a nie w kolejności dowolnej.
Taki sposób działania przypomina nieco wywołania zwrotne używane w GNOME podczas
obsługi zdarzeń. SAX jest interfejsem typu tylko do odczytu, nie generującym dokumentów XML.
W wielu praktycznych zastosowaniach jest to jednak rozwiązanie całkowicie wystarczające, a
pozbycie się niedogodności związanych z przetrzymywaniem całego dokumentu w pamięci
oznacza, że może to być rozwiązanie jedyne dla bardzo dużych dokumentów XML.
Biblioteka libXML (gnome-xml)
W naszej aplikacji obsługującej wypożyczalnię płyt DVD zdecydowaliśmy się skorzystać z
modelu SAX, a w szczególności z biblioteki o nazwie libxml (poprzednio znanej pod nazwą
gnome-xml), z następujących powodów:
Wiedzieliśmy, że biblioteka ta była już stosowana w Glade i że działała ona pewnie.
Występuje w niej interfejs do języka C, czyli tego, który jest podstawowym językiem
programowania stosowanym w tej książce.
Potrzebowaliśmy jedynie odczytywać dokumenty XML, a nie tworzyć je.
Biblioteka ta jest bardzo szybko rozwijana.
Jeżeli na komputerze z systemem Linux jest już zainstalowany pakiet GNOME, to prawie na
pewno można znalezć tam bibliotekę libxml. Jeżeli zamiast GNOME jest stosowany inny system
interfejsów graficznych, to może jej nie być. Nie stwarza to problemu, bowiem libxml jest
dostępna w postaci pakietu RPM.
Na stronie macierzystej libxml można znalezć adresy serwerów umożliwiających
pobranie pakietu. Należy pamiętać o pobraniu zarówno pakietu standardowego, jak i
wersji -devel potrzebnej do kompilacji programów obsługujących XML (chyba że
instalacja odbywa się po własnej kompilacji kodu zródłowego).
Kod korzystający z libxml wydaje się na pierwszy rzut oka nieco dziwny. To wrażenie wynika
częściowo z konieczności zastąpienia oryginalnych konstrukcji w języku Java konstrukcjami w
języku C. Kolejną przyczyną dziwnego wyglądu jest użycie funkcji wywołań zwrotnych, z czym
nie wszyscy programiści są zaznajomieni.
Podstawowy przepis na zastosowanie modelu SAX przy przetwarzaniu dokumentu XML jest
bardzo prosty:
Utworzyć egzemplarz parsera.
Napisać zestaw funkcji, które będą wywoływane po wykryciu przez parser określonych
konstrukcji.
Poinformować parser o swoich funkcjach.
Nakazać, aby parser przeprowadził rozbiór pliku.
Parser wywołuje utworzone funkcje podczas przetwarzania XML, powiadamiając o
przetwarzanych przez siebie danych.
W praktyce trzeba jeszcze pokonać kilka dodatkowych kłopotów, ale schemat działań nie odbiega
zbytnio od powyższego.
Tę sekwencję działań można przedstawić graficznie:
rysunek ze strony 828
1) Podać nazwę pliku, który ma być przetwarzany
2) Utworzyć parser
3) Parser przetwarza plik
4) Parser wywołuje zdefiniowane funkcje wywołań zwrotnych według potrzeby
5) Usunięcie parsera po zakończeniu przetwarzania
Własny program
Zdefiniowane funkcje wywołań zwrotnych
Parser utworzony przez libxml
Przetwarzany plik XML
Tworzenie i wywoływanie parsera
Chyba wszyscy mają już dosyć tej teorii  zapoznajmy się więc z niewielkim przykładowym
programem, który korzysta z parsera zawartego w libxml. Program nazywa się sax1.c:
#include
#include
#include
#include
int main() {
xmlParserCtxtPtr ctxt_ptr;
ctxt_ptr = xmlCreateFileParserCtxt("dvdcatalog.xml");
if (!ctxt_ptr) {
fprintf(stderr, "Failed to create file parser\n");
exit(EXIT_FAILURE);
}
xmlParseDocument(ctxt_ptr);
if (!ctxt_ptr->wellFormed) {
fprintf(stderr, "Document not well formed\n");
}
xmlFreeParserCtxt(ctxt_ptr);
printf("Parsing complete\n");
exit(EXIT_SUCCESS);
}
Program ten jest dość krótki, a więc nie dodawaliśmy do niego żadnych funkcji wywołań
zwrotnych. Nie należy się tym martwić  już wkrótce będziemy mieli okazję je zobaczyć.
Przy kompilacji tego programu należy podać ścieżkę do dołączanych plików nagłówkowych
parser.h i parserInternals.h. Jeśli na komputerze jest zainstalowana wersja libxml
wcześniejsza niż 2, to te pliki są prawdopodobnie umieszczone w katalogu
/usr/include/gnome-xml, zaś poczynając od wersji 2 należy ich szukać w
/usr/include/xml. Program należy także konsolidować z bibliotekami xml i zlib (ta druga
bibliotek jest wymagana dlatego, że libxml może czytać skompresowane pliki XML). Polecenie
uruchamiające kompilację przykładowego programu może mieć postać:
$ gcc -I/usr/include/gnome-xml sax1.c -lxml -lz -o sax1
lub:
$ gcc -I/usr/include/xml sax1.c -lxml -lz -o sax1
Spójrzmy teraz na szczegóły naszego kodu:
xmlParserCtxtPtr ctxt_ptr;
ctxt_ptr = xmlCreateFileParserCtxt("dvdcatalog.xml");
if (!ctxt_ptr) {
fprintf(stderr, "Failed to create file parser\n");
exit(EXIT_FAILURE);
}
Powyższy fragment tworzy parser, wskazywany następnie przez ctxt_ptr. Wywołanie:
xmlParseDocument(ctxt_ptr);
uruchamia rozbiór pliku przez parser, zaś instrukcja:
if (!ctxt_ptr->wellFormed) {
fprintf(stderr, "Document not well formed\n");
}
jest wywoływana po zakończeniu przetwarzania i może ostrzec o tym, że dokument jest
niepoprawnie sformatowany. Końcowe wywołanie:
xmlFreeParserCtxt(ctxt_ptr);
zwalnia parser po zakończeniu pracy.
Po uruchomieniu tego programu zobaczymy na ekranie:
$./sax1
Parsing complete
$
Jest to komunikat początkowy, lecz niewiele z niego wynika: wiemy tylko, że parser nie
potraktował naszego pliku XML jako błędnie sformatowanego. Spróbujmy zaburzyć ten plik aby
sprawdzić, jak zareaguje na to parser.
Umieścimy więc celowo w jednym z wpisów dvd znacznik :

Grand Illusion
29.99
JeanRenoir

Jean Gabin

1938

Uruchamiamy parser ponownie:
$./sax1
dvdcatalog.xml:7: error: Opening and ending tag mismatch: B and
director
JeanRenoir
^
dvdcatalog.xml:12: error: Opening and ending tag mismatch: director
and dvd

^
dvdcatalog.xml:25: error: Opening and ending tag mismatch: dvd and
catalog

^
dvdcatalog.xml:26: error: detected an error in element content
^
dvdcatalog.xml:26: error: Premature end of data in tag
}
printf("XML version %s, encoding %s\n", ctxt_ptr->version, ctxt_ptr-
>encoding);
ctxt_ptr->sax = NULL
Po kompilacji i uruchomieniu tego programu otrzymujemy:
$./sax2
XML version 1.0, encoding UTF-8
Parsing complete
$
Zastosowanie wywołań zwrotnych
Wiemy już, że parser przetwarza nasz plik, sprawdza jego poprawność i pobiera podstawowe
informacje na jego temat, a więc można rozpocząć tworzenie funkcji wywołań zwrotnych. Posłużą
one do uzyskiwania danych zawartych w pliku XML.
W pliku parser.h zdefiniowano strukturę xmlSAXHandler, która podaje miejsca dostępne dla
wywołań zwrotnych. Strukturę tę omówimy za chwilę.
Zdefiniowano także prototypy funkcji, których należy użyć w wywołaniach zwrotnych:
typedef xmlParserInputPtr (*resolveEntitySAXFunc) (void *ctx,
const CHAR
*publicId,
const CHAR
*systemId);
typedef void (*internalSubsetSAXFunc) (void *ctx, const CHAR *name,
const CHAR *ExternalID,
const CHAR *SystemID);
typedef xmlEntityPtr (*getEntitySAXFunc) (void *ctx, const CHAR
*name);
typedef void (*entityDeclSAXFunc) (void *ctx, const CHAR *name, int
type,
const CHAR *publicId,
const CHAR *systemId,
CHAR *content);
typedef void (*attributeDeclSAXFunc) (void *ctx, const CHAR *elem,
const CHAR *name,
int type, int def,
const CHAR *defaultValue,
xmlEnumerationPtr tree);
typedef void (*elementDeclSAXFunc) (void *ctx, const CHAR *name,
int type, xmlElementContentPtr
content);
typedef void (*unparsedEntityDeclFunc)(void *ctx,
const CHAR *name,
const CHAR *publicId,
const CHAR *systemId,
const CHAR *notationName);
typedef void (*setDocumentLocatorSAXFunc) (void *ctx,
xmlSAXLocatorPtr loc);
typedef void (*startDocumentSAXFunc) (void *ctx);
typedef void (*endDocumentSAXFunc) (void *ctx);
typedef void (*startElementSAXFunc) (void *ctx, const CHAR *name,
const CHAR **atts);
typedef void (*endElementSAXFunc) (void *ctx, const CHAR *name);
typedef void (*attributeSAXFunc) (void *ctx, const CHAR *name,
const CHAR *value);
typedef void (*referenceSAXFunc) (void *ctx, const CHAR *name);
typedef void (*charactersSAXFunc) (void *ctx, const CHAR *ch, int len)
typedef void (*ignorableWhitespaceSAXFunc) (void *ctx,
const CHAR *ch, int len);
typedef void (*processingInstructionSAXFunc) (void *ctx,
const CHAR *target,
const CHAR *data);
typedef void (*commentSAXFunc) (void *ctx, const CHAR *value);
typedef void (*warningSAXFunc) (void *ctx, const char *msg, ...);
typedef void (*errorSAXFunc) (void *ctx, const char *msg, ...);
typedef void (*fatalErrorSAXFunc) (void *ctx, const char *msg, ...);
typedef int (*isStandaloneSAXFunc) (void *ctx);
typedef int (*hasInternalSubsetSAXFunc) (void *ctx);
typedef int (*hasExternalSubsetSAXFunc) (void *ctx);
Należy zwrócić uwagę na to, że używany jest tu typ CHAR a nie char. Jest to
nowy typ zadeklarowany w nagłówkach i takie oznaczenie nie jest błędem.
Na szczęście, do przetworzenia pliku XML i pobrania z niego użytecznych informacji potrzeba
tylko kilku wywołań zwrotnych. Zanim utworzymy wymaganą główną funkcję wywołania
zwrotnego, dodajmy do naszego kodu dwie proste funkcje, które będą wykorzystywane w celach
szkoleniowych.
Funkcje te będą sygnalizować początek i koniec dokumentu, i będą wywoływane w tych właśnie
miejscach. Można je znalezć w podanych wyżej deklaracjach pod nazwami
startDocumentSAXFunc i endDocumentSAXFunc. Wykorzystuje się je często w operacjach
inicjujących i oczyszczających pamięć.
Aby użyć wywołania zwrotnego, należy wykonać następujące trzy czynności:
Utworzyć funkcję obsługującą wywołanie zwrotne.
Ustawić w strukturze wywołań zwrotnych libxml wywoływanie tej funkcji.
Przekazać do parsera informację o strukturze wywołań zwrotnych.
Pierwszy etap jest prosty, a nasze funkcje wywołań zwrotnych mają następującą postać (nie
stosujemy jeszcze parametrów):
static void start_document(void *ctx) {
printf("Document start\n");
}
static void end_document(void *ctx) {
printf("Document end\n");
}
Teraz następuje niewielka sztuczka z ustawianiem struktury wywołań zwrotnych. Najpierw należy
we własnym kodzie zadeklarować strukturę typu xmlSAXHandler i przydzielić w odpowiednich
miejscach wskazniki do naszych funkcji.
Struktura xmlSAXHandler opisująca dostępne wywołania zwrotne znajduje się w pliku parse.h:
typedef struct xmlSAXHandler {
internalSubsetSAXFunct internalSubset;
isStandaloneSAXFunc isStandalone;
hasInternalSubsetSAXFunc hasInternalSubset;
hasExternalSubsetSAXFunc hasExternalSubset;
resolveEntitySAXFunc resolveEntity;
getEntitySAXFunc getEntity;
entityDeclSAXFunc entityDecl;
notationDeclSAXFunc notationDecl;
attributeDeclSAXFunc attributeDecl;
elementDeclSAXFunc elementDecl;
unparsedEntityDeclSAXFunc unparsedEntityDecl;
setDocumentLocatorSAXFunc setDocumentLocator;
startDocumentSAXFunc startDocument;
endDocumentSAXFunc endDocument;
startElementSAXFunc startElement;
endElementSAXFunc endElement;
referenceSAXFunc reference;
charactersSAXFunc characters;
ignorableWhitespaceSAXFunc ignorableWhitespace;
processingInstructionSAXFunc processingInstruction;
commentSAXFunc comment;
warningSAXFunc warning;
errorSAXFunc error;
fatalErrorSAXFunc fatalError;
} xmlSAXHandler;
Jak widzimy, wskazniki do funkcji wywołań zwrotnych są odpowiednio ponazywane, a więc
łatwo można z nich skorzystać.
Wszystkie lokalizacje nieużywanych funkcji wywołań zwrotnych muszą mieć wskaznik NULL,
dzięki czemu libxml uzyska informację o tym fakcie. Aby zabezpieczyć się przed zmianami
struktury użyjemy funkcji memset oczyszczającej całą jej zawartość i nadającej jej wartości NULL,
a następnie jawnie wpiszemy wskazniki do używanych funkcji wywołań zwrotnych. Każdy, kto
używał struktur wywołań zwrotnych dobrze wie, jaki chaos może spowodować wpisanie
wskaznika do funkcji w nieprawidłowe miejsce, jeśli lista tych funkcji jest długa...
static xmlSAXHandler mySAXParseCallbacks;
memset(&mySAXParseCallbacks, sizeof(mySAXParseCallbacks), 0);
mySAXParseCallbacks.startDocument = start_document;
mySAXParseCallbacks.endDocument = end_document;
Na zakończenie musimy poinformować parser o naszej strukturze wywołań zwrotnych:
if (!ctxt_ptr) {
fprintf(stderr, "Failed to create file parser\n");
exit(EXIT_FAILURE);
}
ctx_ptr->sax = &mySAXParseCallbacks;
xmlParseDocument(ctxt_ptr);
ctxt_ptr->sax = NULL;
Zwróćmy uwagę na to, że po zakończeniu rozbioru pliku wskaznikowi kontekstu ponownie
nadano wartość NULL.
Po połączeniu tych wszystkich fragmentów w całość nadajemy jej nazwę sax3.c i po
uruchomieniu widzimy, że nasze funkcje są wywoływane automatycznie podczas przetwarzania
dokumentu:
$./sax3
Document start
Document end
Parsing Complete
$
W tym przykładzie usunęliśmy informację o wersji XML i kodowaniu znaków, ponieważ
nie wnosi ona tu nic nowego.
Widzimy więc, że konfiguracja wywołań zwrotnych nie jest trudna. Przejrzyjmy teraz całą listę
wywołań zwrotnych i sprawdzmy, które z nich mogą się przydać. W praktyce około 95%
wszystkich potrzeb występujących przy rozbiorze dokumentu można zaspokoić korzystając tylko z
pięciu wywołań (można także użyć jeszcze trzech dodatkowych, które zajmują się obsługą
błędów). Tymi właśnie funkcjami zajmiemy się niżej. Wszystkie z nich wymagają podania
wskaznika void *ctx jako pierwszy parametr. Jego zastosowanie zostanie omówione przy okazji
opisywania różnic występujących miedzy poszczególnymi wywołaniami zwrotnymi.
Obsługa błędów
Wszystkie funkcje obsługi błędów mają ten sam format, lecz korzystają z różnych wywołań
zwrotnych zależnych od stopnia ważności błędu. Są to następujące trzy funkcje:
typedef void (*warningSAXFunc) (void *ctx, const char *msg, ...);
typedef void (*errorSAXFunc) (void *ctx, const char *msg, ...);
typedef void (*fatalErrorSAXFunc) (void *ctx, const char *msg, ...);
Funkcja warningSAXFunc obsługuje ostrzeżenia, errorSAXFunc obsługuje zwykłe błędy, a
fatalErrorSAXFunc obsługuje błędy krytyczne, przy których parser nie może kontynuować
działania. W odróżnieniu od poprzednich wywołań, tutaj używany jest zwyczajny typ char, a nie
CHAR.
Wszystkie wymienione wyżej funkcje wymagają różnej liczby argumentów. Można uzyskać do
nich dostęp za pomocą wywołania stdarg. Komunikaty o błędach mogą być wówczas
wyświetlane (po dołączeniu ), a więc mamy:
va_list args;
va_start(args, msg);
vprint(msg, args);
va_end(args);
Jeśli wywołujemy nasz parser z wiersza poleceń, tak jak opisywaliśmy, to obsługa błędów działa
bez problemów. Gdy użyjemy jakiegoś interfejsu graficznego, to nie będzie już to takie proste i
musimy utworzyć nieco bardziej rozbudowane procedury korzystające z wywołań zwrotnych.
Oto przykład pochodzący z pliku saxp.c, w którym zastosowano wywołania zwrotne do obsługi
błędów. Plik ten znajduje się w zestawie programów testowych w pakiecie Glade:
static void gladeError(GladeParseState *state, const char*msg, ...) {
va_list args;
va_start(args, msg);
g_logv("XML", G_LOG_LEVEL_CRITICAL, msg, args);
va_end(args);
}
Początek dokumentu
Funkcja startDocumentSAXFunc jest wywoływana jednokrotnie w momencie rozpoczęcia
rozbioru dokumentu, zawsze przed jakimkolwiek innym wywołaniem zwrotnym. Jej prototyp
wygląda następująco:
typedef void (*startDocumentSAXFunc) (void *ctx);
Koniec dokumentu
Funkcja endDocumentSAXFunc jest wywoływana jednokrotnie po zakończeniu rozbioru
dokumentu  albo z powodu wykrycia końca dokumentu, albo po wystąpieniu błędu
krytycznego. Oto jej prototyp:
typedef void (*endDocumentSAXFunc) (void *ctx);
Początek elementu
Funkcja startElementSAXFunc jest wywoływana zawsze po wykryciu nowego elementu:
typedef void (*startElementSAXFunc) (void *ctx, const CHAR *name,
const CHAR **atts);
Parametr name oznacza nazwę elementu, zaś parametr atts ma albo wartość NULL, albo jest listą
wskazników do nazw i wartości atrybutów, zakończoną wartością NULL. W naszym
przykładowym katalogu płyt DVD element dvd ma atrybut asin, którego wartością jest napis 
a więc tablica parametrów atts będzie zawierać dwa wskazniki: jeden na napis  asin a drugi na
faktyczną treść tego napisu (składającego się z cyfr). W następnej wersji parsera pokażemy sposób
dostępu do tych atrybutów.
Koniec elementu
Funkcja endElementSAXFunc jest wywoływana zawsze po wykryciu końca elementu, nawet
wtedy, gdy jest to element pusty (np. zapisany jako ). Dzięki temu każdemu wywołaniu
zwrotnemu związanemu z początkiem elementu towarzyszy odpowiednie wywołanie oznaczające
koniec elementu (pod warunkiem, że nie wystąpi błąd krytyczny):
typedef void (*endElementSAXFunc) (void *ctx, const CHAR *name);
Znaki
Funkcja charactersSAXFunc jest wywoływana zawsze po wykryciu sekwencji znaków nie
tworzących jakiegoś specyficznego składnika, np. elementu lub komentarza:
typedef void (*charactersSAXFunc) (void *ctx, const CHAR *ch, int
len);
W przypadku długich napisów można je dzielić na mniejsze fragmenty wywołując tę funkcję
wielokrotnie. Aplikacja musi wówczas zadbać o odpowiednią obsługę takich wywołań
Przykład wywołania zwrotnego
Wiemy już jak wyglądają wywołania zwrotne i możemy utworzyć jakiś kod, który będzie
realizował bardziej skonkretyzowane zadania, czyli będzie pobierał dane i atrybuty z elementów.
Jest to pierwsze realistyczne podejście do rozbioru dokumentu. Oto ten kod, któremu nadaliśmy
nazwę sax4.c:
#include
#include
#include
#include
#include
static void start_document(void *ctx);
static void end_document(void *ctx);
static voidstart_element(void *ctx, const CHAR *name, const CHAR
**attrs);
static void end_element(void *ctx, const CHAR *name);
static void chars_found(void *ctx, const *chars, int len);
static xmlSAXHandler mySAXParseCallbacks;
int main() {
xmlParserCtxtPtr ctxt_ptr;
memset(&mySAXParseCallbacks, sizeof(mySAXParseCallbacks), 0);
mySAXParseCallbacks.startDocument = start_document;
mySAXParseCallbacks.endDocument = end_document;
mySAXParseCallbacks.startElement = start_element;
mySAXParseCallbacks.endElement = end_element;
mySAXParseCallbacks.characters = chars_found;
ctxt_ptr = xmlCreateFileParserCtxt("dvdcatalog,xml");
if (!ctxt_ptr) {
fprintf(stderr, "Failed to create file parser\n");
exit(EXIT_FAILURE);
}
ctx_ptr->sax = &mySAXParseCallbacks;
xmlParseDocument(ctxt_ptr);
if (!ctxt_ptr->wellFormed) {
fprint(stderr, "Document not well formed\n");
}
ctxt_ptr->sax = NULL;
xmlFreeParserCtxt(ctxt_ptr);
printf("Parsing complete\n");
exit(EXIT_SUCCESS);
} /* main */
static void start_document(void *ctxt) {
printf("Document start\n");
} /* start_document */
static void end_document(void *ctx) {
printf("Document end\n");
} /* end_document */
static void start_element(void *ctx, const CHAR *name, const CHAR
**attrs) {
const char *attr_ptr;
int curr_attr = 0;
printf("Element %s started\n", name);
if (attrs) {
attr_ptr = *attrs;
white(attr_ptr) {
printf("\tAttribute %s\n", attr_ptr);
curr_attr++;
attr_ptr = *(attrs + curr_attr);
}
}
} /* start_element */
static void end_element(void *ctx, const CHAR *name) {
printf("Element %s ended\n", name);
} /* end_element */
#define CHAR_BUFFER 1024
static void chars_found(void *ctx, const CHAR *chars, int len) {
char buff[CHAR_BUFFER + 1]
if (len > CHAR_BUFFER) len = CHAR_BUFFER;
strncpy(buff, chars, len);
buff[len] = '\0';
printf("Found %d characters: %s\n", len, buff);
} /* chars_found */
Przykład jest dosyć długi, ale niezbyt skomplikowany, z wyjątkiem dwóch fragmentów, na które
warto zwrócić szczególną uwagę:
funkcja start_element pokazuje sposób wykrywania obecności atrybutów oraz
dostępu do ich nazw i wartości.
funkcja chars_found wyświetla znalezione dane. Zwróćmy uwagę na to, że
przekazywany napis nie kończy się wartością NULL (a przynajmniej tak się dzieje w
bieżącej implementacji)  a więc chcąc wyświetlać odpowiednią liczbę znaków trzeba
zastosować specjalne środki.
Po uruchomieniu programu sax4 otrzymamy następujące wyniki (podane tu w skróconej postaci):
$ ./sax4
Document start
Element catalog started
Found 5 characters:
Element dvd started
Attribute asin
Attribute 0780020707
Found 8 characters:
Element title started
Found 14 characters: Grand Illusion
Element title ended
Found 8 characters:
Element price started
Found 5 characters: 29.99
Element price ended
Found 8 characters:
Element director started
Found 11 characters: Jean Renoir
Element director ended
Found 8 characters:
Element actors started
Found 11 characters:
Element actor started
Found 10 characters: Jean Gabin
Element actor ended
Found 8 characters:
Element actors ended
...
Wnikliwi czytelnicy wykryją tu bardzo szybko trudności. Przy wywoływaniu procedury
start_element nie jest jeszcze znana zawartość elementu, zaś przy wywołaniu chars_found
już nie wiemy, który element był przetworzony. Oprócz tego, funkcja chars_found jest
wywoływana w tych miejscach, w których nie ma potrzeby przetwarzania znaków. Jest to wada
parsera posługującego się modelem SAX. Można ją ominąć zachowując informację o stanie
parsera.
Utrzymywanie informacji o stanie parsera
Potrzeba utrzymywania informacji o stanie przy sekwencyjnym przetwarzaniu strukturalnych
danych jest prawie oczywista i biblioteka libxml dysponuje pewnymi właściwościami, które
można wykorzystać do tego celu.
W kontekstowej strukturze, podobnie jak we wskazniku do struktury wywołań zwrotnych istnieje
wskaznik void * do zmiennej userData, którą można wykorzystać do przechowywania
informacji o stanie. Informacja ta nie ma ściśle określonej postaci, co jest tu zaletą, ponieważ
można wówczas uzyskać coś więcej, niż tylko czysty stan. Przy każdym wywołaniu funkcji
wywołania zwrotnego wskaznik do naszej struktury jest przekazywany jako pierwszy argument
(wskaznik void * ctx do wywołań zwrotnych) i tej właściwości jeszcze nie wykorzystaliśmy.
Aby utrzymywać informację o stanie parsera musimy najpierw zadeklarować strukturę do jej
przechowywania. W przypadku naszego pliku XML, oprócz informacji o stanie parsera, potrzebna
jest jeszcze dodatkowa informacja o liczbie aktorów występujących w danym filmie. W
poprzednich rozdziałach założyliśmy, że z jednym tytułem filmu będą związane dokładnie dwie
osoby i jeśli brak będzie nazwisk, to na ich miejsce wpisywana będzie wartość NULL.
Specyfikacja DTD w naszym pliku XML przewiduje, że z tytułem jest zawiązany zawsze co
najmniej jeden aktor (musi wystąpić element actors, który zawiera co najmniej jeden element
actor). Mogą więc wystąpić tytuły, dla których podane będą nazwiska więcej niż dwóch
aktorów. Musimy więc wychwycić tylko przypadki z jednym aktorem, aby dla drugiego wpisu
użyć wartości NULL.
Najpierw wyliczymy wszystkie stany, w których może znalezć się parser:
typedef enum {
parse_start_s = 0, /* początek
*/
parse_finish_s, /* koniec
*/
parse_dvd_s, /* przetwarzanie elementu dvd
*/
parse_price_s, /* przetwarzanie elementu price
*/
parse_actor_s, /* przetwarzanie elementu actor
*/
parse_year_made_s, /* przetwarzanie elementu year_made
*/
parse_valid_string_s /* przetwarzanie innych poprawnych elementów
*/
parse_skip_string_s, /* przetwarzanie elementów nie branych pod
uwagę */
parse_unknown_s /* przetwarzanie nieznanych elementów
*/
} parse_state;
Następnie zadeklarujemy strukturę do przechowywania stanu parsera i liczby aktorów:
typedef struct {
parse_state current_state;
int actors_this_movie;
} catalog_parse_state;
W funkcji głównej deklarujemy egzemplarz tej struktury, a w strukturze kontekstowej parsera
przydzielamy jej wskaznik:
xmlparserCtxtPtr ctxt_ptr;
catalog_parse_state parsing_state;
ctxt_ptr = xmlCreateFileParserCtxt("dvdcatalog.xml");
if (!ctxt_ptr) {
fprintf(stderr, "Failed to create file parser\n");
exit(EXIT_FAILURE);
}
ctxt_ptr->sax = &mySAXParseCallbacks;
ctxt_ptr->userData = &parsing_state;
xmlParseDocument(ctxt_ptr);
Dostęp do struktury stanu w każdym z naszych wywołań zwrotnych możemy uzyskać poprzez
wskaznik ctx:
static void start_element(void *ctx, const char *name, const char
**attrs) {
const char *attr = 0;
int curr_attr = 0;
catalog_parse_state *state_ptr;
parse_state curr_state;
parse_event curr_event;
state_ptr = (catalog_parse_state *)ctx;
curr_state = state_ptr->current_state;
Ostateczna wersja parsera
Końcowa wersja programu użytego do przetwarzania naszego pliku XML zawiera większość z
omawianych w tym rozdziale składników. Dodaliśmy tu maszynę obsługującą stan i zdarzenia,
która decyduje o sposobie przetwarzania każdego zdarzenia. Zakładamy, że maszyna znajduje się
w określonym stanie zaś wywołania zwrotne z parsera są przekształcane na zdarzenia. Kombinacja
bieżącego stanu i odebranego zdarzenia przekazana do tej maszyny decyduje o sposobie
przetwarzania informacji.
Nie dołączaliśmy tu własnych funkcji obsługi błędów, ponieważ funkcje wbudowane w libxml
nadają się doskonale do naszych celów.
Zamiast pokazywania pliku CSV będącego wynikiem działania programu, zamieszczamy tu kod
zródłowy naszego programu (nazwaliśmy go sax5.c). Jest on na tyle czytelny, że bardzo łatwo
można wyobrazić sobie wynik jego działania.
Rozpoczynamy od standardowych dyrektyw i deklaracji:
#include
#include
#include
#include
#include
Dalej następują typy wyliczeniowe ułatwiające przetwarzanie zdarzeń i definiowanie stanów:
/* Mapa zdarzeń */
typedef enum {
parse_start_e = 0,
parse_finish_e,
parse_catalog_e,
parse_dvd_e,
parse_title_e,
parse_price_e,
parse_director_e,
parse_actors_e,
parse_actor_e,
parse_year_made_e,
parse_end_element_e,
parse_other_e,
} parse_event;
/* Mapa stanów */
typedef enum {
parse_start_s = 0,
parse_finish_s,
parse_dvd_s,
parse_price_s,
parse_actor_s,
parse_year_made_s,
parse_valid_string_s,
parse_skip_string_s,
parse_unknown_s
} parse_state;
Deklarujemy strukturę do przechowywania informacji przekazywanych miedzy wywołaniami
zwrotnymi. Tą informacją jest stan parsera i liczba aktorów:
/* Struktura przechowująca między wywołaniami stan i liczbę aktorów */
typedef struct {
parse_state current_state;
int actors_this_movie;
} catalog_parse_state;
Deklarujemy teraz prototypy:
/* Prototypy wywołań zwrotnych */
static void start_document(void *ctx);
static void end_document(void *ctx);
static void start_element(void *ctx, const char *name, const char
**attrs);
static void end_element(void *ctx, const char *name);
static void chars_found(void *ctx, const char *chars, int len);
/* Funkje pomocnicze */
static parse_event get_event_from_name(const char *name);
static parse_state state_event_machine(parse_state curr_state,
parse_event
curr_event);
oraz strukturę wywołania zwrotnego:
static xmlSAXHandler mySAXParseCallbacks;
main()
Główna procedura realizuje kolejno następujące zadania:
tworzy parser,
konfiguruje wywołania zwrotne,
ustawia wskaznik na dane przechowujące stan między wywołaniami zwrotnymi,
żąda przetworzenia dokumentu,
usuwa wywołania zwrotne,
usuwa parser.
Kod tej procedury zawiera niewiele więcej wierszy niż powyższy opis:
int main() {
xmlParserCtxtPtr ctxt_ptr;
catalog_parse_state parsing_state;
memset(&mySAXParseCallbacks, sizeof(mySAXParseCallbacks), 0);
mySAXParseCallbacks.startDocument = start_document;
mySAXParseCallbacks.endDocument = end_document;
mySAXParseCallbacks.startElement = start_element;
mySAXParseCallbacks.endElement = end_element;
mySAXParseCallbacks.characters = chars_found;
ctxt_ptr = xmlCreateFileParseCtxt("dvdcatalog.xml");
if (!ctxt_ptr) {
fprintf(stderr, "Failed to create file parser\n");
exit(EXIT_FAILURE);
}
ctxt_ptr->sax = &mySAXParseCallbacks; /* Set callback map */
ctxt_ptr->usrData = &parsing_state;
xmlparseDocument(ctxt_ptr);
if (!ctxt_ptr->wellFormed){
fprintf(stderr, "Document not well formed\n");
}
ctxt_ptr->sax = NULL;
xmlFreeParserCtxt(ctxt_ptr);
printf("Parsing complete\n");
exit(EXIT_SUCCESS);
} /* main */
start_document()
To wywołanie zwrotne jest wzywane na początku rozbioru dokumentu. Zeruje ono informację
zawartą w maszynie stanu:
static void start_document(void *ctx) {
catalog_parse_state *state_ptr;
state_ptr = (catalog_parse_state *)ctx;
state_ptr->current_state = parse_start_s;
state_ptr->actors_this_movie = 0;
} /* start_document */
end_document()
To wywołanie zwrotne jest wzywane na zakończenie rozbioru dokumentu. Zmienia ono
informację zawartą w maszynie stanu w taki sposób, aby każde następne wywołanie zwrotne było
traktowane jako nieważne:
static void end_document(void *ctx) {
catalog_parse_state *state_ptr;
state_ptr = (catalog_parse_state *)ctx;
state_ptr->current_state = parse_finish_s;
} /* end_document */
start_element()
Wywołanie zwrotne start_element jest wzywane po każdym wykryciu początku elementu w
przetwarzanym dokumencie XML. Jego głównym zadaniem jest wywołanie maszyny stanu w celu
określenia jego nowej wartości. Dodatkowo zliczani są aktorzy oraz obsługiwane atrybuty
elementu dvd:
static void start_element(void *ctx, const char *name, const char
**attrs) {
const char *attr_ptr;
int curr_attr = 0;
catalog_parse_state *state_ptr;
parse_state curr_state;
parse_event curr_event;
state_ptr = (catalog_parse_state *)ctx;
curr_state = state_ptr->current_state;
curr_event = get_event_from_name(name);
state_ptr->current_state = state_event_machine(curr_state,
curr_event);
if (curr_event == parse_actor_e) {
state_ptr->actors_this_movie++;
}
if (curr_event == parse_actors_e) {
state_ptr->actors_this_movie = 0;
}
if (state_ptr->current-state == parse_dvd_s) {
/* Element DVD powinien mieć atrybuty */
printf("Element %s started\n", name);
if (attrs) {
attr_ptr = *attrs;
white(attr_ptr) {
printf("tAttribute %s\n", attr_ptr);
curr_attr++;
attr_ptr = *(attrs +curr_attr);
}
}
}
} /* start_element */
end_element()
To wywołanie zwrotne jest wzywane po wykryciu końca elementu. Wywołuje ono maszynę stanu
do obsługi tego zdarzenia:
static void end_element(void *ctx, const char *name) {
catalog_parse_state *state_ptr;
parse_state curr_state;
parse_event curr_event;
state_ptr = (catalog_parse_state *)ctx;
curr_state = state_ptr->current_state;
curr_event = parse_end_element_e;
state_ptr->current_state = state_event_machine(curr_state,
curr_event);
} /* end_element */
chars_found()
Funkcja chars_found jest wywoływana zawsze, gdy wykryty napis nie jest nazwą elementu,
komentarzem lub atrybutem. Na podstawie bieżącego stanu w maszynie stanu określany jest
sposób przetwarzania znaków. Elementy price i year_made są traktowane nieco odmiennie, aby
pokazać, jak maszyna stanu może wykrywać mieszaninę specyficznych i uogólnionych
elementów, używając stanu parse_valid_string_s dla elementów typu rodzimego:
/* W celu uproszczenia założyliśmy ograniczenie długości nazwy
zdarzenia
oraz przekazywanie wszystkich znaków podczas jednego wywołania */
#define CHAR_BUFFER 1024
static void chars_found(void *ctx, const char *chars, int len) {
char buff[CHAR_BUFFER + 1];
catalog_parse_state *state_ptr;
state_ptr = (catalog_parse_state *)ctx;
if (len > CHAR_BUFFER) len = CHAR_BUFFER;
strncpy(buff, chars, len);
buff[len] = '\0';
/* Uzależnienie sposobu obsługi napisu od stanu */
switch(state_ptr->current_state) {
case parse_start_s:
case_parse_finish_s:
case_parse_ded_s:
break;
case_parse_price_s:
printf("Price %s\n", buff);
break;
case_parse_actor_s:
printf("Actor %s (%d)\n", buff, state_ptr->actors_this_movie);
break;
case parse_year_made_s:
printf("Year %s\n", buff);
break;
case parse_valid_string_s:
printf("Other valid %s\n", buff);
break;
case parse_skip_string_s:
break;
case parse_unknown_s:
break;
default:
printf("DEBUG default case in chars_found %d\n", state_ptr-
>current_state);
break;
} /* switch */
} /* chars_found */
get_event_from_name()
Mamy także funkcję pomocniczą, która służy do przekształcania nazw elementów na wyliczone
zdarzenia:
/* Odwzorowanie nazw elementów na wyliczone zadarzenia */
const struct {
const char *name;
parse_event event;
} events[] = {
{"catalog", parse_catalog_e},
{"dvd", parse_dvd_e},
{"title", parse_title_e},
{"price", parse_price_e},
{"director", parse_director_e},
{"actor", parse_actor_e},
{"actors", parse_actors_e},
{"year_made", parse_year_made_e}
};
static parse_event get_event_from_name(const char *name) {
int i;
for (i = 0; i < sizeof(events)/sizeof(*events); i++) {
if (!strcmp(name, events[i].name)) return events[i].event;
}
return parse_other_e;
} /* get_event_from_name */
state_event_machine()
Na zakończenie mamy maszynę stanu, która określa nowy stan na podstawie podanego stanu
bieżącego i zdarzenia:
/* Przeszukiwanie maszyny stanu */
const struct {
const parse_event pe;
parse_statens;
} event_state[] = {
{parse_start_e, parse_start_s},
{parse_finish_e, parse_finish_s},
{parse_dvd_e, parse_dvd_s},
{parse_price_e, parse_price_s},
{parse_actor_e, parse_actor_s},
{parse_year_made_e, parse_year_made_s},
{parse_title_e, parse_valid_string_s},
{parse_director_e, parse_valid_string_s],
{parse_year_made_e, parse_year_made_s},
{parse_title_e, parse_valid_string_s},
{parse_director_e, parse_valid_string_s},
{parse_catalog_e, parse_skip_string_s},
{parse_actors_e, parse_skip_string_s},
{parse_other_e, parse_unknown_s},
{parse_end_element_e, parse_skip_string_s}
};
static parse_state_event_machine(parse_state curr_state, parse_event
curr_event) {
int i;
for (i = 0; i < sizeof(event_state)/sizeof(*event_state); i++ {
if (curr_event == event_state[i].pe) return event_state[i].ns;
}
return parse_unknown_s;
} /* state_event_machine */
Po uruchomieniu tego programu (z plikiem wejściowym skróconym z powodu braku miejsca w
książce) uzyskujemy bardzo przejrzysty wynik. Zwróćmy uwagę na to, że każdy aktor ma
przypisany numer, pod którym występuje w elemencie dvd, a zarówno reżyser, jak i tytuł filmu
mają dodany napis  Other valid . Pokazaliśmy więc, że można uprościć parser jeśli nie trzeba
rozróżniać znaczenia jakichś elementów, obsługując je w jednolity sposób!
Element dvd started
Attribute asin
Attribute 0780020707
Other valid Grand Illusion
Price 29.99
Other valid Jean Renoir
Actor Jean Gabin (1)
Year 1938
Element dvd started
Attribute asin
Attribute 0780020685
Other valid Seven Samurai
Price 27.99
Other valid Akira Kurosawa
Actor Takashi Shimura (1)
Actor Toshiro Mifune (2)
Year 1954
Parsing complete
Materiały zródłowe
Głównym miejscem do rozpoczęcia jakichkolwiek prac z XML jest strona ze standardami W3C
pod adresem: http://www.w3.org/xml.
Warto również zajrzeć do wersji standardu XML opatrzonej komentarzami, którą można znalezć
pod adresem: http://www.xml.com/pub/axml/axmlintro.html.
Strona macierzysta biblioteki libxml, poprzednio znanej jako gnome-xml, znajduje się pod
adresem: http://xmlsoft.org. Oprócz dokumentacji i odnośników do plików, które można pobrać,
jest to również dobre zródło odnośników do innych informacji o XML, które warto prześledzić.
Alternatywne zródło dokumentacji libxml można znalezć pod adresem
http://www.daa.com.au/~james/gnome/xml-sax/xml-sax.html.
Doskonałe zródło prac na temat XML z grupy Open Source można znalezć pod adresem
http://xml.apache.org/, zwłaszcza dotyczy to parsera XML o nazwie Xerces, który jest dostępny
w wersjach Java i C++, działa w systemie Linux i zastosowano w nim model DOM blisko
spokrewniony ze standardem W3C dla schematów XML.
Firma IBM wykonuje dużo prac dotyczących XML (i Linuksa), a więc strona
http://www.alphaworks.ibm.com/ jest często dobrym miejscem na zapoznanie się z nowymi
technologiami.
Dobrym zródłem wiedzy jest także strona Jamesa Clarka  http://www.jclark.com/.
Inny interfejs DOM o nazwie Gdome zbudowany na podstawie libxml można znalezć pod
adresem: http://levien.com/gnome/gdome.html.
Standard interfejsu SAX do rozbioru XML znajduje się pod adresem:
http://www.megginson.com/SAX.
Zestaw najczęściej zadawanych pytań na temat XML z odpowiedziami (FAQ) znajduje się pod
adresem: http://www.ucc.ie/xml.
Bezpłatny edytor XML (napisany w języku Java) znajduje się pod adresem:
http://www.merlotxml.org/.
Napisano także bardzo wiele książek o XML, ale trudno jest wskazać tę, od której warto zacząć.
Jedną z takich pozycji godnych przeczytania może być Professional XML, wyd. Wrox Press
(ISBN 1-861003-11-0).
Podsumowanie
W tym rozdziale omówiliśmy struktury dokumentu XML oraz specyfikacje DTD, definiujące te
struktury. Przedyskutowaliśmy także różnice między  dobrym sformatowaniem dokumentu XML
oznaczającym poprawność składniową, a  poprawnością dokumentu, potwierdzoną przez
związaną z nim specyfikację DTD.
Następnie skrótowo omówiliśmy dwa główne rodzaje parserów stosowanych do dokumentów
XML (model DOM i model SAX).
Pokazaliśmy także szczegółowo bibliotekę libxml, stanowiącą pierwotnie część interfejsu
graficznego GNOME, ale obecnie przekształconą do postaci samodzielnego narzędzia. Zawiera
ona parser działający na zasadzie SAX i wyposażony w interfejs programowy do języka C.
Na zakończenie rozdziału pokazaliśmy parser przetwarzający nasz plik catalog.xml.


Wyszukiwarka

Podobne podstrony:
23 01
Genetyka 5 (23 01 2013)
Podstawy rekreacji ćwiczenia 23 01 10x
ZNK 23 01
fiszki 01 23 i 24
Stromlaufplan Passat 23 Multifunktionslenkrad ab 01 2000
WSM 01 23 pl(1)
TI 02 01 23 T B pl(2)
01 Rozpoznawanie surowców skórzanych i skórid)23

więcej podobnych podstron