XMLReader
Rozszerzenie XMLReader jest kolejnym, obok DOM oraz SimpleXML, służącym do parsowania XML-a i opartym w całości na potężnej bibliotece libxml2. Pojawiło się w repozytorium PECL wraz z wydaniem PHP 5.0, zaś od PHP 5.1 jest dostępne i domyślnie aktywne w standardowej instalacji. Zarówno DOM, jak i SimpleXML udostępniają programiście gotowe drzewo siedzące w pamięci RAM, które odzwierciedla strukturę wczytanego dokumentu. Ma to swoje zalety i wady. Jest bardzo przydatne, gdy interesują nas po prostu dane przechowywane w dokumencie, gdyż mamy do nich bardzo prosty, obiektowy dostęp. Problemy zaczynają się, gdy chcemy czegoś więcej. Dokumenty XML to nie tylko dane, ale również ich struktura i dodatkowe elementy w stylu DTD, atrybutów xmlns itd. Gdy chcemy napisać narzędzie, które przetwarza jeden dokument XML na inny według naszych własnych reguł, nie zdadzą one egzaminu. Powód jest prosty: jako programiści nie będziemy mieć dostępu do części struktury, ponieważ Libxml podczas wczytywania zachowuje niektóre informacje wyłącznie do własnej wiadomości i nie udostępnia na zewnątrz. Tutaj właśnie przydaje się XMLReader. Zasada jego działania jest nieco podobna do wcześniejszych, przedpotopowych rozszerzeń XML-owych z PHP4. Nie budujemy tutaj drzewa w pamięci, lecz parsujemy dokument w locie i w locie jesteśmy powiadamiani o wystąpieniach kolejnych elementów dokładnie w takiej kolejności, w jakiej się one pojawiają. Co więcej, XMLReader daje nam dostęp do wewnętrznych informacji XML-a, które pozostałe rozszerzenia przed nami ukrywają. XMLReader wykorzystuję w nowym parserze dla Open Power Template'a, który będzie domyślnie użyty w wersji 2.1. W przeciwieństwie do autorskich wynalazków, XMLReader w locie także analizuje DTD oraz parsuje encje zgodnie z regułami języka.
Podstawowe użycie
XMLReader jest rozszerzeniem napisanym obiektowo. Aby rozpocząć pracę, musimy utworzyć obiekt XMLReader, wczytać do niego dokument XML oraz ustawić kilka reguł parsowania. Aby przejść do kolejnego elementu, stosujemy metodę read(), która sprawia, że publiczne pola obiektu zostają wypełnione informacjami o ostatnio wczytanym dokumencie.
$reader = new XMLReader; $reader->open('./dokument.xml'); // tutaj mozemy konfigurowac reguly parsowania while($reader->read()) { switch($reader->nodeType) { case XMLReader::ELEMENT: echo 'Znalazlem element o nazwie '.$reader->name.'<br/>'; break; // itd. } } $reader->close();
Dokument XML może być wczytany z zewnętrznego pliku. Otwarcie strumienia realizowane jest przez metodę open(), która spodziewa się URI do dokumentu, który ma otworzyć. Po zakończeniu parsowania strumień wejściowy musi być zamknięty metodą close(). Jeżeli nasz dokument przechowujemy w pamięci w postaci ciągu tekstowego, możemy wczytać go metodą xml(), którą wywołujemy zamiast open().
Podstawowe źródło informacji o elemencie to pole nodeType. Jego wartością jest jedna z kilkunastu stałych klasowych opisujących rodzaj napotkanego węzła, np. ELEMENT. Dzięki instrukcji switch możemy zdecydować, jaką akcję podjąć po napotkaniu węzła określonego typu.
Obsługa błędów
Spróbuj wprowadzić do XMLReadera nieprawidłowy dokument XML. Napotkamy tutaj jedną z bardziej irytujących wad PHP, czyli niesamowitą niekonsekwencję w obsłudze błędów. Mimo iż język posiada mechanizm wyjątków, który byłby tutaj wręcz idealny, jesteśmy zalewani falą wiadomości Warning wyświetlających się bez ładu i składu, co zaprzecza idei eleganckiej obsługi błędów. Niestety nie zmusimy rozszerzenia do rzucania wyjątków, ale ostrzeżeń da się pozbyć. Obsługa błędów w rozszerzeniach korzystających z libxml2 kontrolowana jest przez wspólny zestaw specjalnych funkcji. Nasz kod po poprawieniu będzie wyglądać następująco:
// Uzyj obslugi bledow wbudowanej w libxml libxml_use_internal_errors(true); // Tutaj tradycyjny kod do parsowania $reader = new XMLReader; // ... while(@$reader->read()) { // ... } $reader->close(); // Sprawdzamy, czy nie bylo bledow w trakcie parsowania $errors = libxml_get_errors(); if(sizeof($errors) > 0) { foreach($errors as $error) { echo $error->message.' ('.$error->line.')<br/>'; } }
Kluczowym elementem jest wywołanie funkcji libxml_use_internal_errors(), która przełącza tryb obsługi błędów biblioteki libxml i sprawia, że PHP nie rzuca już ostrzeżeń o każdą możliwą głupotę. Niestety, metoda read() w dalszym ciągu czuje się w obowiązku rzucenia ostrzeżenia, że wystąpił JAKIŚ błąd, bez wnikania w szczegóły i póki co jedynym znanym mi sposobem pozbycia się go jest wykorzystanie operatora śmierci @.
Po zakończeniu parsowania sprawdzamy bufor błędów biblioteki. Jeśli zwrócona tablica ma rozmiar większy niż 0, oznacza to, że zawiera ona przynajmniej jeden obiekt libXMLError, gdzie znajdziemy bardziej szczegółowe informacje o problemie:
- level - poziom powagi błędu: LIBXML_ERR_WARNING, LIBXML_ERR_ERROR lub LIBXML_ERR_FATAL,
- code - numeryczny kod błędu,
- message - komunikat błędu,
- file - plik XML, w którym wystąpił błąd,
- line - linia, w której wystąpił błąd.
Listę kodów błędów można znaleźć pod tym adresem.
Przetwarzanie atrybutów
Metoda read() nie schodzi do wnętrza znaczników, dlatego do obsługi atrybutów musimy wykorzystać trochę dodatkowego kodu. XMLReader umożliwia nam dostęp do konkretnego atrybutu poprzez metodę getAttribute(), zwracając jego wartość, lub zwykły przegląd listy atrybutów. Poniższy kod umieszczamy w sekcji do parsowania węzłów ELEMENT:
if($reader->moveToFirstAttribute()) { do { echo 'Znalazłem atrybut '.$reader->name.': '.$reader->value.'<br/>'; } while($reader->moveToNextAttribute()); $reader->moveToElement(); }
Metodą moveToFirstAttribute() sprawdzamy, czy znacznik w ogóle ma jakieś atrybuty. Jeśli uda nam się tam przeskoczyć, rozpoczynamy pętlę, którą jedziemy po wszystkich po kolei w takiej kolejności, w jakiej pojawiają się one w dokumencie. Użyłem pętli do.. while, ponieważ musi ona zawsze wykonać się co najmniej raz (na wejściu już wskoczyliśmy do pierwszego atrybutu), a ponadto po jej zakończeniu musimy metodą moveToElement() powrócić do listy elementów.
Właściwości atrybutów zrzucane są do pól obiektu klasy XMLReader: nazwę możemy odczytać z pola name, a wartość z pola value. Nadmieńmy, że pole nazwy zawiera także dołączoną przestrzeń nazw, dlatego nie musimy jej specjalnie w tym celu wyciągać. Jednak gdybyśmy potrzebowali sprawdzić, co to jest za przestrzeń, jej nazwa jest zapisana w polu prefix. Przypominam, że XMLReader jest na tyle uczciwy, że nie pomija przy parsowaniu przestrzeni xmlns oraz atrybutu xmlns, które są kluczowym elementem określania używanych przestrzeni nazw w dokumencie. Wciąż mamy do nich dostęp i jeśli pojawią się one w parsowanym kodzie, na pewno zobaczymy je na naszym wyjściu i będziemy mogli coś z nimi zrobić.
Encje i parsowanie DTD
XMLReader potrafi także parsować sekcję DTD zawartą w dokumencie oraz sprawdzać kod pod kątem jego poprawności z zapisanych w niej komend. Wymaga to włączenia dwóch opcji:
$reader->open('...'); $reader->setParserProperty(XMLReader::VALIDATE, true);
Teraz każdy błąd użycia znaczników zostanie zgłoszony w identyczny sposób, jak błąd składni. Radzę z tymi opcjami jednak uważać, szczególnie gdy chcemy tak analizować dokumenty HTML. Czasami dostęp do DTD na stronach W3C może być z jakiegoś powodu zablokowany (przeciążanie łącza, itd.). W tym wypadku libxml nie będzie w stanie pobrać DTD, a my zostaniemy zalani informacjami o nieznanych znacznikach. Jeżeli chcemy jedynie załadować DTD bez analizy dokumentu pod tym kątem, zamiast VALIDATE włączamy opcję LOADDTD.
W dokumentach XML encje definiowane są przez DTD. Dlatego parsując nasz własny dokument nie możemy stosować znanych z HTML-a encji typu Aacute, dopóki jej sobie nie zdefiniujemy. Wystąpienie encji w dokumencie sygnalizowane jest rozpoznaniem węzła ENTITY_REF, zaś w name dostajemy nazwę wczytanej encji. Jest jednak mały problem, bowiem nazwie nie towarzyszy wartość, którą zdefiniowaliśmy w DTD :). Aby tak szczegółowo się nie bawić, wystarczy po prostu nakazać parserowi, aby zamieniał encje na odpowiadające im wartości w trakcie parsowania:
$reader->setParserProperty(XMLReader::SUBST_ENTITIES, true);
Węzły ENTITY_REF wciąż będą pojawiać się na wyjściu, lecz tym razem możemy je po prostu ignorować.
Konwersja
Jak wspomniałem na początku, rozszerzeń opartych na libxml2 można używać wymiennie i w locie konwertować dokument z jednej postaci do drugiej. Nie kosztuje to wiele, gdyż wszystko i tak trzymane jest w tej samej, wewnętrznej reprezentacji. Zmienia się jedynie opakowanie, pod którym jest ona dostępna dla skryptów. W naszym przypadku możemy w każdej chwili przekonwertować aktualnie parsowany węzeł na drzewo DOM metodą expand(), a stamtąd jest już tylko krok do bardziej zaawansowanych możliwości oraz SimpleXML-a:
$domNode = $reader->expand();
Zakończenie
XMLReader to znakomite rozszerzenie dla osób, które potrzebują bardziej precyzyjnej kontroli nad dokumentem lub chcą wyłuskać jedną, konkretną informację. Zamiast pisać skomplikowany kod, który przechodzi przez drzewo DOM w poszukiwaniu jednego, konkretnego znacznika, po prostu czytamy wejście i czekamy, aż po wykonaniu funkcji read() pojawi nam się nasz szukany węzeł. Jedyne, co boli, to idiotyczny sposób obsługi błędów, który aż prosi się o zastąpienie wyjątkami. Nie wiem, może jak znajdę czas, pokuszę się o przygotowanie jakiegoś patcha na kod źródłowy rozszerzenia, który to poprawi? Pożyjemy, zobaczymy.





