R-23-01, Programowanie Linux


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):

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>

<!DOCTYPE catalog [

<!ELEMENT catalog (dvd+) >

<!ELEMENT dvd (title, price, director, actors, year_made)>

<!ATTLIST dvd

asin CDATA #REQUIRED >

<!ELEMENT title (#PCDATA)>

<!ELEMENT price (#PCDATA)>

<!ELEMENT director (#PCDATA)>

<!ELEMENT actors (actor+)>

<!ELEMENT actor (#PCDATA)>

<!ELEMENT year_made (#PCDATA)>

]>

<catalog>

<dvd asin="0780020707">

<title>Grand Illusion</title>

<price>29.99</price>

<director>Jean Renoir</director>

<actors>

<actor>Jean Gabin</actor>

</actors>

<year_made>1938</year_made>

</dvd>

<dvd asin="0780020685">

<title>Seven Samurai</title>

<price>27.99</price>

<director>Akira Kurosawa</director>

<actors>

<actor>Takashi Shimura</actor>

<actor>Toshiro Mifune</actor>

</actors>

<year_made>1954</year_made>

</dvd>

...

</catalog>

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 rozgryźć 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:

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 źró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:

Niezależnie od tego, że pierwotne wersje HTML koncentrowały się wokół opisu elementów dokumentu (np. „to jest nagłówek”), to później 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.

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 HTML znacznik <H1> jest traktowany tak samo jak znacznik <h1>, 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 źró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:

<?xml version="1.0"?>

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:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>

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 <!DOCTYPE (tak jak w 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 <HTML>...</HTML>. 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 </P>). 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ć:

<my_tag_name>The data content goes here</my_tag_name>

W pełni dozwolony jest brak zawartości między parą znaczników. Oznacza to, że zamiast pisać:

<my_tag_name> </my_tag_name>

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

<my_tag_name/>

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:

<TABLE BORDER="2" CELLPADDING="10">

...

</TABLE>

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

<my_tag_name text_type="example">The data content goes here</my_tag_name>

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:

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:

<B>Hello<I>Word</B></I>

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 <!-- i kończy znakami -->.

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:

<dvd asin="0780020707">

<title>Grand Illusion</title>

<price>29.99</price>

<director>Jean Renoir</director>

<actors>

<actor>Jean Gabin</actor>

</actors>

<year_made>1938</year_made>

</dvd>

<director>Jean Renoir</director>

<wibble>Black Adder</wibble>

<year_made>1954</year_made>

<dvd asin="0780020685">

<title>Seven Samurai</title>

<price>27.99</price>

<director>Akira Kurosawa</director>

<actors>

<actor>Takashi Shimura</actor>

<actor>Toshiro Mifune</actor>

</actors>

<year_made>1954</year_made>

</dvd>

Co oznaczają np. poniższe elementy?

<director>Jean Renoir</director>

<wibble>Black Adder</wibble>

<year_made>1954</year_made>

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 znaleźć 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”.

Łatwo 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 źródłowymi wskazanymi na końcu tego rozdziału. Podstawę DTD stanowi deklaracja ELEMENT, która ma następującą postać:

<!ELEMENT mytagname ... >

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:

<!ELEMENT sandwich (bread, (honey | jelly), bread) >

Fragment XML spełniający tę specyfikację powinien mieć postać:

<sandwich><bread>/><honey/><bread/></sandwich>

Załóżmy teraz, że wypełnienie kanapki ma być opcjonalne. Dodajemy więc odpowiedni kwantyfikator:

<!ELEMENT sandwich (bread, (honey | jelly)?, bread) >

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:

<!ELEMENT catalog (dvd+) >

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:

<!ELEMENT dvd (title, price, director, actors, year_made)>

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

<!ELEMENT actors (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:

<!ATTLIST dvd

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:

<!ATTLIST nazwa_znacznika nazwa_atrybutu typ_danych_atrybutu 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 <wartość>

Atrybut musi mieć podaną wartość

<wartość domyślna>

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:

<!ELEMENT title (#PCDATA)>

<!ELEMENT price (#PCDATA)>

<!ELEMENT director (#PCDATA)>

<!ELEMENT actor (#PCDATA)>

<!ELEMENT year_made (#PCDATA)>

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

<!ELEMENT catalog (dvd+) >

<!ELEMENT dvd (title, price, director, actors, year_made)>

<!ATTLIST dvd

asin CDATA #REQUIRED >

<!ELEMENT title (#PCDATA)>

<!ELEMENT price (#PCDATA)>

<1ELEMENT director (#PCDATA)>

<!ELEMENT actors (actor+)>

<!ELEMENT actor (#PCDATA)>

<!ELEMENT year_made (#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 <!DOCTYPE określającego typ dokumentu:

<!DOCTYPE catalog [

<!ELEMENT catalog (dvd+) >

<!ELEMENT dvd (title, price, director, actors, year_made)>

<!ATTLIST dvd

asin CDATA #REQUIRED >

<!ELEMENT title (#PCDATA)>

<!ELEMENT price (#PCDATA)>

<!ELEMENT director (#PCDATA)>

<!ELEMENT actors (actor+)>

<!ELEMENT actor (#PCDATA)>

<!ELEMENT YEAR_MADE (#pcdata)>

]>

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 znaleźć najświeższe informacje (adres jest podany w wykazie źró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 źró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:

Jeżeli na komputerze z systemem Linux jest już zainstalowany pakiet GNOME, to prawie na pewno można znaleźć 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 znaleźć 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 źró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:

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 <stdlib.h>

#include <stdio.h>

#include <parser.h>

#include<parserInternals.h>

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 <B>:

<dvd asin="0780020707">

<title>Grand Illusion</title>

<price>29.99</price>

<director>Jean<B>Renoir</director>

<actors>

<actor>Jean Gabin</actor>

</actors>

<year_made>1938</year_made>

</dvd>

Uruchamiamy parser ponownie:

$./sax1

dvdcatalog.xml:7: error: Opening and ending tag mismatch: B and director

<director>Jean<B>Renoir<director>

^

dvdcatalog.xml:12: error: Opening and ending tag mismatch: director and dvd

</dvd>

^

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

</catalog>

^

dvdcatalog.xml:26: error: detected an error in element content

^

dvdcatalog.xml:26: error: Premature end of data in tag <catalog>

<dvd asin="07800

^

Document not well formed

Parsing complete

Upewniliśmy się już, że parser rzeczywiście przetwarza nasz dokument i podaje użyteczne komunikaty po wykryciu błędów.

Zanim przejdziemy do omawiania dalszych zagadnień, musimy przywrócić plik XML do pierwotnego stanu. Sposób obsługi błędów zgłaszanych przez parser omówimy nieco później, gdy już staną się jasne metody dołączania własnych funkcji wywołań zwrotnych, które można będzie użyć jeśli domyślna obsługa błędów nie będzie wystarczająca.

Informacja o dokumencie

W naszym pierwszym przykładzie występowało wyrażenie:

ctxt_ptr->wellFormed

Umożliwiało ono sprawdzenie, czy dokument jest poprawnie sformatowany. Biblioteka libxml zawiera także kilka innych użytecznych elementów w tej kontekstowej strukturze. Przeglądając plik parser.h zobaczymy zdefiniowany typ _xmlParserCtxt, który zwiera kilka elementów, a między innymi informacje o wersji XML i sposobie kodowania znaków. Można je wykorzystać do uzyskania pełniejszej informacji o pliku przetwarzanym przez parser. Użyjemy tej właściwości w programie sax2.c, różniącym się od sax1.c tylko następującym fragmentem:

if (!ctxt_ptr->wellFormed) {

fprintf(stderr, "Document not well formed\n");

}

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 znaleźć 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:

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 wskaźniki 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, wskaźniki 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ć wskaźnik 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 wskaźniki do używanych funkcji wywołań zwrotnych. Każdy, kto używał struktur wywołań zwrotnych dobrze wie, jaki chaos może spowodować wpisanie wskaźnika 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 wskaźnikowi 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 sprawdźmy, 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 wskaźnika 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 <stdarg.h>), 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ą wskaźnikó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 wskaźniki: 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 <fud/>). 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 <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <parser.h>

#include <parserInternals.h>

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ę:

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 wskaźniku do struktury wywołań zwrotnych istnieje wskaźnik 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 wskaźnik do naszej struktury jest przekazywany jako pierwszy argument (wskaźnik 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 znaleźć 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 wskaźnik:

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 wskaźnik 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 źró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 <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <parser.h>

#include <parserInternals.h>

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:

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 źró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 znaleźć pod adresem: --> http://www.xml.com/pub/axml/axmlintro.html.[Author:MH]

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 źródło odnośników do innych informacji o XML, które warto prześledzić. Alternatywne źródło dokumentacji libxml można znaleźć pod adresem http://www.daa.com.au/~james/gnome/xml-sax/xml-sax.html.

Doskonałe źródło prac na temat XML z grupy Open Source można znaleźć 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 źró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 znaleźć 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.

25 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

25 F:\helion\oryginaly\R-23-t.doc

nieaktualny!



Wyszukiwarka

Podobne podstrony:
Podstawy rekreacji ćwiczenia 23 01 10x
brzuch i miednica 2003 2004 23 01
23 - Funkcje, Programowanie, Klasa III
2543 2007 2 17 23 01 07
23 01
Ćwiczenia 8 (23 01 15)
Finanse publiczne i prawo finansowe – dr J. Stankiewicz 23-01-05r, Finanse publiczne i prawo finanso
SKRYPT- matematyka finansowa, Szpital Miejski Gdańsk - Zaspa Gdańsk, 23.01.1996
Wyniki sprawdzianu ze statystyki matematycznej i teorii estymacji z dn 23.01.13
01 program literatura
Nauka czytania w klasie I prezentacja 23 01
ściąga na kolokwium 23.01, Socjologia, metody badań socjologicznych

więcej podobnych podstron