A Python programozási nyelv

XML

Az XML formátum napjainkban nagy, és egyre növekvő népszerűségének okait, a használatának előnyeit hosszasan lehetne taglalni, most azonban csak az XML formátum Pythonnal való kezelésének lehetőségeit vizsgáljuk meg.

A Python a "Batteries included" filozófiának megfelelően természetesen rendelkezik beépített lehetőségekkel az XML dokumentumok kezelésére, a lehetőségeink azonban ezekkel koránt sem merültek ki. Az XML-SIG* célja, hogy a nyelvhez olyan eszközöket készítsenek, melyek a Pythont az elsődleges XML-kezelő nyelvvé emelik. * A Python felhasználói és fejlesztői SIG-ekbe (Special Interest Group) tömörülnek, hogy bizonyos területeken a nyelv fejlődését elősegítsék, illetve gyorsítják. Ezek egyike az XML-SIG, teljes nevén Special Interest Group for XML Processing in Python. Ezen kívül jelenleg körülbelül tíz SIG működik többek közt a képfeldolgozás, az adatbázis-kezelés, illetve a Python webes -kliens és szerver oldali- alkalmazásának elősegítésért.

Más csoportok már elérték céljukat és befejezték működésüket, de levelezőlistáik archívuma továbbra is elérhető. Ide tartoznak például a threadekkel, grafikus felülettel, matematikai számításokkal foglalkozó csoportok.

Lehetőségek röviden

A nyelv, mint említettem elve több lehetőséget biztosít az XML dokumentumok kezelésére, így lehetőségünk van a feladat jellegének megfelelő módszert választani.

xmllib
A 2.0-s verzió óta deprecated, vagyis használata nem javasolt!
Ha nem akarjuk reguláris kifejezésekkel nulláról kezdeni az XML-kezelést, akkor ez a legalacsonyabb szintű megoldás, amit a Python nyújt. Stream alapú megoldásról van szó, ami azt jelenti, hogy a program folyamatosan kapja meg az XML kód részleteit, és az egyes XML-tagek megtalálásakor az általunk az adott taghez rendelt függvényt hajtja végre a program. Egy beépített osztályból való származtatással, illetve a származtatott osztályban meglévő műveletek felüldefiniálásával és új metódusok definiálásával hozhatunk létre a saját XML formátumunkhoz illeszkedő parsert. Nem végez validálást, vagyis nem tudja megállapítani, hogy egy adott DTD szerint a dokumentum helyes-e.

SAX (Simple API for XML)
Működésében nagyon hasonlít az xmllibhez, de annál kicsit magasabb szinten való XML-kezelést tesz lehetővé. Szintén stream alapú, eseményvezérelt módszer, de már tudja ellenőrizni, hogy a dokumentum megfelel-e egy adott DTD-nek, és a hibakezelésre is ad lehetőséget. A parser fölé egy ún. handlert rendelünk, ez végzi munka a lényegi (magasabb szintű) részét.

DOM (Document Object Model)

Az XML dokumentum fa-szerkezetű reprezentációját adja, vagyis az előzőekkel ellentétben egyszerre beolvassa az egész dokumentumot a memóriába, ahol a fa-szerkezet tetszőlegesen bejárható és módosítható, majd visszaírható a lemezre. Új XML állományok létrehozására is kényelmesen használható, hiszen csak egy megfelelően definiált osztály egy objektumának adattagjait kell a szükséges adatokkal kitölteni, majd egyetlen metódushívással létrejön az új XML fájl. Eredetileg sem a SAX-ot, sem a DOM-ot nem a Pythonhoz találták ki, mindkettőnek vannak más nyelvű implementációi is.

SAX

Ha a feladatunk azt kívánja, hogy egy XML fájlt elejétől a végéig elolvasunk, és közben bizonyos műveleteket hajtsunk végre, mint például egy adatszerkezet felépítése, vagy valamilyen információ összegyűjtése (összeg-, átlagszámítás, stb) akkor a SAX a legmegfelelőbb eszköz. Kisebb változtatások (attribútumok, vagy egy elem tartalmának megváltoztatása) könnyen elvégezhetők vele, de nem célszerű viszont olyankor használni, mikor a dokumentum szerkezetében kell módosításokat végezni, mondjuk elemek egymásba ágyazását megváltoztatni. Például egy könyvet leíró XML dokumentumban a fejezetek sorrendjének megváltoztatásához a DOM a megfelelő eszköz, de ha csak bizonyos típusú elemek tartalmát akarjuk kigyűjteni, akkor a SAX is tökéletes.

A SAX egyik előnye a gyorsaság és egyszerűség. Tegyük fel, hogy van egy adatbázisunk, egy hozzá tartozó bonyolult DTD-vel, képregények nyilvántartására, és ki akarjuk keresni Neil Gaiman összes művét. Ehhez a konkrét feladathoz szükségtelen értelmezni az összes képregény összes adatát, tehát elég egy olyan osztály-példányt készíteni, amit csak a writer elemekkel foglalkozik, az összes többit figyelmen kívül hagyja.

A másik előny, hogy nem kell az egész dokumentumot egyszerre a memóriában tartani. Mivel az XML meglehetősen "terjengős" nyelv, általában az 1MB-nál nagyobb állományok kezelésére a nagyon nagy memóriaigény miatt a DOM és más olyan módszerek használata, melyek az egész dokumentum-szerkezetet a memóriában tárolják nem javasolt.

A SAX használatához négy interfészre van szükség. Mivel ilyen nyelvi elem a Pythonban nincsen, ezek osztályokként vannak megvalósítva. Metódusaik törzse egyetlen pass utasítás így csak azokat a metódusokat kell megírni a leszármazottaikban, amelyek ténylegesen csinálnak valamit.

ContentHandler Ez az osztály a SAX lelke, ennek metódusai hívódnak meg a feldolgozás elején, végén illetve az egyes elemek feldolgozásának elején és végén.
DTDHandler A DTD kezelésével kapcsolatos metódusok összefogója
EntityResolver A külső hivatkozások feloldására szolgál. Ha nincsenek a dokumentumban ilyenek, akkor használható az alapértelmezett üres EntityResolver.
ErrorHandler Nem meglepő módon a parser ennek az osztálynak a metódusait használja hibajelzésre és kezelésre.

Egy XML fájl feldolgozása SAX-szal valahogy így néz ki:
# Először létrehozunk egy saját ContentHandlert... from xml.sax import ContentHandler, ... class docHandler(ContentHandler): # ... itt felüldefiniáljuk a szükséges műveleteit ... # Példányosítjuk dh = docHandler() # Létrehozzuk a parsert parser = ... # Hozzárendeljük a ContentHandlert parser.setContentHandler(dh) # Végül egyszerűen meghívjuk a parse műveletét. Addig folytatja a # feldolgozást, míg a standard inputról egy EOF jelet nem kap. parser.parse(sys.stdin)

Térjünk vissza az előző példához! Itt van a képregény-gyűjtemény egy része:

<collection> <comic title="Sandman" number='62'> <writer>Neil Gaiman</writer> <penciller pages='1-9,18-24'>Glyn Dillon</penciller> <penciller pages="10-17">Charles Vess</penciller> </comic> </collection>

Egy XML dokumentumnak pontosan egy gyökéreleme kell, hogy legyen, ez most a collection elem. Minden kiadvány számára egy comic elemet tartalmaz, melynek attribútumai tárolják a kiadvány címét, és számát. A comic elemek viszont több más elemet tartalmazhatnak, mint például writer, penciler, felsorolva az adott szám készítőit.

Először készítsünk egy egyszerű osztályt, ami megmondja, hogy egy adott szám szerepel-e a gyűjteményünkben.

from xml.sax import saxutils class FindIssue(saxutils.DefaultHandler): def __init__(self, title, number): self.search_title, self.search_number = title, number

A DefaultHandler leszármazottja mind a négy osztálynak: ContentHandler, DTDHandler, EntityResolver, ErrorHandler. Ezt a megoldást célszerű választani akkor, ha csak egy egyszerű működésű osztályra van szükségünk. A másik lehetőség, hogy mind a négy osztályból külön származtatunk egyet-egyet. Egyik megoldás sem szükségszerűen jobb a másiknál, a választás leginkább csak ízlés kérdése.

A keresés végrehajtásához az osztálynak tudnia kell, hogy mit is keres. Ezt a konstruktorának adjuk meg paraméterként. Ez az egyszerű keresés csak az attribútumok értékeit vizsgálja, ezért csak a startElement metódust kell felüldefiniálnunk.

def startElement(self, name, attrs): # Nem képregény, kihagyjuk if name != 'comic': return title = attrs.get('title', None) number = attrs.get('number', None) if (title == self.search_title and number == self.search_number): print title, '#' + str(number), 'found'

A startElement() metódus paraméterként a feldolgozás alatt álló elem nevét és egy asszociatív tömbben az attribútumait kapja. Ez a metódus a dokumentum minden XML elemére meghívódik. Ha a függvény elejére beszúrjuk az alábbi sort:

print 'Starting element:', name
ezt a kimenetet kapjuk:
Starting element: collection Starting element: comic Starting element: writer Starting element: penciller Starting element: penciller

Ahhoz, hogy ezt ténylegesen használni tudjuk szükség van még egy legfelső szintű kódrészletre, amit létrehozza és összekapcsolja a parser és a FindIssue objektumokat, és a parser egy metódusának meghívásával elindítja az elemzést.

from xml.sax import make_parser from xml.sax.handler import feature_namespaces if __name__ == '__main__': # A parser létrehozása parser = make_parser() # handler létrehozása dh = FindIssue('Sandman', '62') # összekapcsolás parser.setContentHandler(dh) # feldolgozás parser.parse(file)

SAXot használva a feladatunk alapvetően a handler létrehozása, a parser már adott. Ha szükségesnek látjuk, kiválaszthatjuk mi magunk is a használt parsert, de ha nem akarunk ezzel foglalkozni, akkor a make_parser osztály megteszi ezt helyettünk.

Jelenleg is számos XML parser létezik a nyelvhez, és még továbbiak megjelenése várható. Az xmllib.py a Python standard könyvtárának része, így mindig használható, de nem túl gyors. Ennek egy gyorsabb változatra megtalálható az xml.parsers modulban.

Az xml.parsers.expat még gyorsabb, így ha az adott rendszeren hozzáférhető, jelenleg ez a legjobb választás. Ez a modul C-ben íródott, emiatt lett gyorsabb, viszont nem minden Python disztribúciónak része (pl. Windowson hozzátartozik, UNIXokon nem).
A make_parser osztály kiválasztja nekünk az elérhető parserek közül a leggyorsabbat, így ezzel nem kell foglalkoznunk, de megadható neki egy lista is, ebben az esetben a listán szereplők közül választja a leggyorsabbat.

Ha sikeresen létrehoztuk a parsert a setContentHandler(), setDTDHandler(), setEntityResolver(), és setErrorHandler() metódusokkal rendelhetjük hozzá a megfelelő osztályokat.

A fenti program kimenete ez lesz:

Sandman #62 found

Hibakezelés

Tekintsük a következő XML dokumentumot:

<collection> &foo; <comic title="Sandman" number='62'> </collection>

Ebben a &foo; entitás nem létezik, és a comic elem nincs lezárva (ha ez egy üres elem lenne, a "/>" karakterekkel kellene lezárni ">" helyett). Emiatt a feldolgozáskor egy SAXParseException kivételt kapunk.

xml.sax._exceptions< .SAXParseException: undefined entity at None:2:2

Az ErrorHandler alapértelmezésben kivételt dob minden hibánál. Ha nem ez az elvárt viselkedés, felül kell definiálnunk a hibakezelő osztály error() és fatelError() metódusait.
Az ErrorHandler warning(), error(), fatalError() függvényei mind egy paramétert kapnak, a hibát leíró kivétel-példányt, ami a SAXException leszármazottja. Ennek az str() függvényétől kaphatjuk meg a hiba szöveges leírását. Például, ha nem végzetes kivételek esetén csak figyelmeztetést akarunk adni, és folytatni az elemzést, a következő kódrészlethez hasonlóra van szükségünk:

def error(self, exception): import sys sys.stderr.write("\%s\n" \% exception)

Ezután a programunk csak végzetes hibák esetén fog leállni.

DOM

Ha SAX-ot használunk, egy XML dokumentum feldolgozását egy osztályt metódusainak egymás után történő meghívásával végezzük. A Document Object Model (DOM) egy másik megközelítést ad. Ekkor a teljes dokumentumot beolvassuk a memóriába egy fa-szerkezetbe, és azon hajtunk végre módosításokat.

A legfelső szintű Document példány reprezentálja a fa gyökerét. Ennek egyetlen gyermeke van, a legfelső szintű Element példány. Ennek gyermekei reprezentálják a neki megfelelő XML elembe beágyazott további elemeket.

Minden dologhoz, ami XML dokumentumokban előfordulhat rendelkezésünkre állnak az azokat reprezentáló osztályok: Text, Comment, CDATASection, EntityReference stb.
A csúcsokat alkotó osztályok attribútumai, és metódusai teszik lehetővé, hogy lekérdezzük és megváltoztassuk az adott elem attribútumait, gyermekeket töröljünk belőle vagy éppen újat adjunk hozzá, illetve visszaalakítsuk XML formátumba.

Bár a DOM specifikációja nem követeli meg, hogy az egész dokumentumot leíró fát folyamatosan a memóriában kell tartani, a jelenlegi Python implementáció így működik, vagyis nagyon nagyméretű dokumentumok módosítására egyelőre nem alkalmas.
A DOM rengeteg szolgáltatást nyújt, ezekből itt csak a legalapvetőbbeket tudom bemutatni.

Valójában a Python DOM implementációi nem is valósítják meg az összes szolgáltatást. A DOM-ot a W3C három szintre osztva definiálta, ebből a Python jelenleg az első kettőt valósítja meg, a harmadik szint implementációja most készül. Hogy tovább bonyolódjon a helyzet, a Python két DOM implementációval rendelkezik:


Dokumentum-fa létrehozása

Az xml.dom.ext.reader csomag számos osztályt tartalmaz, amik különböző forrásokból építik fel a dokumentum-fát. Az xml.dom.Sax2 modul tartalmazza a Reader osztályt, amint fromStream metódusa input streamből olvassa az XML kódot. Az input lehet egy fájl-szerű objektum (bármi, aminek van read() metódusa), vagy egy string. Utóbbi esetben a stringet URL-ként értelmezi, és az urllib modult használja a megnyitásához.

import sys from xml.dom.ext.reader import Sax2 # Létrehozzuk a Reader objektumot reader = Sax2.Reader() # feldolgozzuk a fájlt doc = reader.fromStream(sys.stdin)

A fromStream() az XML objektum fájának gyökerét adja vissza.

A fa kiíratása

Az alábbiakban a következő egyszerű XML dokumentumot fogjuk használni

<?xml version="1.0" encoding="iso-8859-1"?> <xbel> <?processing instruction?> <desc>No description</desc> <folder> <title>XML bookmarks</title> <bookmark href="http://www.python.org/sigs/xml-sig/" > <title>SIG for XML Processing in Python</title> </bookmark> </folder> </xbel>

Ezt DOM fává alakítva a következő szerkezetet kaphatjuk

Element xbel None Text #text ' \012 ' ProcessingInstruction processing 'instruction' Text #text '\012 ' Element desc None Text #text 'No description' Text #text '\012 ' Element folder None Text #text '\012 ' Element title None Text #text 'XML bookmarks' Text #text '\012 ' Element bookmark None Text #text '\012 ' Element title None Text #text 'SIG for XML Processing in Python' Text #text '\012 ' Text #text '\012 ' Text #text '\012'

Nem feltétlenül pontosan ezt fogjuk kapni, mivel a különböző parserek esetleg eltérően kezelik a szöveg típusú elemeket, így a fenti fa Text elemeit elképzelhető, hogy több Text elem sorozataként kapjuk meg.

A DOM fa a Print(doc, stream), vagy a PrettyPrint(doc, stream) metódusokkal alakítható vissza XML-be. Ha a stream-et nem adjuk meg akkor a függvények a standard outputra írnak. A Print() változtatás nélkül, egyszerűen visszaalakítja a fát XML-be, a PrettyPrint() viszont kitörölhet és beszúrhat whitespace karaktereket, hogy szépen formázott XML kódot adjon.

A fa megváltoztatása

Először tekintsük a Node osztály fontosabb attribútumait és metódusait. Az összeses többi XML elemet reprezentáló osztály (Document, Element,Text stb) ennek a leszármazottja. Nagyon sok feladat megoldható pusztán a Node által biztosított interfész használatával.

AttribútumJelentés
nodeType Egész konstans. Megadja a fa adott csúcsának típusát, pl. ELEMENT_NODE, TEXT_NODE stb.
nodeName A csúcs neve. Bizonyos típusú elemeknél, pl. Element, a név az elem neve, másoknál, pl. Text-nél egy konstans string pl. #text
nodeValue A csúcs értéke. Bizonyos típusú elemeknél, pl. Text, az érték egy string az elem szövegszerű tartalmával, másoknálegyszerűen None
parentNode A csúcs szülője. A gyökérnél None
childNodes A gyermek csúcsok listája (lehet üres is)
firstChild A csúcs első gyereke. None ha nincs gyereke
lastChild A csúcs utolsó gyereke. None ha nincs gyereke
previousSibling A csúcs előző testvére. None ha a csúcsnak nincs szülője, vagy előző testvére
nextSibling A csúcs következő testvére. None ha a csúcsnak nincs szülője, vagy következő testvére
ownerDocument Arra a dokumentumra mutató referencia, ami az adott csúcsot tartalmazza
attributes A NamedNodeMap osztály egy példánya. Lényegében egy asszociatív tömb attribútum név-érték párokkal

MetódusHatás
appenChile(newChild) Hozzáfűzi a newChild objektumot a gyermekek listájának végéhez.
removeChild(oldChild) Kiveszi a fából az oldChild csúcsot, de nem törli. Az oldChild szülője a továbbiakban None lesz.
replaceChild(newChild, oldChild) Kicseréli az oldChild csúcsot newChild-ra. Mindkettőnek az adott csúcs gyermekének kell lennie!
insertBefore(newChild, refChild) Beszúrja a newChild csúcsot refChild elé. A refChild-nak az adott csúcs gyermekének kell lennie!
hasChildNodes() Igaz, ha a csúcsnak vannak gyermekei
cloneNode(deep) A csúcs másolatával tér vissza. Ha deep hamis a másolatnak nem lesznek gyermekei. Ha igaz, a művelet a gyermekeket is lemásolja.

Az Element és Document csúcsoknak van még egy nagyon hasznos metódusa, a getElementsByTagName(tagName). Ez egy listát ad vissza, amiben megtalálható a csúcs összes adott taggel kezdődő eleme. Például a dokumentum fejezetei megkaphatók a document.getElementsByTagName('chapter') függvényhívással.

Új csúcsok létrehozása

Az egész dokumentum-fa gyökere a Document csúcs. Ennek a documentElement attribútuma az az Element csúcs, ami a tényleges dokumentumszerkezet gyökérelemét tartalmazza. A Document csúcsnak lehetnek más gyermekei is, pl. a ProcessingInstruction csúcsok, de legfeljebb egy Elementet tartalmazhat. Ha egy új DOM-fát építünk fel, a Document csúcs create*() metódusait használhatjuk, például a createElement() és createTextNode() metódusokat.

new = document.createElement('chapter') new.setAttribute('number', '5') document.documentElement.appendChild(new)

A fa bejárása

Ha már megvan a fánk, a másik legalapvetőbb művelet annak bejárása.
A Document csúcs

Tarzan = createTreeWalker(root, whatToShow, filter, entityRefExpansion)

metódusát használva létrehozhatunk egy TreeWalker objektumot. Ennek segítségével járhatjuk be a root elemben gyökerező részfát. Az éppen aktuális elem a TreeWalker currentNode attribútumában található, a továbblépés pedig a beszédes nevű parentNode(), firstChild(), lastChild(), and nextSibling(), previousSibling() metódusokkal lehetséges.

A whattoshow egy bitmaszk, amiben minden elemtípusra megadhatjuk, hogy bejárás közben az adott típusú elemeket látni szeretnénk-e. A bitmaszk értékeit a NodeFilter osztály konstansai segítségével állíthatjuk be: a 0 elrejt minden csúcsot, a NodeFilter.SHOW_ALL mindent megmutat, a SHOW_ELEMENT, SHOW_TEXT és társaik értelemszerűen csak az adott típusú elemeket.
Ezt tovább lehet finomítani: a filter egy függvény, ami minden bejárt elemre meghívódik, és NodeFilter.FILTER_ACCEPT, NodeFilter.FILTER_SKIP vagy NodeFilter.FILTER_REJECT értékkel térhet vissza. REJECT esetén a csúcsot és a gyermekeit is kihagyjuk a bejárásból, SKIP esetén csak a csúcsot hagyjuk ki, de a gyermekeit bejárjuk, ACCEPT esetén a csúcsot és gyermekeit is bejárjuk. Ha filter helyett None-t adunk meg mindent bejárunk. A következő példa a teljes dokumentumot bejárja, és minden tag nevét kiírja.

from xml.dom.NodeFilter import NodeFilter walker = doc.createTreeWalker(doc.documentElement, NodeFilter.SHOW_ELEMENT, None, 0) while 1: print walker.currentNode.tagName next = walker.nextNode() if next is None: break

Említésre érdemes még a pixie modul, mely egy újabb érdekes megközelítése az XML feldolgozásnak. A DOM-hoz hasonlóan egy fa-reprezentációját adja az XML dokumentumnak, de ezen kívül van még egy lényeges funkciója: az XML dokumentumokat át tudja alakítani egy sor-orientált formátumba (és persze vissza is). A PYX formátum értelme, hogy az XML-nél sokkal egyszerűbben kezelhető a Python stringeket és reguláris kifejezéseket kezelő utasításaival, illetve olyan sor-alapú eszközökkel, mint például a UNIX szűrők.