Normalerweise nutze ich das DOM-basierte Einlesen von XML-Dateien. Mit oga unter Ruby zum Beispiel so:
require 'oga' doc = Oga.parse_xml('<people> <author> <name>onli</name> <real>yes</real> </author> <author> <name>Unsichtbares Einhorn</name> <real>no</real> </author> </people>')
doc
ist dann eine Baumstruktur, die z.B. mit xpath durchgegangen werden kann:
p doc.xpath("//name").map{|x| x.text } # => ["onli", "Unsichtbares Einhorn"]
So vorzugehen hat aber einen Nachteil: Den Speicherbedarf. Ich arbeitete vorhin mit einer XML-Datei, die als .gz heruntergeladen schlanke 13 MB wog. Entpackt waren es dann schon 164 MB. Der Speicherverbrauch beim direkten Einlesen nach obiger Methode? 4 GB. Viel zu viel für den kleinen Mini-PC, der später regelmäßig diese XML-Datei bearbeiten und bestimmte Einträge finden soll. Denn der hat nur 500 MB Ram.
Ich bin dann schließlich bei SAX gelandet. Grundsätzlich ist das nicht schneller, deswegen hatte ich diesen Ansatz nach einem Erstkontakt vor vielen Jahren nicht mehr in Betracht gezogen. Aber hier passt es: Anstatt die Datei auf einmal zu lesen und eine komplexe Struktur in den Speicher zu packen wird die XML-Datei Zeile für Zeile durchgegangen. Und bei jedem Schritt wird ein Handler benachrichtigt, welches Element das gerade ist. Der Handler kann dann alles ignorieren was für ihn nicht wichtig ist und seine Aufgabe erledigen. Das könnte z.B. sein, spezielle Elemente zu zählen, ein Standardbeispiel für SAX. Oder man kann es auch nutzen, um nur die Daten aus der XML-Datei herauszuholen die interessant sind. Entsprechend gering kann der Speicherverbrauch bleiben.
Wollte ich in der Beispieldatei von oben nur Autorennamen speichern, die real sind, dann könnte der SAX-Parser das so machen:
require 'oga' class RealAuthorNames attr_reader :names attr_reader :currentElement attr_reader :currentText attr_reader :name def initialize @names = end def on_element(namespace, name, attrs = {}) @currentElement = name end def on_text(text) unless text.strip.empty? @currentText = text @name = @currentText if currentElement == 'name' @names.push(@name) if @currentElement == 'real' && @currentText == 'yes' end end end handler = RealAuthorNames.new Oga.sax_parse_xml(handler, '<people> <author> <name>onli</name> <real>yes</real> </author> <author> <name>Unsichtbares Einhorn</name> <real>no</real> </author> </people>') p handler.names # => ['onli']
Die Liste der möglichen SAX-Ereignisse habe ich aus dem obersten Kommentar von ogas sax_parser.rb entnommen. Nokogiri hätte sie im Tutorial gelistet.
Der Code zeigt, warum ich SAX normalerweise vermeide: Schön ist das nicht. Selbst dieses simple Beispiel hat mehrere Zustandsvariablen und verlässt sich darauf, dass die XML-Datei regelmäßig aufgebaut ist. Aber es funktioniert eben: Die größere XML-Datei zu verarbeiten verbraucht jetzt nicht mehr 4 GB Speicher, wenn die für mich relevanten Daten rausgeholt werden, sondern der Arbeitsspeicherverbrauch bleibt unter 80 MB.
onli blogging am : Effizienter CSV-Dateien verarbeiten, mit Ruby und generell
Vorschau anzeigen
onli blogging am : Oga als Alternative zu Nokogiri
Vorschau anzeigen