XML parsen: Probleme mit key / Value Einträgen

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
puemer
User
Beiträge: 8
Registriert: Dienstag 3. Juli 2012, 16:33

Hi Python Freunde
Als blutiger Python-Anfänger habe ich eine für mich interessante, aber eben echt schwierige Aufgabe zu lösen: Wir verwalten Mobiles für ein paar Personen. Unser Mobile Device Management Programm hat folgendes Reporting:

Code: Alles auswählen

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<deviceManagementWebServiceResponse>
    <messages>
        <message>15 Device(s) returned.</message>
    </messages>
    <devices>
        <device id="16">
            <uuid>0765458d-8d1c-4b5d-934c-f14897def35d</uuid>
            <principal>mwendlin</principal>
            <clientId>1073741846</clientId>
            <countryCode>46</countryCode>
            <countryId>165</countryId>
            <countryName>Sweden</countryName>
            <details>
                <entry>
                    <key>ImeiOrMeid</key>
                    <value>35721503047246101</value>
                </entry>
            </details>
            <deviceCount>0</deviceCount>
            <emailAddress>marcus.wendling@systeam.se</emailAddress>
            <employeeOwned>false</employeeOwned>
            <homeOperator>42IT</homeOperator>
            <manufacturer>HTC</manufacturer>
            <mdmManaged>false</mdmManaged>
            <model>HTC HD mini T5555</model>
            <name>mwendlin:WinMo:761055694</name>
            <notifyUser>true</notifyUser>
            <operator>42IT</operator>
            <operatorId>27</operatorId>
            <phoneNumber>761055694</phoneNumber>
            <platform>Windows Mobile 6.5 Professional</platform>
            <platformType>WINMO_6_5_PRO</platformType>
            <regCount>0</regCount>
            <regType>DEFAULT</regType>
            <status>ACTIVE</status>
            <statusCode>97</statusCode>
            <userDisplayName>Marcus Wendling</userDisplayName>
            <userFirstName>Marcus</userFirstName>
            <userLastName>Wendling</userLastName>
            <userUUID>49197f4e-c659-4b16-b43a-877c95d51354</userUUID>
        </device>
    </devices>
</deviceManagementWebServiceResponse>
Dieses XML-File würde ich nun liebend gerne mittels einem Pythonscript parsen und kontrollieren, ob alle IMEI Nummern auch in der csv Liste vom Provider zu finden sind:

Code: Alles auswählen

Firmenname;IMEI;Mobile Nr;
Fun&Work;89454957963543670;766432098;
Fun&Work;35721503047246101;761055694;
Fun&Work;53456213433509722;752210998;
Als Code habe ich schon mal:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf8 -*-

#!/usr/bin/env python

import codecs
from xml.etree import ElementTree as et
files = file("devices.xml", "r")
etree = et.parse(files)

root_tag = etree.getroot()
for deviceslist in root_tag.findall("devices"):
    for device in deviceslist.findall("device"):
        uuid = device.find("uuid").text
        Name = device.find("userDisplayName").text
	print Name
	print uuid
Das funktioniert, jedoch komme ich keine Stufe weiter herunter im XML-File. Wie lese ich nun die Struktur

Code: Alles auswählen

<entry>
                    <key>ImeiOrMeid</key>
                    <value>35721503047246101</value>
</entry>
aus?

Vielen herzlich Dank schon im Voraus für Eure Hilfe!

Puemer
Zuletzt geändert von Anonymous am Sonntag 17. Februar 2013, 23:45, insgesamt 1-mal geändert.
Grund: Quelltexte in Code-Tags gesetzt.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

XPath könnte Dir weiterhelfen.
BlackJack

@puemer: Das kann ich irgendwie nicht nachvollziehen. Du kennst sowohl die `find()`- als auch die `findall()`-Methode. Das funktioniert genau so wie bei den „höheren Stufen”. Du kennst also schon alle nötigen Methoden. Den <details>-Unterbaum würde ich in ein Wörterbuch (`dict`) verarbeiten. Es sei denn Du willst tatsächlich nur diesen einen Wert zu dem Schlüssel 'ImeiOrMeid', dann reicht es die <entry>\s durchzugehen, bis man den passenden Schlüsselwert gefunden hat.

@kbr: `xml.etree` hat nur eine ziemlich eingeschränkte XPath-Unterstützung. Wenn man mehr will, muss man `lxml.etree` verwenden.
puemer
User
Beiträge: 8
Registriert: Dienstag 3. Juli 2012, 16:33

Hi kbr und BlackJack
Besten Dank für Eure Tipps!

@BlackJack: Den <details>-Unterbaum würde ich in ein Wörterbuch (`dict`) verarbeiten.

Das ist sicherlich eine sehr gute Idee. Wörterbuch (`dict`) ist scheinbar eine Art Array. Sorry, ich bin wie gesagt noch ein blutiger Anfänger. Den Code oben habe ich mir aus einem Beispiel abgeschaut.

Wenn Du mir ein kleines Beispiel machen könntest, wäre ich Dir echt dankbar.

Gruss
Puemer
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

So in der Art:

Code: Alles auswählen

from xml.etree import ElementTree as et
devicemanagement = et.parse("devices.xml")

for device in devicemanagement.findall('devices/device'):
    details = dict((entry.findtext('key'), entry.findtext('value')) for entry in device.findall('details/entry'))
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

@puemer: Du kannst beispielsweise folgendes machen:

Code: Alles auswählen

for device in devices.findall("device"):
    key = device.find("./details/entry/key").text
    value = device.find("./details/entry/value").text
Mit key und value kannst Du dann ein Dictionary aufbauen, das am Ende die "IMEI"s als keys und die zugehörigen Nummern als values aufweist.
Das ist vielleicht noch nicht die ganz große Kunst, bringt Dich aber Deinem Ziel näher.

Und hier noch die kompakte Variante :) :

Code: Alles auswählen

print {device.findtext("./details/entry/key"):
       device.findtext("./details/entry/value")
       for device in root_tag.iter("device")}
puemer
User
Beiträge: 8
Registriert: Dienstag 3. Juli 2012, 16:33

Hi Sirius3, hi kbr
Entschuldigt dass ich mich erst jetzt melde...

Habt vielen Dank für Eure Beispiele, welche mich bestimmt einen Schritt weiter bringen. Aller Anfang ist schwer...

Einlesen kann ich die xml Daten jetzt in ein Python dict. Jetzt mache ich mich grad schlau, wie ich die Key Values wieder ansprechen kann. Das wird wahrscheinlich die
dictionary get() Methode sein:

dict.get('ImeiOrMeid')

Nochmals Dank & Gruss
Puemer
Benutzeravatar
/me
User
Beiträge: 3555
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

puemer hat geschrieben:Jetzt mache ich mich grad schlau, wie ich die Key Values wieder ansprechen kann. Das wird wahrscheinlich die
dictionary get() Methode sein:

dict.get('ImeiOrMeid')
Ein kurzer Blick ins offizielle Tutorial zeigt dir eine einfachere Variante. => http://docs.python.org/2/tutorial/datas ... ctionaries
puemer
User
Beiträge: 8
Registriert: Dienstag 3. Juli 2012, 16:33

So, jetzt bekomme ich alle imei Nummern und die dazugehörenden Namen aus dem xml-File:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf8 -*-

#!/usr/bin/env python

import csv
import sys

filename = sys.argv[1]
row_number, column_number = [int(arg, 10)-1 for arg in sys.argv[2:])]

from xml.etree import ElementTree as et
files = file("devices.xml", "r")
devicemanagement = et.parse(files)

root_tag = devicemanagement.getroot()

for device in devicemanagement.findall('devices/device'):
	uuid = device.find("uuid").text
        Name = device.find("userDisplayName").text
        print Name
        #print uuid
	details = dict((entry.findtext('key'), entry.findtext('value')) for entry in device.findall('details/entry'))
	if details.get('ImeiOrMeid') is not None: print details.get('ImeiOrMeid').replace('-', '').replace(' ', '') 
	if details.get('imei') is not None: print details.get('imei').replace('-', '').replace(' ', '')
Jetzt muss ich noch die imei Nummern im csv-File finden (Abfrage ob vorhanden, ja / nein). Hat mir da jemand vielleicht noch so einen netten Tipp wie open? Wie suche ich einen spezifischen Wert in einer csv-Datei und gebe aus ob der Wert in der csv-Datei ist oder nicht?



Besten Dank schon im Voraus für jeden Hinweis!

Puemer
Zuletzt geändert von Anonymous am Mittwoch 20. Februar 2013, 00:50, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
BlackJack

@puemer: Setz den Quelltext das nächste mal doch bitte selbst in die entsprechenden Tags. Dann habe ich mehr Zeit für Anderes. :-)

Importe kommen üblicherweise vor dem anderen Code, damit man am Anfang schon sieht wovon ein Modul abhängt und es auch nicht so leicht passiert, dass man Importe aus versehen mehrfach ausführt.

`filename`, `row_number`, `column_number`, und `root_tag` werden nirgends verwendet!?

Dateien die man öffnet sollte man auch wieder schliessen. Die ``with``-Anweisung ist in dem Zusammenhang hilfreich.

`files` ist ein schlechter Name für *ein* Dateiobjekt. Welches man mit `open()` öffnen sollte. `file` ist als Basisklasse gedacht, falls man eigene Dateitypen davon ableiten möchte. Ab Python 3 gibt es den Namen `file` auch gar nicht mehr als „built-in”.

Einrücktiefe ist per Konvention vier Leerzeichen und nach einem Kontrollkonstrukt mit einem ``:`` sollte man eine neue, eingerückte Zeile anfangen, auch wenn es nur eine Zeile ist. Das ist übersichtlicher. Lies Dir am besten mal den Style Guide for Python Code durch.

So wie Du `dict.get()` verwendest, muss man das ja zweimal verwenden wenn es den Schlüssel gibt. Davon abgesehen wäre für den Test ein ``in`` üblicher und auch kürzer und leichter zu lesen. Aber im Tutorial solltest Du noch eine andere Art für den Zugriff gefunden haben, bei dem man sich dann entsprechend um einen `KeyError` kümmern müsste.

Der Quelltext könnte Funktionen vertragen. Statt das alles auf Modulebene runter zu schreiben sollte man die verschiedenen, in sich abgeschlossenen Teilaufgaben in Funktionen aufteilen.

Zur CSV-Datei: Eine Funktion die diese Datei in ein `set()` mit den IMEI-Nummern verarbeitet, wäre der nächste Schritt. Beziehungsweise ein Schritt vor der Verarbeitung der XML-Datei, denn dann hat man diese Daten schon wenn man die Geräte-Einträge aus dem XML-Dokument verarbeitet.

Edit: Ungetestet:

Code: Alles auswählen

import csv
import logging
from collections import namedtuple
from xml.etree import ElementTree as etree


Device = namedtuple('Device', 'uuid name imei')


def load_imeis(filename):
    with open(filename, 'rb') as csv_file:
        reader = csv.reader(csv_file, delimiter=';')
        headers = next(reader)
        assert headers[1] == 'IMEI'
        return set(row[1] for row in reader)


def iter_devices(filename):
    with open(filename, 'rb') as xml_file:
        root = etree.parse(xml_file).getroot()
        for device_node in root.findall('devices/device'):
            uuid = device_node.findtext('uuid')
            name = device_node.findtext('userDisplayName')
            for entry in device_node.findall('details/entry'):
                if entry.findtext('key') in ['ImeiOrMeid', 'imei']:
                    imei = entry.findtext('value')
                    break
            else:
                logging.warn('No IMEI found for %s (%s).', name, uuid)
                imei = None
            yield Device(uuid, name, imei)


def main():
    imeis = load_imeis('imei.csv')
    for device in iter_devices('devices.xml'):
        print device.name, device.imei in imeis


if __name__ == '__main__':
    main()
puemer
User
Beiträge: 8
Registriert: Dienstag 3. Juli 2012, 16:33

@BlackJack

Sorry betreffend dem Formatieren des Quellcodes, ich werde in Zukunft darauf achten.

Hey, Du bist mein Python-Gott :D
Dein Code funktioniert auf Anhieb, super! Unglaublich, dass Du das einfach so auf die Schnelle hinbekommst.

Eine letzte Frage habe ich noch, aber hey, Du hast mir schon viel zu viel geholfen, falls ich nervig sein sollte, ich finde es bestimmt so nach einer Woche :wink: selber irgendwie raus...

In der csv-Datei sind die IMEI-Nummern manchmal mit Bindestrichen formatiert, z.B. 01-304256-705051-0. In der xml-Datei sind sie oft mit Leerzeichen versehen. Darum habe ich mir gesagt, trimme ich einfach alle IMEI-Nummern. Bei Deiner Funktion

Code: Alles auswählen

def iter_devices(filename):
habe ich

Code: Alles auswählen

imei = entry.findtext('value').strip()
geschrieben, das klappt. Jedoch weiss ich noch nicht, wie ich die Bindestriche in den IMEI-Nummern in der csv-Datei wegbekommen soll, da Deine Funktion

Code: Alles auswählen

def load_imeis(filename):
die kommplette Spalte der csv zurückgibt.

Aber wie gesagt, das ist nur noch ein kleiner Schönheitsfehler.

Tausend Dank!
Puemer
BlackJack

@puemer: Das bekommt man so ähnlich wie mit dem `strip()` beim XML hin, man muss nur bei den Einzelwerten die '-'-Zeichen ersetzen:

Code: Alles auswählen

        return set(row[1].replace('-', '') for row in reader)
Wenn mehr gemacht werden soll und/oder das auch bei den IMEIs aus der XML-Datei gemacht werden soll, würde ich eine extra Funktion dafür schreiben und die beispielsweise `clean_imei()` nennen und dann in beiden Fällen verwenden.
Antworten