XML lezen met SAX en XmlReader
Door Michiel van Otegem
28 november 2007
XML lezen met SAX en XmlReader
Er zijn meerdere mogelijkheden als je XML wilt lezen en manipuleren. Je kunt een XML document zelf
parsen, maar je kunt ook gebruik maken van bestaande parsers. Een de facto standaard hiervoor is
SAX. Door de opkomst van het .NET Framework wordt ook de XmlReader methode steeds belangrijker.
Die twee nemen we in dit artikel onder de loep.
In XML gegevensopslag is parsen
met het Document Object Model (DOM) behandeld. DOM is heel eenvoudig, en bovendien de standaard
die verzorgd wordt door het W3 Consortium (W3C), de belangrijkste standaardenorganisatie voor het
wereldwijde web. Een nadeel aan DOM is dat het een heel document eerst in moet lezen alvorens
ermee gewerkt kan worden. Vooral bij grote documenten is dit eigenlijk alleen maar lastig, omdat
er dan enorme hoeveelheden geheugen gebruikt worden. Bovendien kan het lang duren om een bepaald
element te vinden. Verder is het behoorlijk inefficiënt als je een document van meerdere megabytes
inleest, terwijl je alleen waardes van bepaalde elementen wilt weten. Dit betekent overigens niet
dat DOM nutteloos is, het is alleen niet geschikt voor iedere situatie. Er zijn echter voldoende
situaties waarbij het juist heel nuttig is om het hele document in het geheugen te hebben. In
andere situaties kun je grofweg gebruik maken van twee alternatieven voor DOM:
-Simple API for XML (SAX)
-XmlReader
SAX is oorspronkelijk gemaakt voor het Java platform, en wordt daar ook veruit het meest gebruikt.
Er zijn ook SAX implementaties voor andere platformen, maar die worden aanzienlijk minder gebruikt.
Door de impliciete verbintenis met Java is dit niet onbegrijpelijk. Sinds de komst van het
.NET Framework heeft men op het Windows platform met XmlReader ook een alternatief dat veel beter
aansluit op de Microsoft filosofie. Daarmee is de keuze voor SAX op Windows vrij ongewoon en
onnatuurlijk geworden.
SAX en XmlReader zijn allebei geen standaarden die zoals DOM door een standaardenorganisatie ontwikkeld worden of geratificeerd zijn. SAX is ontwikkeld buiten de standaardenorganisaties op het zelfde moment dat de W3C aan de ontwikkeling van DOM werkte. SAX is derhalve geboren uit de noodzaak iets te hebben waarmee XML verwerkt kon worden. SAX 1.0 stamt net als de eerste DOM versie uit 1998, en is daarmee in XML-termen stokoud. In 2001 is er een tweede versie van SAX uitgebracht, en dat is momenteel de dominante standaard. In vergelijking met DOM en SAX is XmlReader relatief nieuw. Het is uitgebracht als onderdeel van het .NET Framework in 2002. Omdat XmlReader onderdeel uitmaakt van het .NET Framework, heeft het per definitie een groot bereik. Het is dus snel gegroeid tot een veel gebruikte technologie.
SAX
De aanpak van SAX verschilt nogal van DOM. Met DOM vraag je elementen op uit het document dat in
het geheugen staat. Met SAX daarentegen worden elementen één voor één ingelezen. SAX stelt de
applicatie op de hoogte van wat het inleest met een parsing event, oftwel: een signaal. Het is
vervolgens aan de applicatie om te bepalen wat het met het parsing event doet. De applicatie kan
besluiten iets met het gelezen element te doen, of om het weg te gooien en te wachten op het
volgende parsing event. Als je niet gewend bent op deze manier te werken, klinkt dit erg vreemd.
Ook is het vreemd als je een XML document als een hiërarchie van elementen ziet, aangezien SAX
daar helemaal geen rekening mee houdt. Het leest ‘dom’ elementen in. Deze aanpak heeft echter een
aantal belangrijke voordelen. Ten eerste wordt er haast geen geheugen gebruikt. Alleen de elementen
die je besluit op te slaan nemen geheugen is, maar het is niet waarschijnlijk dat je ooit het hele
document in het geheugen hebt zitten. Ten tweede ben je niet gebonden aan de data structuur van
DOM. Je kunt zelf een structuur definiëren, en waarden die ingelezen worden door SAX aan die
structuur toekennen. Het is zeer waarschijnlijk dat een dergelijke representatie veel minder
geheugen inneemt dat het XML document, omdat een objectrepresentatie veel efficiënter is. Je hoeft
immers niet de structuur van het XML document te bewaren, want die heb je al vastgelegd in de
objectrepresentatie. Dit betekent uiteraard wel dat het document dat je inleest relevant moet zijn
voor de objectrepresentatie. Een laatste voordeel is dat SAX uitermate geschikt is als je maar een
gedeelte van de gegevens in het document nodig hebt. Je kunt de parser zijn werk laten doen, en
alleen werken met parsing events die van toepassing zijn op je applicatie. Dit is bijvoorbeeld bij
webservices handig. Een webservice aanroep bestaat uit een XML document dat bestaat uit een
"envelope" voor de adressering en afhandeling, en een "body" die informatie bevat voor de service
die aangeroepen wordt. Wanneer een webservice aangeroepen wordt, heeft het webservice mechanisme
alleen de gegevens in de envelope nodig, en moet het de body doorgegeven aan de applicatie door
middel van de gegevens in de envelope. De gegevens in de body zijn totaal niet interessant voor de
webservice, terwijl de envelope niet interessant is voor de onderliggende applicatie. Met SAX kun
je de overbodige actie van het inlezen van niet-relevante gegevens voorkomen.
SAX heeft uiteraard ook nadelen. Ten eerste betekent de manier van werken dat er geen mogelijkheid is voor zogenaamde Random Access. Dat wil zeggen dat het niet mogelijk is om te zeggen “wat is de inhoud van element X?”. Om die vraag te beantwoorden, moet SAX het hele document doorzoeken, of die informatie al ergens hebben opgeslagen. Aangezien SAX die opslag aan jou over laat, is dat laatste dus niet het geval. In dit soort gevallen is DOM veel handiger, omdat DOM het hele document in het geheugen houdt. Gerelateerd aan dit probleem is dat je geen complexe zoekacties kunt uitvoeren op een document. Je kunt dus moeilijk zeggen “geef me alle elementen X”, omdat ook daarmee het hele document doorlopen moet worden. DOM implementaties zijn daarentegen berekend op dit soort acties, en gaan hier dus efficiënt mee om.
De werkwijze van SAX wordt het best geïllustreerd aan de hand van een voorbeeld. Afbeelding 1 laat
de code zien van een simpele applicatie. Java is object georiënteerd, dus de applicatie wordt
gemaakt als een class (NetOpus) die gebaseerd is op de DefaultHandler class. Dat is de standaard
class die gebruikt wordt voor het maken van SAX2 parsers. De class begint met een main methode
die dient als beginpunt van de applicatie. Deze wordt automatisch aangeroepen door de Java omgeving
als je de applicatie start. De main methode initieert de applicatie door achtereenvolgens een XML
parser op te zetten en deze te voeden aan de hand van een bestand dat je meegeeft op de command
line.
1: //Importeer libraries
2: import java.io.FileReader;
3: import org.xml.sax.XMLReader;
4: import org.xml.sax.Attributes;
5: import org.xml.sax.InputSource;
6: import org.xml.sax.helpers.XMLReaderFactory;
7: import org.xml.sax.helpers.DefaultHandler;
8:
9: public class NetOpus extends DefaultHandler
10: {
11: //Start van de applicatie
12: public static void main (String args[])
13: throws Exception //Standaard foutafhandeling
14: {
15: //XML parser op te zetten
16: XMLReader xr = XMLReaderFactory.createXMLReader();
17: NetOpus handler = new NetOpus();
18: xr.setContentHandler(handler);
19: xr.setErrorHandler(handler);
20:
21: //Lees bestand van command line
22: FileReader r = new FileReader(args[0]);
23: xr.parse(new InputSource(r));
24: }
25:
26: //Constructor
27: public NetOpus()
28: {
29: super();
30: }
31:
32: //---Parsing event handlers---
33: public void startDocument ()
34: {
35: System.out.println("Start");
36: }
37:
38: public void endDocument ()
39: {
40: System.out.println("Einde");
41: }
42:
43: public void startElement (String uri, String name,
44: String qName, Attributes atts)
45: {
46: if (name.equals("NetOpus") &&
47: atts.getValue("artikel") != null)
48: {
49: System.out.println("NetOpus: " + atts.getValue("artikel"));
50: }
51: }
52:
53: public void endElement (String uri, String name, String qName)
54: {
55: //Afhandeling eind element hier
56: }
57:
58: public void characters (char ch[], int start, int length)
59: {
60: //Karakter afhandeling hier
61: }
62: }
Afbeelding 1: SAX voorbeeld applicatie (Java)
Wanneer main uitgevoerd is, gaat de SAX parser elementen inlezen en komen de parsing events aan de
beurt. Dat begint met startDocument als het begin van het document gelezen is, en endDocument als
de parser het hele document gehad heeft. In het voorbeeld leveren die parsing events niet meer dan
een stukje tekst die aangeven wat er gebeurd is. Veel interessanter is startElement, omdat daarmee
wordt aangegeven dat de parser de begintag van een element gevonden heeft. Het geeft daarbij alle
naam informatie mee over het element, en ook de attributen die bij het element horen. In het
voorbeeld kijken we of de naam van het element gelijk is aan NetOpus, en of het element een artikel
attribuut heeft. Als dat zo is, wordt een tekst geschreven met daarin NetOpus gevolgd door de naam
van het artikel. In het voorbeeld gebeurt er niets als de eind tag gevonden wordt, en wordt er ook
niets gedaan met het characters parsing event. Met die laatste wordt tekst tussen tags afgehandeld.
In Afbeelding 2 staat een voorbeeld van een XML document dat je kunt lezen met de applicatie. Er
vanuit gaande dat Afbeelding 2 opgeslagen is als netopus.xml, start je de applicatie van de command
line als volgt:
java -Dorg.xml.sax.driver=gnu.xml.aelfred2.SAXDriver NetOpus netopus.xml
1: - <Magazines>
2: <NetOpus artikel="XML Deel 4 - SAX" />
3: <NetOpus artikel="XML Deel 5 - XSLT" />
4: <ASPNL artikel="ASP.NET Handleiding" />
5: </Magazines>
Afbeelding 2: Invoer voorbeeld
De SAX implementatie is gebaseerd op een verzameling interfaces. Deze interfaces bepalen de
signatuur van de parsing events. Voor alle SAX implementaties is deze signatuur hetzelfde, waardoor
de code vrijwel onafhankelijk is van de daadwerkelijk gebruikte parser. Met name in de Java wereld
geldt daarom dat een applicatie die gebruik maakt van SAX door het wijzigen van enkele regels code
een andere parser kan gebruiken. Omdat vrijwel elke Java implementatie een SAX parser bevat, is
het zelfs zo dat je aan de Java omgeving kunt vragen om een generieke SAX parser, zodat je de code
überhaupt niet gewijzigd hoeft te worden. Je kunt echter ook een specifieke parser aanroepen, maar
dan moet je wel zeker weten dat die beschikbaar is. In het voorbeeld wordt echter gebruik gemaakt
van de generieke parser, en wordt op de command line aangegeven welke "driver" (zeg maar parser)
gebruikt moet worden. In dit geval wordt gebruik gemaakt van de Ælfred2 SAX parser, maar je zou
ook een andere op kunnen geven, zoals bijvoorbeeld Xerces (org.apache.xerces.parsers.SAXParser).
Als de parser die je aangeeft beschikbaar is in de Java omgeving, wordt de applicatie uitgevoerd,
en krijg je Afbeelding 3 als resultaat. Merk op dat het ASPNL artikel niet in het resultaat staat,
omdat daarop gefilterd wordt in startElement.
1: Begin
2: NetOpus: XML Deel 4 - SAX
3: NetOpus: XML Deel 5 - XSLT
4: Einde
Afbeelding 3: Uitvoer van Afbeelding 2 gelezen met Afbeelding 1
XmlReader
SAX en XmlReader komen erg overeen, maar gebruiken een omgekeerde aanpak. Met SAX is de parser
leidend en roept de applicatie aan. Met XmlReader is dit juist andersom. De applicatie vraagt
telkens om het volgende element. Deze aanpak geeft ontwikkelaars iets meer controle over het
parsing proces, en geeft daardoor bijvoorbeeld de mogelijkheid om de parser te vertellen onder
welke voorwaarden je het volgende element wilt hebben. Zo kun je dus eenvoudig een gedeelte van
een document overslaan, zonder dat jouw code uit moet zoeken of het element dat je gekregen hebt
wel relevant is. Uiteindelijk betekent dit dat de verwerking een fractie sneller is dan met SAX
omdat de parsing events altijd plaatsvinden, en jouw code dus altijd wat moet doen. Een ander
voordeel is dat je met XmlReader meerdere documenten tegelijk kunt lezen, iets wat met SAX niet
kan, omdat parsing events aan één document gekoppeld zijn.
Afbeelding 4 laat een voorbeeld zien van een XmlReader applicatie die hetzelfde resultaat geeft
als de SAX applicatie uit Afbeelding 1. Je ziet dat de structuur anders is, omdat er geen sprake
is van events. In tegenstelling tot de SAX applicatie, moet je expliciet het volgende element
opvragen. Dit wordt gedaan in een ‘loop’ totdat het einde is bereikt. Verder moet je zelf bepalen
wat voor element je binnen krijgt, terwijl SAX dat afhandelt doordat je een bepaald parsing event
ontvangt. De afhandeling van het element is vervolgens overeenkomstig, hoewel de details wat anders
zijn. Voordat je de applicatie kunt uitvoeren, dien je deze eerst te compileren. Met het .NET
Framework geïnstalleerd en Afbeelding 4 opgeslagen als netopus.cs, doe je die met het volgende
commando:
csc netopus.cs
Vervolgens kun je de applicatie uitvoeren met:
netopus.exe netopus.xml
1: using System;
2: using System.Xml;
3:
4: namespace NetOpus
5: {
6: class XmlReaderVoorbeeld
7: {
8: //Startpunt van de applicatie
9: static void Main(string[] args)
10: {
11: string filename = args[0];
12:
13: //XmlReader initialiseren
14: XmlTextReader xr = new XmlTextReader(filename);
15: xr.WhitespaceHandling = WhitespaceHandling.None;
16:
17: Console.WriteLine("Begin");
18:
19: //Door document heen lopen en elementen opvragen
20: while(xr.Read()) {
21: //Alleen elementen afhandelen (geen tekst enz.)
22: if(xr.NodeType == XmlNodeType.Element)
23: {
24: //Controle op NetOpus element
25: if(xr.Name == "NetOpus" &&
26: xr.HasAttributes)
27: {
28: //Controle op artikel attribuut
29: string artikel = xr.GetAttribute("artikel");
30: if(artikel != null)
31: {
32: Console.WriteLine("NetOpus: " + artikel);
33: }
34: }
35: }
36: }
37: Console.WriteLine("Einde");
38: }
39: }
40: }
Afbeelding 4: XmlReader voorbeeld applicatie (C#)
Tot slot
SAX en XmlReader zijn beide heel krachtig, maar wel lastiger om mee te werken dan DOM. Er zijn
echter behoorlijk wat situaties waarin SAX en XmlReader veel betere keuzes zijn dan DOM. Het
aardige van beide architecturen is ook dat ze documenten kunnen inlezen die niet XML zijn, maar
doorgegeven kunnen worden aan de applicatie alsof dit wel zo is. Ten opzichte van DOM is dit vrij
eenvoudig te realiseren. Op XML Tools Update vind je een hele serie
van XmlReader implementaties die gebruik maken van deze mogelijkheid.
Dit artikel is eerder verschenen in NetOpus, september 2004.
|