XML-Datei auslesen und in csv-Format umwandeln

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Hallo zusammen,

stehe vor der Aufgabe eine XML-Datei (Preisliste) auszulesen und dann in ein CSV-Format umzuwandeln.
Das Format der XML-Datei ist <BMECAT version="1.2">.

Folgender Code, habe ich bis jetzt:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# For Python3.x

from lxml import etree

filepath = '/pfad/zu/preisliste/preisliste.xml'

ET = etree.ElementTree()
tree = ET.parse(filepath)
root = tree.getroot()
names = [entry.text for entry in root.findall("entry/BMECAT/HEADER")]
Ich erhalte folgende Fehlermeldung:
  • <Element {http://www.bmecat.org/bmecat/1.2/bmecat ... log}HEADER at 0x7f7041c7dfc8>
    <Element {http://www.bmecat.org/bmecat/1.2/bmecat ... log}HEADER at 0x7f7041179dc8>
    Traceback (most recent call last):
    File "xml.py", line 11, in <module>
    root = tree.getroot()
    AttributeError: 'lxml.etree._Element' object has no attribute 'getroot'
    Error in sys.excepthook:
    Traceback (most recent call last):
    File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 63, in apport_excepthook
    from apport.fileutils import likely_packaged, get_recent_crashes
    File "/usr/lib/python3/dist-packages/apport/__init__.py", line 5, in <module>
    from apport.report import Report
    File "/usr/lib/python3/dist-packages/apport/report.py", line 15, in <module>
    import xml.dom, xml.dom.minidom
    File "/media/daten/Scripte/officeplanet/firmware_officeplanet/xml.py", line 11, in <module>
    root = tree.getroot()
    AttributeError: 'lxml.etree._Element' object has no attribute 'getroot'

    Original exception was:
    Traceback (most recent call last):
    File "xml.py", line 11, in <module>
    root = tree.getroot()
    AttributeError: 'lxml.etree._Element' object has no attribute 'getroot'
Hoffe, dass Ihr mir da weiterhelfen könnt, was falsch ist und wie es richtig sein müsste.

Grüße Nobuddy
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Nobuddy: woher kommen denn die ersten beiden Zeilen Deiner "Fehlermeldung"? Du hast in Deinem Programm nämlich keine print-Anweisungen.
Der Rückgabewert der parse-Methode liefert direkt das Root Objekt. Das BMECAT-Format verwendet Namespaces und hat keine entry-Tags und BMECAT ist schon der Tagname von root, so dass Dein findall so nicht funktioniert. Das Header-Element hat dann keinen Text-Inhalt.

Also mal ein sinnvolleres Beispiel:

Code: Alles auswählen

import lxml.etree as et
 
BMENS = "{http://www.bmecat.org/bmecat/1.2/bmecat_new_catalog}"
PATH = "{0}T_NEW_CATALOG/{0}PRODUCT/{0}PRODUCT_DETAILS/{0}DESCRIPTION_SHORT".format(BMENS)

filepath = '/pfad/zu/preisliste/preisliste.xml'
 
tree = et.parse(filepath)
root = tree.getroot()
products = [entry.text for entry in root.findall(path)]
BlackJack

Die Dokumentation von ElementTree.parse() verrät was der Rückgabewert ist. Ist allerdings auch ein bisschen ungewöhnlich die Methode aufzurufen statt der Funktion im Modul.
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Hallo und Danke für Eure Hilfe!

@Sirius3, habe Deinen Beispielcode mal übernommen.
Bei Zeile 10, gibt es path nicht. Dies sollte sich wohl auf PATH beziehen?
Habe mal path durch PATH ersetzt. Fehler kommen keine mehr, das ist ja schon mal gut.
Nur products gibt [] aus, also keine Daten.
Habe dann mal die Werte (T_NEW_CATALOG, PRODUCT, PRODUCT_DETAILS, DESCRIPTION_SHORT) in der XML-Datei überprüft,
PRODUCT und PRODUCT_DETAILS, gibt es dort nicht sondern ARTICLE und ARTICLE_DETAILS.
Ist wohl nicht die neuste BMECAT-Version, denn laut Wikipedia wurde ja ARTICLE durch PRODUCT ersetzt.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# For Python3.x

import lxml.etree as et
     
BMENS = "{http://www.bmecat.org/bmecat/1.2/bmecat_new_catalog}"
# PATH = "{0}T_NEW_CATALOG/{0}PRODUCT/{0}PRODUCT_DETAILS/{0}DESCRIPTION_SHORT".format(BMENS)
PATH = "{0}T_NEW_CATALOG/{0}ARTICLE/{0}ARTICLE_DETAILS/{0}DESCRIPTION_SHORT".format(BMENS)

filepath = '/home/whtb/Downloads/preisliste/BMEcat_514281.xml'
     
tree = et.parse(filepath)
root = tree.getroot()
products = [entry.text
    for entry in root.findall(PATH)]
c = 0
for product in products:
    print(product)
    c += 1
    if c == 50:
        break
Die Ausgabe von product, ist die Spalte DESCRIPTION_SHORT.
Nun gibt es ja etliche Spalten, die ein Katalog-Produkt beschreiben und definieren.
Wie löst man dies, dass alle Spalten für ein Produkt ausgegeben werden?
Ich poste mal hier den Inhalt eines "Datensatzes" zu ARTICLE, hoffe dass es hilfreich für Euch ist.[codebox=xml file=Unbenannt.xml]
</ARTICLE>
<ARTICLE mode="new">
<SUPPLIER_AID>011108010423</SUPPLIER_AID>
<ARTICLE_DETAILS>
<DESCRIPTION_SHORT>Multifunktions-Papier inapa tecno star, A4, 80 g/m², weiß, 2500 Blatt</DESCRIPTION_SHORT>
<DESCRIPTION_LONG>Multifunktions-Papier inapa tecno star. Ein klassisches Geschäftspapier höchster Zuverlässigkeit und hoher Weiße für alle professionellen Anwendungen. Mit guten Abbildungs- und Laufeigenschaften auf allen modernen Bürogeräten (Laser, Copy, Inkjet, Fax). Und das zu einem wirklich überzeugenden Preis.
Für den umweltorientierten Einkauf auch in der TCF-Version verfügbar, produziert in Europa und unter Einsatz ausschließlich chlorfrei gebleichten Zellstoffs. Ausführung des Papiertyps: holzfrei. Farbe: weiß. Anzahl der Blätter je Packung: 2500 Blatt. Oberfläche: ungestrichen. Verwendung für Druck- oder Schreibgerät: Drucker, Fax, Kopierer. beidseitig bedruckbar. Papierformat: DIN A4 = 21,0 cm x 29,7 cm. Flächenmasse des Papiers: 80 g/m². </DESCRIPTION_LONG>
<EAN>4011211054962</EAN>
<MANUFACTURER_NAME>Inapa</MANUFACTURER_NAME>
<MANUFACTURER_TYPE_DESCR>inapa tecno star</MANUFACTURER_TYPE_DESCR>
<DELIVERY_TIME>1</DELIVERY_TIME>
</ARTICLE_DETAILS>
<ARTICLE_FEATURES>
<REFERENCE_FEATURE_SYSTEM_NAME>ECLASS-6.2</REFERENCE_FEATURE_SYSTEM_NAME>
<REFERENCE_FEATURE_GROUP_ID>24260604</REFERENCE_FEATURE_GROUP_ID>
<FEATURE>
<FNAME>Zolltarifnummer</FNAME>
<FVALUE>48025620</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Hersteller-Name</FNAME>
<FVALUE>Inapa</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Lieferanten-Artikelnummer</FNAME>
<FVALUE>011108010423</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>GTIN</FNAME>
<FVALUE>4011211054962</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Artikelbezeichnung</FNAME>
<FVALUE>inapa tecno star</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Farbe</FNAME>
<FVALUE>weiß</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Zertifikate und Zulassungen</FNAME>
<FVALUE>Sonstige</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Lieferanten-Name</FNAME>
<FVALUE>Papier Union GmbH</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Papierformat</FNAME>
<FVALUE>DIN A4 = 21,0 cm x 29,7 cm</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Anzahl der Blätter je Packung</FNAME>
<FVALUE>2500</FVALUE>
<FUNIT>C62</FUNIT>
</FEATURE>
<FEATURE>
<FNAME>Verwendung für Druck- oder Schreibgerät</FNAME>
<FVALUE>Drucker, Fax, Kopierer</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Oberfläche</FNAME>
<FVALUE>ungestrichen</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Ausführung des Papiertyps</FNAME>
<FVALUE>holzfrei</FVALUE>
</FEATURE>
<FEATURE>
<FNAME>Flächenmasse des Papiers</FNAME>
<FVALUE>80</FVALUE>
<FUNIT>gm2</FUNIT>
</FEATURE>
<FEATURE>
<FNAME>beidseitig bedruckbar</FNAME>
<FVALUE>Ja</FVALUE>
</FEATURE>
</ARTICLE_FEATURES>
<ARTICLE_ORDER_DETAILS>
<ORDER_UNIT>CT</ORDER_UNIT>
<CONTENT_UNIT>SH</CONTENT_UNIT>
<NO_CU_PER_OU>2500</NO_CU_PER_OU>
<PRICE_QUANTITY>1</PRICE_QUANTITY>
<QUANTITY_MIN>5</QUANTITY_MIN>
<QUANTITY_INTERVAL>5</QUANTITY_INTERVAL>
</ARTICLE_ORDER_DETAILS>
<ARTICLE_PRICE_DETAILS>
<ARTICLE_PRICE price_type="net_customer">
<PRICE_AMOUNT>Preis nicht vorhanden</PRICE_AMOUNT>
<PRICE_CURRENCY>EUR</PRICE_CURRENCY>
<TAX>0.19</TAX>
<LOWER_BOUND>1</LOWER_BOUND>
<TERRITORY>DE</TERRITORY>
</ARTICLE_PRICE>
</ARTICLE_PRICE_DETAILS>
<MIME_INFO>
<MIME>
<MIME_TYPE>application/pdf</MIME_TYPE>
<MIME_SOURCE>TechnischeDatenblaetter/Inapa_Tecno/TDB_it_star.pdf</MIME_SOURCE>
<MIME_PURPOSE>data_sheet</MIME_PURPOSE>
</MIME>
<MIME>
<MIME_TYPE>image/jpeg</MIME_TYPE>
<MIME_SOURCE>ProduktAbbildungen/MDS/MDS_inapatecno_star_EBD.jpg</MIME_SOURCE>
<MIME_PURPOSE>STE</MIME_PURPOSE>
<MIME_DESCR>Dieses Bild zeigt eine Ries-Abbildung des Artikels inapa tecno star</MIME_DESCR>
</MIME>
<MIME>
<MIME_TYPE>application/postscript</MIME_TYPE>
<MIME_SOURCE>ProduktAbbildungen/MDS/MDS_inapatecno_star_EBD.eps</MIME_SOURCE>
<MIME_PURPOSE>STE</MIME_PURPOSE>
<MIME_DESCR>Dieses Bild zeigt eine Ries-Abbildung des Artikels inapa tecno star</MIME_DESCR>
</MIME>
<MIME>
<MIME_TYPE>image/jpeg</MIME_TYPE>
<MIME_SOURCE>ProduktAbbildungen/MDS/MDS_inapatecno_Riese_premium_green_formula_star_colour_laser_pro_laser_SAB_WEB-FULLSIZE.jpg</MIME_SOURCE>
<MIME_PURPOSE>STS</MIME_PURPOSE>
<MIME_DESCR>Dieses Bild zeigt eine Abbildung der Produktfamilie inapa tecno Büropapiere</MIME_DESCR>
</MIME>
<MIME>
<MIME_TYPE>application/postscript</MIME_TYPE>
<MIME_SOURCE>ProduktAbbildungen/MDS/MDS_inapatecno_Riese_premium_green_formula_star_colour_laser_pro_laser_SAB.eps</MIME_SOURCE>
<MIME_PURPOSE>STS</MIME_PURPOSE>
<MIME_DESCR>Dieses Bild zeigt eine Abbildung der Produktfamilie inapa tecno Büropapiere</MIME_DESCR>
</MIME>
<MIME>
<MIME_TYPE>image/jpeg</MIME_TYPE>
<MIME_SOURCE>Logos_MDS/MDS_inapatecno_allgemein_Logo.jpg</MIME_SOURCE>
<MIME_PURPOSE>MLO</MIME_PURPOSE>
</MIME>
<MIME>
<MIME_TYPE>application/postscript</MIME_TYPE>
<MIME_SOURCE>Logos_MDS/MDS_inapatecno_allgemein_Logo.eps</MIME_SOURCE>
<MIME_PURPOSE>MLO</MIME_PURPOSE>
</MIME>
<MIME>
<MIME_TYPE>application/pdf</MIME_TYPE>
<MIME_SOURCE>TechnischeDatenblaetter/Inapa_Tecno/TDB_it_star.pdf</MIME_SOURCE>
<MIME_PURPOSE>PDB</MIME_PURPOSE>
</MIME>
</MIME_INFO>
<USER_DEFINED_EXTENSIONS>
<UDX.MDS.Kurztext>Multifunktions-Papier inapa tecno star, A4, 80 g/m², weiß, 2500 Blatt</UDX.MDS.Kurztext>
<UDX.MDS.MARKE>inapa tecno star</UDX.MDS.MARKE>
<UDX.MDS.Gattungsbegriff>Multifunktions-Papier</UDX.MDS.Gattungsbegriff>
<UDX.MDS.Langtext>Multifunktions-Papier inapa tecno star · Ausführung des Papiertyps: holzfrei · Farbe: weiß · Anzahl der Blätter je Packung: 2500 Blatt · Oberfläche: ungestrichen · Verwendung für Druck- oder Schreibgerät: Drucker, Fax, Kopierer · beidseitig bedruckbar · Papierformat: DIN A4 = 21,0 cm x 29,7 cm · Flächenmasse des Papiers: 80 g/m² · </UDX.MDS.Langtext>
<UDX.MDS.Marketingtext>Multifunktions-Papier inapa tecno star. Ein klassisches Geschäftspapier höchster Zuverlässigkeit und hoher Weiße für alle professionellen Anwendungen. Mit guten Abbildungs- und Laufeigenschaften auf allen modernen Bürogeräten (Laser, Copy, Inkjet, Fax). Und das zu einem wirklich überzeugenden Preis.
Für den umweltorientierten Einkauf auch in der TCF-Version verfügbar, produziert in Europa und unter Einsatz ausschließlich chlorfrei gebleichten Zellstoffs. Ausführung des Papiertyps: holzfrei. Farbe: weiß. Anzahl der Blätter je Packung: 2500 Blatt. Oberfläche: ungestrichen. Verwendung für Druck- oder Schreibgerät: Drucker, Fax, Kopierer. beidseitig bedruckbar. Papierformat: DIN A4 = 21,0 cm x 29,7 cm. Flächenmasse des Papiers: 80 g/m². </UDX.MDS.Marketingtext>
</USER_DEFINED_EXTENSIONS>[/code]
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Glaube eine Lösungen gefunden zu haben.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# For Python3.x

import lxml.etree as et
     
BMENS = "{http://www.bmecat.org/bmecat/1.2/bmecat_new_catalog}"
# PATH = "{0}T_NEW_CATALOG/{0}PRODUCT/{0}PRODUCT_DETAILS/{0}DESCRIPTION_SHORT".format(BMENS)
PATH = "{0}T_NEW_CATALOG/{0}ARTICLE".format(BMENS)
ROW_0 = "/{0}SUPPLIER_AID".format(BMENS)
ROW_1 = "/{0}ARTICLE_DETAILS/{0}DESCRIPTION_SHORT".format(BMENS)
DATA_LINE = [ROW_0, ROW_1]

filepath = '/home/whtb/Downloads/preisliste/BMEcat_514281.xml'
     
tree = et.parse(filepath)
root = tree.getroot()
products = dict()
for subpath in DATA_LINE:
    path = PATH + subpath
    for i, entry in enumerate(root.findall(path)):
        try:
            mylist = products[i]
            mylist.append(entry.text)
        except KeyError:
            products[i] = [entry.text]
c = 0
for i in products:
    product = products[i]
    print(product)
    c += 1
    if c == 50:
        break
Vielleicht gibt ess auch noch eine bessere Lösung?
BlackJack

@Nobuddy: Besser wäre es statt zweimal das gesamte Dokument erst nach den AIDs und dann nach den Beschreibungen zu durchsuchen, eine Schleife über die Artikel in dem Dokument zu schreiben und aus jedem Artikel dann die AID und Beschreibung zu holen.
BlackJack

Komplett ungetestet:

Code: Alles auswählen

from lxml import etree
     
BME_NS = '{http://www.bmecat.org/bmecat/1.2/bmecat_new_catalog}'
ARTICLE_PATH = '{0}T_NEW_CATALOG/{0}ARTICLE'.format(BME_NS)
SUPPLIER_AID_SUBPATH = './{0}SUPPLIER_AID'.format(BME_NS)
SHORT_DESCRIPTION_SUBPATH = './{0}ARTICLE_DETAILS/{0}DESCRIPTION_SHORT'.format(
    BME_NS
)
ARTICLE_SUBPATHS = [SUPPLIER_AID_SUBPATH, SHORT_DESCRIPTION_SUBPATH]


def main():
    filepath = '/home/whtb/Downloads/preisliste/BMEcat_514281.xml'
    
    root = etree.parse(filepath).getroot()
    articles = [
        [article_node.find(p).text for p in ARTICLE_SUBPATHS]
        for article_node in root.findall(ARTICLE_PATH)
    ]
    
    for article in articles[:50]:
        print(article)


if __name__ == '__main__':
    main()
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Hallo BlackJack,

das mit dem einzelnen Durchlaufen des Dokumentes zum Erstellen der Spalten in einem Datensatz, ist wirklich nicht optimal und braucht auch mehr Zeit zum Abarbeiten, als wie Dein Vorschlag. In der Regel bleibt es ja nicht nur bei 2 Spalten in einer Preisliste.
Danke auch für Deinen Code-Vorschlag, werde in gleich testen!

Eine Frage ist da noch, wie erhalte ich den Wert root (BME_NS), wenn er nicht wie im Code oben schon mit angegeben ist?

Grüße Nobuddy
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Habe das Ganze nun so umgesetzt:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# For Python3.x

from lxml import etree


class XML_BMECAT(object):
    """
    Verarbeitung von XML-Dateien im Format BMECAT.
    """

    def __init__(self, order, filepath, xml_startpath, xml_paths):

        self.order = order
        self.filepath = filepath
        self.xml_startpath = xml_startpath
        self.xml_paths = xml_paths


    def start(self):

        if self.order.upper() == 'XML2TEXT':
            self.build_paths()
            return self.build_list()
        if self.order.upper() == 'TEXT2XML':
            print('Abbruch, noch in Arbeit!')
            return
            


    def build_paths(self):

        self.root = etree.parse(self.filepath).getroot()

        # Überprüfung ob Austauschformat BMECAT ist.
        if not self.root.tag.endswith('BMECAT'):
            text = 'Abbruch, keine Unterstützung für Austauschformat'
            print('{} < {} >!'.format(text, self.root.tag.split('}')[1]))
            return
            
        # bmens = root.tag.replace('BMECAT', '')
        bmens = self.root.tag.split('}')[0] + '}'

        # Basispfad
        self.startpath = ''.join(["{}{}/".format(bmens, name)
            for name in self.xml_startpath])[:-1]

        self.subpaths = list()
        for names in self.xml_paths:
            sub_path = ['.']
            for name in names:
                sub_path.append('/')
                sub_path.append(bmens)
                sub_path.append(name)
            self.subpaths.append(''.join(sub_path))
        
     

    def build_list(self):

        data = list()
        for article_node in self.root.findall(self.startpath):
            data_line = list()
            for p in self.subpaths:
                try:
                    data_line.append(article_node.find(p).text)
                except AttributeError:
                    data_line.append('')
            data.append(data_line)
        return data

        # Fehler bei nicht vorhandenem subpath.
        #data = [
        #    [article_node.find(p).text for p in self.subpaths]
        #    for article_node in self.root.findall(self.startpath)
        #    ]

def main():
    order = 'xml2text'
    filepath = '/home/whtb/Downloads/preisliste/BMEcat_514281.xml'
    xml_startpath = 'T_NEW_CATALOG', 'ARTICLE'
    xml_paths = [
        ['ARTICLE_DETAILS', 'MANUFACTURER_NAME'],
        ['SUPPLIER_AID'],
        ['ARTICLE_DETAILS', 'DESCRIPTION_SHORT'],
        ['ARTICLE_DETAILS', 'EAN'],
        ['ARTICLE_ORDER_DETAILS', 'ORDER_UNIT'],
        ['ARTICLE_ORDER_DETAILS', 'CONTENT_UNIT'],
        ['ARTICLE_ORDER_DETAILS', 'NO_CU_PER_OU'],
        ['ARTICLE_ORDER_DETAILS', 'PRICE_QUANTITY'],
        ['ARTICLE_ORDER_DETAILS', 'QUANTITY_MIN'],
        ['ARTICLE_ORDER_DETAILS', 'QUANTITY_INTERVAL'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'PRICE_AMOUNT'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'PRICE_CURRENCY'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'TAX'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'LOWER_BOUND'],
        ]
   
    data = XML_BMECAT(order, filepath, xml_startpath, xml_paths).start()
    if data:
        # Test-Ausgabe
        for row in data[:50]:
            print(row)
 
 
if __name__ == '__main__':
    main()
Zu

Code: Alles auswählen

bmens = self.root.tag.split('}')[0] + '}'
habe ich noch keine Alternative gefunden.
Es gäbe zwar die Möglichkeit dies so zu machen:

Code: Alles auswählen

bmens = root.tag.replace('BMECAT', '')
Das funktioniert aber nur, wenn kein zweites BMECAT (Großschreibung vorhanden ist). Ich glaube dass dies nicht unbedingt sichergestellt werden kann.

Das

Code: Alles auswählen

data = [
            [article_node.find(p).text for p in self.subpaths]
            for article_node in self.root.findall(self.startpath)
            ]
konnte ich leider bisher nicht umsetzen, da wie ich festgestellt habe, es vorkommt dass manche Subpfade nicht überall vorkommen.

Die Namensgebung, habe ich versucht neutral zu halten, aber da habe ich immer noch Probleme die richtige Wahl zu treffen ...

Würde mich über Eure Kritik und weitere Vorschläge freuen!
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Nobuddy: der Name der Klasse XML_BMECAT ist so geschrieben, als wäre es eine Konstante, besser wäre XmlBmecat. Aber die Klasse macht, so wie sie jetzt da steht überhaupt keinen Sinn, besser wären zwei Funktionen read_bmecat und write_bmecat. Namespaces sind nicht nur Folklore sonder zwei Tags »{xyz}catalog« und »{abc}catalog« sind völlig verschieden. Daher solltest Du auch nicht einfach nehmen, was da kommt. Dann hast Du auch kein Problem mehr mit Deinem split.

Code: Alles auswählen

from lxml import etree

BME_NS = '{http://www.bmecat.org/bmecat/1.2/bmecat_new_catalog}'

def generate_bmepath(path):
    return '/'.join('{}{}'.format(BME_NS, t) for t in path)

def read_bmecat(filename, xml_startpath, xml_paths)
    root = etree.parse(filename).getroot()
    assert root.tag == "{}BMECAT".formta(BME_NS), "Falsches Format"
 
    xml_startpath = generate_bmepath(xml_startpath)
    xml_paths = [generate_bmepath(path) for path in xml_paths]
    
    result = []
    for entry in root.findall(xml_startpath):
        result.apend([
            entry.findtext(path)
            for path in xml_paths
        ])
    return result
 
def main():
    filepath = '/home/whtb/Downloads/preisliste/BMEcat_514281.xml'
    xml_startpath = 'T_NEW_CATALOG', 'ARTICLE'
    xml_paths = [
        ['ARTICLE_DETAILS', 'MANUFACTURER_NAME'],
        ['SUPPLIER_AID'],
        ['ARTICLE_DETAILS', 'DESCRIPTION_SHORT'],
        ['ARTICLE_DETAILS', 'EAN'],
        ['ARTICLE_ORDER_DETAILS', 'ORDER_UNIT'],
        ['ARTICLE_ORDER_DETAILS', 'CONTENT_UNIT'],
        ['ARTICLE_ORDER_DETAILS', 'NO_CU_PER_OU'],
        ['ARTICLE_ORDER_DETAILS', 'PRICE_QUANTITY'],
        ['ARTICLE_ORDER_DETAILS', 'QUANTITY_MIN'],
        ['ARTICLE_ORDER_DETAILS', 'QUANTITY_INTERVAL'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'PRICE_AMOUNT'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'PRICE_CURRENCY'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'TAX'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'LOWER_BOUND'],
    ]
   
    data = read_bmecat(order, filepath, xml_startpath, xml_paths)
    for row in data[:50]:
        print(row)
 
if __name__ == '__main__':
    main()
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Hallo Sirius3,

das mit XML ist für mich Neuland und bin bisher von manchen falschen Voraussetzungen aus gegangen.

Bei der Benennung der Klasse, dürfte auch 'class XML' ausreichen, wenn alle XML-Formate mit den Funktionen 'generate_bmepath' und 'read_bmecat', zum gleichen Ergebnis kommen. Ist da meine Vermutung richtig?

Was mich noch beschäftigt ist die konkrete Angabe von 'BME_NS' im Code.

Code: Alles auswählen

BME_NS = '{http://www.bmecat.org/bmecat/1.2/bmecat_new_catalog}'
Muss ich immer zuvor herausfinden, wie der Wert von 'BME_NS' ist und diesen dann im Code eintragen, oder gibt es doch nicht eine Möglichkeit dies im Vorfeld beim Einlesen der XML-Datei festzustellen?
Es kann ja sein, dass der Lieferant irgendwann von Version 1.2 auf Version 2005 wechselt, wo statt ARTICLE durch PRODUCT ersetzt wurde.
Wobei die Änderung von ARTICLE zu PRODUCT, im Code sich leicht im voraus schon gehandelt werden kann.

Wie sieht es bei Zeile 15 bis 20 aus:

Code: Alles auswählen

    result = []
    for entry in root.findall(xml_startpath):
        result.apend([
            entry.findtext(path)
            for path in xml_paths
        ])
Wenn z.B. in einem Datensatz der XML-Datei ein Pfad fehlt, wird dann ein leerer Wert an result übergeben?
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Nobuddy: ob es sich um Version 1.2 oder Version 2005 handelt, kann man ja leicht am Root-Tag unterscheiden, beim einen ist es »{http://www.bmecat.org/bmecat/1.2/bmecat ... log}BMECAT«, beim anderen »{http://www.bmecat.org/bmecat/2005fd}BMECAT«. Auch alle anderen Tags heißen anders. Warum die Entwickler des Formats sich dafür entschieden haben, alle Tags umzubenennen, ist deren Geheimnis. Wahrscheinlich haben sie den Sinn von Namespaces nicht verstanden, da diese nicht dazu dienen, verschiedene Versionen voneinander zu unterscheiden, sondern es erlauben, verschiedene Komponenten in einer Datei zu speichern. Dass ich den Namespace als Konstante herausgezogen habe, hat einzig und allein Gründe für die Lesbarkeit. Wenn ich in einem String mehrere male die selben 52 Zeichen lesen muß, kann ich nur schwer die wirklichen Unterschiede erkennen.

Um es noch einmal klar zu sagen: Der Namespace-Teil gehört zum Tag dazu, wie der Vorname zum Nachnamen gehört. Du würdest ja auch nicht, wenn Du einen "Peter Müller" suchst, eine Addressliste durchgehen und vom ersten Müller den Vornamen herauslesen und sagen ich nehme jetzt immer "Annette" als Vornamen.

Zur letzten Frage: warum probierst Du es nicht einfach aus?
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Sirius3, Danke für die Info!
Zu meiner letzten Frage, das funktioniert.

Ich habe aus dem Link von BlackJack, weitere Bausteine gefunden, um aus der XML-Datei wichtige Infos zu erhalten.
Das Ganze ist ein klein bisschen umfangreicher geworden.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# For Python3.x

from lxml import etree

"""Private Module"""
from gui_MyMessage import MessageBox as MB

# Unterstützte XML-Formate
XML_FORMATE = {'test' : 'test', 'BMECAT' : 'BMECAT'}


class XML(object):
    """
    Verarbeitung von XML-Dateien im Format XML_FORMATE.
    """

    def __init__(self, filepath, xml_startpath, xml_paths):

        self.filepath = filepath
        self.xml_startpath = xml_startpath
        self.xml_paths = xml_paths


    def control_integrity(self, xml_format, xml_children, root):
        """
        Überprüfung auf XML-Format und Datenintegrität.
        Aktualisierung von xml_startpath und xml_paths, bei Versionänderung.
        """

        # Überprüfung frei gegebener XML-Formate
        try:
            text = 'Abbruch, falsches Format'
            assert root.tag == '{}{}'.format(self.xml_head,
                XML_FORMATE[xml_format]), text
        except AssertionError as e:
            print(e)
            return
        # Erstellung XML-Startpfad
        xml_startpath = '{}/{}'.format(xml_children[-1].tag,
            self.generate_path(self.xml_startpath))
        # Überprüfung von xml_startpath in root
        if not [x for x in root.iterfind(xml_startpath)]:
            # Textbausteine zur Ausgabe des Statusinfo
            self.check = 'Bitte Überprüfung Datenintegrität'
            text_0 = 'Änderung Version und XML-Struktur!'
            text_1 = 'vor Freigabe der Daten!'
            text_10 = 'Abbruch, Durchführung des Auftrages nicht möglich!'
            if xml_format == 'BMECAT':
                change2startname = {
                    'ARTICLE' : 'PRODUCT',
                    'PRODUCT' : 'ARTICLE'
                    }
                startname_data = ''.join([name
                    for name in ['ARTICLE', 'PRODUCT']
                    if name == xml_startpath.split('}')[-1]])
                # Aktualisierung self.xml_startpath
                self.xml_startpath = [path.replace(startname_data,
                    change2startname[startname_data])
                    for path in self.xml_startpath]
                # Erstellung XML-Startpfad
                xml_startpath = '{}/{}'.format(xml_children[-1].tag,
                    self.generate_path(self.xml_startpath))
                # Aktualisierung self.xml_paths
                self.xml_paths = [path.replace(startname_data,
                    change2startname[startname_data])
                    for path in self.xml_paths]
                MB().ok('{}\n{} {}'.format(text_0, self.check, text_1))
            else:
                MB().ok('{}\n{} {}'.format(text_0, text_10))
                return
        return xml_startpath


    def generate_path(self, path):
        """
        Erstellung und Ausgabe XML-Pfaden.
        """

        return '/'.join('{}{}'.format(self.xml_head, t) for t in path)
     

    def read_xml(self):
        """
        Lade XML-Datei und beschaffe die notwendigen Informationen,
        zum Erstellen der XML-Pfade für das Auslesen der Daten.
        Ausgabe der erstellten Liste.
        """

        root = etree.parse(self.filepath).getroot()
        xml_children = root.getchildren()
        # Head
        self.xml_head = xml_children[0].tag.replace('HEADER', '')
        # XML-Format
        xml_format = root.tag.replace(self.xml_head, '')
        self.xml_startposition = xml_children[-1].tag
        # Kontrolle Integritätsbedingung, Übergabe xml_startpath
        xml_startpath = self.control_integrity(xml_format, xml_children,
            root)
        if not xml_startpath:
            return
        # Erstellung XML-Pfade für Daten
        xml_paths = [self.generate_path(path) for path in self.xml_paths]

        result = []
        for entry in root.findall(xml_startpath):
            result.append([
                entry.findtext(path)
                for path in xml_paths
            ])

        try:
            # Überprüfung Datenintegrität, bei Änderung von XML-Struktur
            self.check
            for row in result[:5]:
                print(row)
                print('----------')
            if not MB().yesno('Daten frei geben?'):
                return
        except AttributeError:
            pass
        return result

def main():
    filepath = '/home/whtb/Downloads/preisliste/BMEcat_514281.xml'
    xml_startpath = ['ARTICLE']
    xml_paths = [
        ['ARTICLE_DETAILS', 'MANUFACTURER_NAME'],
        ['SUPPLIER_AID'],
        ['ARTICLE_DETAILS', 'DESCRIPTION_SHORT'],
        ['ARTICLE_DETAILS', 'EAN'],
        ['ARTICLE_ORDER_DETAILS', 'ORDER_UNIT'],
        ['ARTICLE_ORDER_DETAILS', 'CONTENT_UNIT'],
        ['ARTICLE_ORDER_DETAILS', 'NO_CU_PER_OU'],
        ['ARTICLE_ORDER_DETAILS', 'PRICE_QUANTITY'],
        ['ARTICLE_ORDER_DETAILS', 'QUANTITY_MIN'],
        ['ARTICLE_ORDER_DETAILS', 'QUANTITY_INTERVAL'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'PRICE_AMOUNT'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'PRICE_CURRENCY'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'TAX'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'LOWER_BOUND'],
        ]
   
    data = XML(filepath, xml_startpath, xml_paths).read_xml()
    if data:
        # Test-Ausgabe
        for row in data[:50]:
            print(row)
 
 
if __name__ == '__main__':
    main()
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Nobuddy: Du kannst Dich doch nicht für die Info bedanken aber mit dem gleichen #*!$ weitermachen!

1. Das was Du da als Klasse schreibst ist formal keine Klasse.
2. self.xml_head ist der falsche Name für einen Namespace
3. Namespaces sind nichts, was man dynamisch aus einer XML-Datei herausliest, sondern das ist eine KONSTANTE, die für die Beschreibung des Dateiformats notwendig ist.
4. Wenn Du beide Versionen unterstützen willst, mußt Du anhand des Root-Tags entscheiden, welche der beiden Varianten vorliegt und entsprechen eine andere Liste für xml_paths verwenden. Im Prinzip ist xml_paths auch eine Konstante. control_integrity gehört damit weg.
5. Eine Liste ist der falsche Datentyp, nimm ein Wörterbuch.
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Ok, manchmal darf man auch ins Fettnäpfchen treten ... :wink:

Zu Punkt 1:
Ab wann wird es eine Klasse?
Das ist doch noch weiter ausbaubar.

Zu Punkt 2:
Das hatte ich mir auch schon überlegt, warum dann nicht eigentlich einfach 'namespace'.

Zu Punkt 3:
Das Herauslesen bietet sich doch an und ist eine Konstante der Datei und bietet doch einen Vergleich zur eigenen festgesetzten Konstante.

Zu Punkt 4:
Das verstehe ich, so können sich auch nicht so schnell Fehler einschleichen.

Zu Punkt 5:
Verstehe ich nicht, zu welchem Bezug?

Ich habe einige Punkte verändert, wahrscheinlich aber noch nicht ganz, was Du Dir vorstellst.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# For Python3.x

from lxml import etree

"""Private Module"""
from gui_MyMessage import MessageBox as MB

# Unterstützte XML-Formate
XML_FORMATE = {'test' : 'test', 'BMECAT' : 'BMECAT'}


class XML(object):
    """
    Verarbeitung von XML-Dateien im Format XML_FORMATE.
    """

    def __init__(self, filepath, key2xml):

        self.filepath = filepath
        self.key2xml = key2xml


    def generate_path(self, path):
        """
        Erstellung und Ausgabe XML-Pfaden.
        """

        return '/'.join('{}{}'.format(self.namespace, t) for t in path)
     

    def read_xml(self):
        """
        Lade XML-Datei und beschaffe die notwendigen Informationen,
        zum Erstellen der XML-Pfade für das Auslesen der Daten.
        Ausgabe der erstellten Liste.
        """

        root = etree.parse(self.filepath).getroot()
        xml_children = root.getchildren()
        # Namespaces
        self.namespace = xml_children[0].tag.replace('HEADER', '')
        # XML-Format
        xml_format = root.tag.replace(self.namespace, '')
        # Integritätsprüfung
        try:
            text = 'Abbruch, falsches Format'
            assert root.tag == '{}{}'.format(self.namespace,
                XML_FORMATE[xml_format]), text
            text = 'Abbruch, falsche {}-Version'.format(xml_format)
            assert 2 == len(self.key2xml[root.tag]), text
            xml_startpath, xml_paths = self.key2xml[root.tag]
        except AssertionError as e:
            MB().ok(e)
            return
        # Erstellung XML-Startpfad
        xml_startpath = '{}/{}'.format(xml_children[-1].tag,
            self.generate_path(xml_startpath))
        # Erstellung XML-Pfade für Daten
        xml_paths = [self.generate_path(path) for path in xml_paths]

        result = []
        for entry in root.findall(xml_startpath):
            result.append([
                entry.findtext(path)
                for path in xml_paths
            ])
        return result

def main():
    filepath = '/home/whtb/Downloads/preisliste/BMEcat_514281.xml'
    root_tag = '{http://www.bmecat.org/bmecat/1.2/bmecat_new_catalog}BMECAT'
    xml_startpath = ['ARTICLE']
    xml_paths = [
        ['ARTICLE_DETAILS', 'MANUFACTURER_NAME'],
        ['SUPPLIER_AID'],
        ['ARTICLE_DETAILS', 'DESCRIPTION_SHORT'],
        ['ARTICLE_DETAILS', 'EAN'],
        ['ARTICLE_ORDER_DETAILS', 'ORDER_UNIT'],
        ['ARTICLE_ORDER_DETAILS', 'CONTENT_UNIT'],
        ['ARTICLE_ORDER_DETAILS', 'NO_CU_PER_OU'],
        ['ARTICLE_ORDER_DETAILS', 'PRICE_QUANTITY'],
        ['ARTICLE_ORDER_DETAILS', 'QUANTITY_MIN'],
        ['ARTICLE_ORDER_DETAILS', 'QUANTITY_INTERVAL'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'PRICE_AMOUNT'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'PRICE_CURRENCY'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'TAX'],
        ['ARTICLE_PRICE_DETAILS', 'ARTICLE_PRICE', 'LOWER_BOUND'],
        ]
    key2xml = {root_tag : (xml_startpath, xml_paths)}
   
    data = XML(filepath, key2xml).read_xml()
    if data:
        # Test-Ausgabe
        for row in data[:50]:
            print(row)
 
 
if __name__ == '__main__':
    main()
BlackJack

@Nobuddy: Es ist eine Klasse wenn es Daten und Funktionen die darauf operieren sinnvoll zu einem Objekt zusammenfasst. Also dann wenn es gegenüber Funktionen einen Vorteil bietet.

`namespace` ist ein bisschen Allgemein. Dem Leser stellt sich dann als nächstes die Frage welcher Namespace es ist. Der Sinn von Namespaces ist ja gerade das man mehrere davon haben kann.

Die Elemente vom Ergebnis von `read_xml()` sind Listen wo die einzelnen Elemente abhängig vom Index eine andere Bedeutung haben. Das heisst man muss wissen was welcher Index bedeutet und wenn man darauf zugreifen will braucht man magische Indexnummern. Das ist die Stelle wo man ein Wörterbuch oder ein `collections.namedtuple` verwenden würde, damit der Code lesbar und verständlich bleibt. Da die Daten am Ende in eine CSV-Datei gechrieben werden sollen, würde sich hier beispielsweise ein Wörterbuch anbieten und ein `csv.DictWriter` zum schreiben der Daten.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Nobuddy: das read_xml kann man ganz einfach als Funktion definieren, ohne irgendwelche Einschränkungen zu haben. Was soll eine Konstante der Datei sein? Inhalte von Dateien sind per Definition variabel. XML ist als Dateiformat sehr strikt. Jede kleinste Abweichung vom Erwarteten wird normalerweise als Fehler geahndet. Benutzinteraktion sollte nicht in einer Funktion erfolgen, die so generelle Sachen macht, wie eine Datei lesen. Das gehört auf einer höheren Ebene abgearbeitet. Fehler werden als Exceptions hochgemeldet.

Code: Alles auswählen

from collections import namedtuple
from lxml import etree
 
XmlFormatDescription = namedtuple("XmlFormatDescription", "namespace, start_path, details")
 
# Unterstützte XML-Formate
XML_DESCRIPTIONS = {
    '{http://www.bmecat.org/bmecat/1.2/bmecat_new_catalog}BMECAT': XmlFormatDescription(
        "{http://www.bmecat.org/bmecat/1.2/bmecat_new_catalog}",
        ["ARTICLE"],
        {
            "manufacturer": ['ARTICLE_DETAILS', 'MANUFACTURER_NAME'],
            "supplier_aid": ['SUPPLIER_AID'],
            "description": ['ARTICLE_DETAILS', 'DESCRIPTION_SHORT'],
            "ean": ['ARTICLE_DETAILS', 'EAN'],
        })
    '{http://www.bmecat.org/bmecat/2005fd}BMECAT': XmlFormatDescription(
        "{http://www.bmecat.org/bmecat/2005fd}",
        ["PRODUCT"],
        {
            "manufacturer": ['PRODUCT_DETAILS', 'MANUFACTURER_NAME'],
            "supplier_aid": ['SUPPLIER_AID'],
            "description": ['PRODUCT_DETAILS', 'DESCRIPTION_SHORT'],
            "ean": ['PRODUCT_DETAILS', 'EAN'],
        })
}

def generate_path(namespace, path):
    """
    Erstellt einen XML-Pfaden.
    """
    return '/'.join('{}{}'.format(namespace, t) for t in path)
     
def read_xml(filename, descriptions):
    """
    Lade XML-Datei in eine Liste mit Wörterbüchern.
    """
    root = etree.parse(self.filepath).getroot()
    description = descriptions[root.tag]
    
    start_path = generate_path(description.namespace, description.start_path)
    xml_paths = {key: generate_path(description.namespace, path) for key, path in description.details.items()}
    result = []
    for entry in root.findall(start_path):
        for key, path in xml_paths.items():
            result[key] = entry.findtext(path)
        return result
 
def main():
    filepath = '/home/whtb/Downloads/preisliste/BMEcat_514281.xml'
    data = read_xml(filepath, XML_DESCRIPTIONS)
    for row in data[:50]:
        print(row)
 
if __name__ == '__main__':
    main()
Nobuddy
User
Beiträge: 994
Registriert: Montag 30. Januar 2012, 16:38

Hallo und Danke Euch beiden, für Eure Geduld und Hilfe! :wink:

Habe mir gestern bei BMECAD, die verschiedenen Schnittellenbeschreibungen angeschaut.
Mir ist jetzt klar geworden, dass ich von einem falschen Ausgangspunkt das Ganze betrachtet habe.
Was ich will, ist Produktdaten von Lieferanten in mein System einzuspielen. Da werden unterschiedlichste Datei-Formate angeboten, mit denen man sich oft intensiv auseinandersetzen muss, bevor man die Daten in das System einspielen kann.
BMECAD bietet eine Schnittstellenbeschreibung, diese ist eine Konstante für alle die dies für Ihren Produktkatalog verwenden.
Dies vereinfacht das Ganze, da man das Rad nicht jedes mal neu erfinden muss.

Jetzt ist mir auch klar geworden, was Ihr mir mit Euren Beiträgen klar machen wollt.
Sirius3, Dein letztes Code-Beispiel, hat mir das nochmals veranschaulicht, wie und wo die Prioritäten einzusetzen sind.

Ich werde das letzte Code-Beispiel von Sirius3 als Basis verwenden und die Schnittstellenbeschreibung von BMECAT zu studieren.

Bei weiteren Fragen zu diesem Thema melde ich mich wieder.

Grüße Nobuddy
Antworten