XML SAX Verständisproblem

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
diablo75
User
Beiträge: 30
Registriert: Dienstag 8. September 2009, 23:12

Da ich nicht XML Dateien in den Arbeitsspeicher laden will schreib ich mein Programm um und will dabei SAX verwenden.

Vorerst mal der Code:

Code: Alles auswählen

from xml.sax import make_parser, handler
import sqlite3
import time

class OmdsHeader(handler.ContentHandler):

    def __init__(self) -> None:
        self.header_list = []
        self.vu_list = []    

    def startElement(self, name, attrs):
        if name == 'PAKET':
            self.header_list.append(attrs['MaklerID'])
            self.header_list.append(attrs['PaketZpktErstell'])
            self.header_list.append(attrs['PaketZpktLetztErstell'])
            self.header_list.append(attrs['PaketInhCd'])
            self.header_list.append(attrs['PaketUmfCd'])
            self.header_list.append(attrs['OMDSVersion'])
            self.header_list.append(attrs['VUVersion'])
            self.header_list.append(attrs['DVRNrAbs'])
            self.header_list.append(attrs['PaketKommentar'])

            self.vu_list.append(self.header_list)

    def characters(self, content):
        pass

        
    def endElement(self, name):
        pass

    def getVuNr(self):              
        return self.vu_list
        
parser = make_parser()
b = OmdsHeader()
parser.setContentHandler(b)
parser.parse("muki.xml")
print(b.getVuNr())
Hier die XML Datei:

Code: Alles auswählen

<?xml version="1.0" encoding="utf-8"?>
<OMDS xmlns="urn:omds20" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:omds20 .\omds211-00.xsd">
  <PAKET VUNr="063" MaklerID="7016281" PaketZpktErstell="2021-08-23T04:56:11.515" PaketZpktLetztErstell="2021-08-23T04:56:11.515" PaketInhCd="VV" PaketUmfCd="G" OMDSVersion="211-00" VUVersion="211-00" DVRNrAbs="2110857" PaketKommentar="muki Bestandsdaten erstellt am 23.08.2021">
</PAKET>
<PAKET VUNr="063" MaklerID="7016282" PaketZpktErstell="2021-08-23T04:56:11.515" PaketZpktLetztErstell="2021-08-23T04:56:11.515" PaketInhCd="VV" PaketUmfCd="G" OMDSVersion="211-00" VUVersion="211-00" DVRNrAbs="2110857" PaketKommentar="muki Bestandsdaten erstellt am 23.08.2021">
  </PAKET>
</OMDS>
Was ich nicht verstehe ist, dass die Ausgabe zu oft erscheint, da es ja nur 2 Pakete gibt. Hier die Ausgabe:
[['7016281', '2021-08-23T04:56:11.515', '2021-08-23T04:56:11.515', 'VV', 'G', '211-00', '211-00', '2110857', 'muki Bestandsdaten erstellt am 23.08.2021', '7016282', '2021-08-23T04:56:11.515', '2021-08-23T04:56:11.515', 'VV', 'G', '211-00', '211-00', '2110857', 'muki Bestandsdaten erstellt am 23.08.2021'],
['7016281', '2021-08-23T04:56:11.515', '2021-08-23T04:56:11.515', 'VV', 'G', '211-00', '211-00', '2110857', 'muki Bestandsdaten erstellt am 23.08.2021', '7016282', '2021-08-23T04:56:11.515', '2021-08-23T04:56:11.515', 'VV', 'G', '211-00', '211-00', '2110857', 'muki Bestandsdaten erstellt am 23.08.2021']]

Sollte aber so ausschauen:
[['7016281', '2021-08-23T04:56:11.515', '2021-08-23T04:56:11.515', 'VV', 'G', '211-00', '211-00', '2110857', 'muki Bestandsdaten erstellt am 23.08.2021'],
['7016282', '2021-08-23T04:56:11.515', '2021-08-23T04:56:11.515', 'VV', 'G', '211-00', '211-00', '2110857', 'muki Bestandsdaten erstellt am 23.08.2021']]
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@diablo75: Du erweiterst immer `header_list` und steckst dann die immer gleiche und immer länger werdende `header_list` in `vu_list`. Mal ohne den ganzen sums drum herum:

Code: Alles auswählen

In [17]: xs = []                                                                

In [18]: ys = []                                                                

In [19]: xs.append(1); xs.append(2)                                             

In [20]: ys.append(xs)                                                          

In [21]: xs                                                                     
Out[21]: [1, 2]

In [22]: ys                                                                     
Out[22]: [[1, 2]]

In [23]: xs.append(4); xs.append(5)                                             

In [24]: xs                                                                     
Out[24]: [1, 2, 4, 5]

In [25]: ys.append(xs)                                                          

In [26]: ys                                                                     
Out[26]: [[1, 2, 4, 5], [1, 2, 4, 5]]

In [27]: xs.append(42)                                                          

In [28]: ys                                                                     
Out[28]: [[1, 2, 4, 5, 42], [1, 2, 4, 5, 42]]
`header_list` sollte wohl eher nicht dauerhaft Zustand des Objekts sein der ständig erweitert wird, sondern jedes mal eine neue, lokale Liste in der Methode sein, damit immer neue, frische Listen mit *einem* Datensatz hinzugefügt werden. Wobei eine Liste mit 9 Werten mit unterschiedlichen Bedeutungen ein „code smell“ sind. Wenn das so direkt beispielsweise für eine CSV-Datei verwendet wird okay, aber wenn das Programm darauf noch irgendwie operieren soll und dann mit magischen Indexzahlen anfängt, sollte man das besser in ein Objekt verpacken. Klasse oder `collections.namedtuple` beispielsweise. Oder falls das in einer SQL-Datenbank landen soll, bietet sich vielleicht auch gleich SQLAlchemy und eine ORM-Klasse an.

Du rufst die `__init__()` der Basisklasse nicht auf. Das ist ein Fehler, denn `Contenthandler` hat eine `__init__()`-Methode und die macht auch etwas!

`getVuNr()` ist falsch. Der Name ist falsch geschrieben (`get_vu_nummer()`), inhaltlich falsch, weil da deutlich mehr als eine Nummer zurückgegeben wird, und es ist ein trivialer Getter den man in Python nicht schreiben würde. Man kann doch einfach auf das Attribut zugreifen.

Grunddatentypen haben in Namen nichts verloren. Den Typ ändert man während der Entwicklung öfter mal, und dann hat man einen falschen, irreführenden Namen im Programm, oder muss die betroffenen Namen überall anpassen.

Der Sinn `Contenthandler` als Basisklasse zu verwenden ist doch gerade, dass man dann nicht alle Methoden implementieren muss. Wenn da also nichts weiter als ``pass`` drin steht, macht es keinen Sinn die Methode zu implementieren.

Warum heisst die Klasse `OmdsHeader`? Wofür steht `b`? Einbuchstabige Namen sind nur sehr selten sinnvolle Namen.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
from operator import itemgetter
from pprint import pprint
from xml.sax import make_parser
from xml.sax.handler import ContentHandler

get_paket_values = itemgetter(
    "MaklerID",
    "PaketZpktErstell",
    "PaketZpktLetztErstell",
    "PaketInhCd",
    "PaketUmfCd",
    "OMDSVersion",
    "VUVersion",
    "DVRNrAbs",
    "PaketKommentar",
)


class OmdsHandler(ContentHandler):
    def __init__(self):
        ContentHandler.__init__(self)
        self.pakete = []

    def startElement(self, name, attrs):
        if name == "PAKET":
            self.pakete.append(get_paket_values(attrs))


def main():
    parser = make_parser()
    omds_handler = OmdsHandler()
    parser.setContentHandler(omds_handler)
    parser.parse("muki.xml")
    pprint(omds_handler.pakete)


if __name__ == "__main__":
    main()
Ausgabe:

Code: Alles auswählen

[('7016281',
  '2021-08-23T04:56:11.515',
  '2021-08-23T04:56:11.515',
  'VV',
  'G',
  '211-00',
  '211-00',
  '2110857',
  'muki Bestandsdaten erstellt am 23.08.2021'),
 ('7016282',
  '2021-08-23T04:56:11.515',
  '2021-08-23T04:56:11.515',
  'VV',
  'G',
  '211-00',
  '211-00',
  '2110857',
  'muki Bestandsdaten erstellt am 23.08.2021')]
SAX ist aber doof. Nicht nur das die API so eklig javaesque ist, hier werden ja doch wieder alle Daten im Speicher gesammelt. Die ElementTree-API bietet einen Iterator über die Parser-Ereignisse, so dass man eine echte „pull API“ hat, statt einen Handler in den die Ereignisse rein ge-pusht werden.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Mit der ElementTree-API und `iterparse()`:

Code: Alles auswählen

#!/usr/bin/env python3
from operator import itemgetter
from pprint import pprint
from xml.etree.ElementTree import iterparse

get_paket_values = itemgetter(
    "MaklerID",
    "PaketZpktErstell",
    "PaketZpktLetztErstell",
    "PaketInhCd",
    "PaketUmfCd",
    "OMDSVersion",
    "VUVersion",
    "DVRNrAbs",
    "PaketKommentar",
)


def iter_pakete(xml_source):
    for event_type, element in iterparse(xml_source, events=["start", "end"]):
        if event_type == "start":
            if element.tag == "{urn:omds20}PAKET":
                yield get_paket_values(element.attrib)
        elif event_type == "end":
            element.attrib.clear()
            element.clear()
        else:
            assert False, f"unexpected event type {event_type!r}"


def main():
    pprint(list(iter_pakete("muki.xml")))


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
diablo75
User
Beiträge: 30
Registriert: Dienstag 8. September 2009, 23:12

Danke für die ausführliche Erklärung.
Jetzt bin ich wieder unschlüssig ob ich SAX oder ElementTree-API verwenden soll.

Mit der ElementTree-API hatte ich auch begonnen zu programmieren und kann auch schon ca. 40% auslesen. Dann hatte ich in einer Facebook Gruppe gelesen, das es besser ist SAX zu verwenden, da diese nicht das komplett XML in den Arbeitsspeicher haut.

Da mir nicht genau Bekannt ist, wie groß die XML Dateien bei anderen Usern ist hatte ich Bedenken bezüglich Performance. Die XML Datei enthält Kunden, Vertrags, Schadens, Provisionsdaten und auch Versicherungsklauseln. Bei uns im Betrieb sind sie XML Dateien max. 10 MB groß - wir sind aber ein kleiner Versicherungsmakler. Nun kann es natürlich sein, dass es bei anderen >500 MB sind.

Was würdet Ihr empfehlen?? Und warum?
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wieviel Hautpspeicher hat denn dein Rechner? Unter 8GB gibt's doch heute nur noch im Ueberraschungsei. Mit einem 64 Bit Python kannst du das auch grundsaetzlich nutzen.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@diablo75: `iterparse()` lädt ja auch nicht die komplette XML-Datei in den Speicher. Es hätte halt den Vorteil, dass man auch die kompletten Datensätze nicht im Speicher halten muss, denn bei SAX sammelst Du ja erst die kompletten Werte in einer Liste, bevor Du sie ausgeben kannst. Wenn Du die Datensatz für Datensatz ausgeben/verarbeiten willst, musst Du das im Handler machen, also entweder direkt in der Klasse, oder wenn Du es davon entkoppeln willst, beispielsweise über eine Rückruffunktion, die Du dann mit übergibst. So eine Push-API ist in Sprachen wie JavaScript üblich, aber in Python sind Pull-APIs die Regel, und dementsprechend gibt es einen Haufen fertige Werkzeuge und Syntax dafür. Bei so Grundlagen wie ``for``-Schleifen angefangen, über eingebaute Funktionen wie `map()` und `filter()` bis zu `itertools` in der Standardbibliothek.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

diablo75 hat geschrieben: Samstag 27. November 2021, 16:43 Was würdet Ihr empfehlen?? Und warum?
Es kommt ein bisschen drauf an: Grundsätzlich liegen die Menschen aus der Facebook-Gruppe nicht ganz falsch. Beim Parsen wird die XML-Datei in eine Objekthierarchie überführt und die kann zum Beispiel bei einer 1GB Datei dazu führen, dass der Prozess einen Speicherbedarf im (niedrigen) zweistelligen GB Bereich hat. Mit deinen 10 MB bist du da aber noch weit von entfernt und selbst die genannten 500 MB sollten auf heute üblichen Arbeitsplatzrechnern handlebar sein. Man sollte auch bedenken, dass man beim Stream-Parsing (Sax, etree.iterparse, etc.) ein anderes Programmiermodell hat, das deutlich mehr händisches Management erfordert. Bei etree.iterparse muss man zum Beispiel auch dafür sorgen, dass nicht verwendete Knoten entfernt werden, da der Baum sonst genau so anwächst wie beim regulären parsen. Den geringeren Speicherbedarf erkauft man sich dann z.T. mit einer etwas längeren Verarbeitungszeit.

Wenn du kleinere Dateien hast, bietet sich meiner Meinung nach an, diese am Stück einzulesen, da man damit am flexibelsten und komfortabelsten arbeiten kann. Wenn du große Dateien hast, die regelmäßig verarbeitet werden müssen und/oder weiter wachsen, bietet sich meiner Meinung nach das schon angesprochene lxml.etree.iterparse an. Das ist, was ich selbst verwende, wenn ich XML-Dateien im GB-Bereichen auf regulärer Hardware verarbeiten muss.
tmoz
User
Beiträge: 1
Registriert: Montag 14. November 2022, 14:46

Hallo,
sehr interessiert bin ich über diese CodeSchnipsel gestolpert.... auch ich bin bei einem Versicherungsmakler in Österreich tätig und suche Möglichkeiten, den OMDS als xml mit Python einzulesen/verarbeiten/importieren. Ich wäre sehr an einem Gedanken- und Code-Austausch/Diskussion interessiert!
danke schon vorab,
Thomas
Antworten