r 23 07 225OKYTCWJ7EI4ZMAPP5XPOXWU4BDDYXXCTK53A


Rozdział 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
R-23-07.doc Strona 1AGES |1|1}1
|1
43
43
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 raczej 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, ang. comma separated variable) 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, eXtensible Stylesheet Language), 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óż 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,
R-23-07.doc Strona 2AGES |1|1}1
|1
43
43
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 (ang. markup
tags) 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  mówią natomiast 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.
R-23-07.doc Strona 3AGES |1|1}1
|1
43
43
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, 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ść (ang. body) 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 to, ż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
R-23-07.doc Strona 4AGES |1|1}1
|1
43
43
.... 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:

...
R-23-07.doc Strona 5AGES |1|1}1
|1
43
43

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 to 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.
R-23-07.doc Strona 6AGES |1|1}1
|1
43
43
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
R-23-07.doc Strona 7AGES |1|1}1
|1
43
43
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
Komentarze 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 (--) ani 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
R-23-07.doc Strona 8AGES |1|1}1
|1
43
43

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, 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. Zanim rozpocznie się jego rozpowszechnianie, należy uzgodnić jego
strukturę. Wszystko to można osiągnąć, stosując DTD.
R-23-07.doc Strona 9AGES |1|1}1
|1
43
43
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, możemy zastosować dowolnie dużo
nawiasów, powiększając stopień złożoności naszej definicji.
R-23-07.doc Strona 10AGES |1|1}1
|1
43
43
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 (ang. sub-elements), 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:

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
R-23-07.doc Strona 11AGES |1|1}1
|1
43
43
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)>


R-23-07.doc Strona 12AGES |1|1}1
|1
43
43

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ę, tzw. schematy (ang. schemas). 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 zatem
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

R-23-07.doc Strona 13AGES |1|1}1
|1
43
43
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).
R-23-07.doc Strona 14AGES |1|1}1
|1
43
43
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 (ang. callbacks) 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,
R-23-07.doc Strona 15AGES |1|1}1
|1
43
43
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:
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() {
R-23-07.doc Strona 16AGES |1|1}1
|1
43
43
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; 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");
R-23-07.doc Strona 17AGES |1|1}1
|1
43
43
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:
R-23-07.doc Strona 18AGES |1|1}1
|1
43
43
$./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,
R-23-07.doc Strona 20AGES |1|1}1
|1
43
43
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,
R-23-07.doc Strona 21AGES |1|1}1
|1
43
43
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");
}
R-23-07.doc Strona 22AGES |1|1}1
|1
43
43
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 nazwane, a więc łatwo
można z nich skorzystać.
R-23-07.doc Strona 23AGES |1|1}1
|1
43
43
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.
R-23-07.doc Strona 24AGES |1|1}1
|1
43
43
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);
R-23-07.doc Strona 25AGES |1|1}1
|1
43
43
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);
R-23-07.doc Strona 26AGES |1|1}1
|1
43
43
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 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;
R-23-07.doc Strona 27AGES |1|1}1
|1
43
43
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) {
R-23-07.doc Strona 28AGES |1|1}1
|1
43
43
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
R-23-07.doc Strona 29AGES |1|1}1
|1
43
43
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.
R-23-07.doc Strona 30AGES |1|1}1
|1
43
43
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;
R-23-07.doc Strona 31AGES |1|1}1
|1
43
43
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.
R-23-07.doc Strona 32AGES |1|1}1
|1
43
43
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,
R-23-07.doc Strona 33AGES |1|1}1
|1
43
43
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,
R-23-07.doc Strona 34AGES |1|1}1
|1
43
43
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);
R-23-07.doc Strona 35AGES |1|1}1
|1
43
43
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;
R-23-07.doc Strona 36AGES |1|1}1
|1
43
43
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:
R-23-07.doc Strona 37AGES |1|1}1
|1
43
43
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;
R-23-07.doc Strona 38AGES |1|1}1
|1
43
43
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},
R-23-07.doc Strona 39AGES |1|1}1
|1
43
43
{"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},
R-23-07.doc Strona 40AGES |1|1}1
|1
43
43
{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)
R-23-07.doc Strona 41AGES |1|1}1
|1
43
43
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/a/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/, dotyczy to zwłaszcza 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ć XML. Vademecum profesjonalisty, wyd.
Helion (ISBN 83-7197-434-5).
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
R-23-07.doc Strona 42AGES |1|1}1
|1
43
43
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.
R-23-07.doc Strona 43AGES |1|1}1
|1
43
43


Wyszukiwarka

Podobne podstrony:
Atachment 13 09 23 07 18 38
R 23 07 (5)
23 07
23 07
instrukcja serwisowa termet gco 23 07 17 29 08
07 (23)
07 00 23 m3mi3av2fkgjslwyqiwx27ub4sne4556eeuvzxi
TI 97 07 23 N pl
23 T44 07 P415s
07 (23)
TI 97 07 23 N pl(1)

więcej podobnych podstron