Geocaching-Hilfsprogramm

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
S0S
User
Beiträge: 50
Registriert: Samstag 9. Februar 2013, 18:59

Habe jetzt das so gemacht, wie von noisefloor vorgeschlagen:

Code: Alles auswählen

import os
import re
import glob
import xml.etree.ElementTree as ElementTree

#PATH = r"C:\Users\Susanne\Dateien\Verschiedenes\Geocaching"       # Testpfad 
#PATH = r"D:\Verschiedenes\Python\Geocaching\gpx_test"             # anderer Testpfad
PATH = r"F:\Garmin"                                                # Pfad zu dem Gerät

CODIERUNG = "cp1252"

def xml_anschauen(xml_tree):             
    """zeigt Tags, Attribute und Text eines xml-Baumes an (zum Herausfinden der Tags)"""

    for x in xml_tree.iter():
        if x.text:
            t = x.text.encode("utf-8")
        print x.tag, x.attrib, t
        
def zeichen_ersetzen(string):
    """"ersetzt Zeichen, die Probleme bei der Darstellung machen 
    bisher: u2013 -> Bindestrich"""    
    
    return string.replace(u"\u2013", "-")

class Geocache(object):

    def __init__(self, dateiname_path):
        self._dateiname_path = dateiname_path
        self._gccode = os.path.splitext(os.path.basename(dateiname_path))[0]  # GC-Code
        
        geocache_tree = ElementTree.parse(dateiname_path)    # .gpx-Datei einlesen 
        #xml_anschauen(geocache_tree)
        
        name = geocache_tree.find(".//{http://www.groundspeak.com/cache/1/0}name").text # Name auslesen
        self._name = zeichen_ersetzen(name) # TODO: Warnung bei Zeichen ausgeben, die Probleme machen und nicht ersetzt werden
        
        # TODO: weitere Eigenschaften auslesen und speichern

    def get_gccode(self):
        return self._gccode

    def get_name(self):
        return self._name
        
    def get_dateiname_path(self):
        return self._dateiname_path

    def kurzinfo(self):                                  
        """ gibt eine einzeilige Kurzinfo zurueck"""
        return u"{} : {}".format(self._gccode,self._name)

    def langinfo(self): 
        """gibt eine ausfuehrliche Info zurueck (im Moment noch nicht vorhanden)""" # TODO, wenn andere Eigenschaften da
        return u"{} : {}".format(self._gccode,self._name)


def sortieren(cacheliste, kriterium_zahl, richtung):    
    """sortiert die Cacheliste nach dem angegebenen Kriterium"""
    
    kriterien = ["_gccode", "_name"] # Sortierkriterium festlegen
    try:
        kriterium = kriterien[int(kriterium_zahl)-1]
    except IndexError:
        return cacheliste # bei ungueltiger Eingabe (zu große Zahl) unsortierte Liste zurueckgeben
    except ValueError:
        return cacheliste # bei ungueltiger Eingabe (kein Int) unsortierte Liste zurueckgeben

    rev = (richtung == "2")  # Richtung festlegen
    
    return sorted(cacheliste, key = lambda geocache: getattr(geocache, kriterium).lower(), reverse = rev)
           
def alle_anzeigen(caches):  
    """zeigt die Kurzinfo zu allen Caches in der Liste "caches" an"""

    print "\nWonach sollen die Geocaches sortiert werden?"
    print "1: GC-Code"
    print "2: Name"
    eingabe_kriterium = raw_input(">> ")
    print "In welche Richtung sollen die Caches sortiert werden?"
    print "1: aufsteigend"
    print "2: absteigend"
    eingabe_richtung = raw_input(">> ")
    caches = sortieren(caches, eingabe_kriterium, eingabe_richtung)
        
    for c in caches:
        print c.kurzinfo()
    if len(caches) == 0:
        print "keine Geocaches auf dem Geraet"

def einen_anzeigen(caches):  
    """zeigt die ausfuehrliche Info zu einem bestimmten Cache an und fuehst ggf. weitere Aktionen damit durch (z.B. loeschen)"""

    gc = raw_input("Gib den GC-Code ein: ")
    cache = None
    for c in caches:
        if gc == c.get_gccode():
            cache = c
            break
            
    if not cache:
        print "dieser GC-Code existiert nicht"
    else:
        print cache.langinfo()

        print "\nWas moechstest du als naechstes tun?"
        print "1: diesen Cache loeschen"
        print "2: zurueck"
        eingabe = raw_input(">> ")

        if eingabe == "1":
            caches = loeschen([cache], caches, "suche", False, False)[0] # False nur Dummy fuer warning und found_exists
        elif eingabe == "2":
            pass
    return caches # Rueckgabe notwendig, falls Cache geloescht wurde

def loeschen(del_liste, cacheliste, quelle, warning, found_exists):                       
    """loescht den oder die Caches in del_liste aus cacheliste 
    -> gibt cacheliste ohne geloeschte Caches zurueck sowie Information, 
    ob es nach dem Loeschen noch immer als gefunden markierte Caches auf dem Geraet gibt"""
    
    if quelle == "found":
        print "Achtung! Du solltest die Caches vor dem Loeschen auf geocaching.com loggen."
        if warning == True:   
            print "Warnung! Bei Fortfahren werden auch Log-Informationen ueber Caches geloescht, die nicht gefunden wurden."
    eingabe = raw_input("\nWillst du den / die ausgewaehlten Cache(s) wirklich loeschen? (y/n) ")
    if eingabe == "y":
        for d in del_liste:
            os.remove(d.get_dateiname_path())
        cacheliste = [c for c in cacheliste if c not in del_liste]
        if quelle == "found":
            found_exists = False
            os.remove(os.path.join(PATH,"geocache_visits.txt"))
            os.remove(os.path.join(PATH,"geocache_logs.xml"))

    return [cacheliste, found_exists]

def suchen(caches):                    
    """durchsucht Liste "caches" nach bestimmter Eigenschaft -> suchergebnisse
    ermoeglich weitere Aktionen mit den gefundenen Caches"""

    suchergebnisse = [] 
    print "\nWonach willst du suchen?"
    print "1: Name"
    # TODO: hier noch weitere Suchmoeglichkeiten
    eingabe = raw_input(">> ")

    if eingabe == "1":                      # Suche im Cachenamen
        suchbegriff = raw_input("Suche nach... ").decode(CODIERUNG)
        for c in caches:
            if suchbegriff in c.get_name():
                print c.kurzinfo()
                suchergebnisse.append(c)
    # TODO: andere Suchmoeglichkeiten
    else:
        return caches        # Ende der Funktion bei ungueltiger Eingabe

    if len(suchergebnisse) == 0:
        print "keine Geocaches gefunden"

    else:   # Aktionen mit den Auswahl von Caches
        caches = aktionen_cacheauswahl(suchergebnisse, caches, "suche", False, False) # False nur Dummy fuer warning und found_exists 
    return caches   # Rueckgabe notwendig, falls Cache(s) geloescht wurden
    
def aktionen_cacheauswahl(auswahl, caches, quelle, warning, found_exists):
    """Untermenue für eine Auswahl an Caches (aus Suche oder den gefundenen Caches)"""
    
    while True:
        print "\nWas moechtest du als naechstes tun?"
        if quelle == "suche":
            print "1: Alle Suchergebnisse erneut anzeigen (bei evtl. Loeschen nicht aktualisiert)"
            print "2: Alle Suchergebnisse loeschen"
            print "3: Beschreibung fuer eines der Suchergebnisse anzeigen"
            print "4: zurueck"
        elif quelle == "found":
            print "1: Alle gefundenen Caches erneut anzeigen (bei evtl. Loeschen nicht aktualisiert)"
            print "2: Alle gefundenen Caches loeschen"
            print "3: zurueck"
        eingabe = raw_input(">> ")

        if eingabe == "1":
            for f in auswahl:
                print f.kurzinfo()
        elif eingabe == "2":
            [caches, found_exists] = loeschen(auswahl, caches, quelle, warning, found_exists)
        elif eingabe == "3" and quelle == "suche":
            caches = einen_anzeigen(caches)
        elif eingabe == "3" and quelle == "found":
            return [caches, found_exists]
        elif eingabe == "4" and quelle == "suche":
            return caches
    
def gefundene_anzeigen(found_caches, caches, warning, found_exists):
    for fc in found_caches:
        print fc.kurzinfo()
    [caches, found_exists] = aktionen_cacheauswahl(found_caches, caches, "found", warning, found_exists)   
    return [caches, found_exists]   # Rueckgabe noetig, falls Caches geloescht wurden    
    

def get_logged_and_found_caches(visits_file):
    """liest aus visits_file die geloggten und gefundenen Caches aus"""

    logged_caches_raw = []           
    with open(visits_file) as visits:
        for linenumber,line in enumerate(visits):
            line_new = ""
            for buchstabe in line:
                if buchstabe.encode("hex") != "00":
                    line_new = line_new + buchstabe
            if linenumber == 0 and line_new[0].encode("hex") == "ff" and line_new[1].encode("hex") == "fe":
                line_new = line_new[2:]     # skip UTF-16 byte order mark
            logged_caches_raw.append(line_new)
           
    logged_caches = []           
    for lcr in logged_caches_raw:
        lcr = lcr.split(",")
        lcr.remove(lcr[-1])
        if len(lcr) > 0:
            logged_caches.append(lcr)
            
    found_caches = []
    for lc in logged_caches:
        if lc[-1] == "Found it":
            try:
                found_caches.append(Geocache(os.path.join(PATH,"GPX",lc[0]+".gpx")))
            except IOError:
                print "\nWARNUNG! Der Geocache {} befindet sich nicht auf dem Geraet. Er wird daher im Folgenden nicht mehr beruecksichtigt.".format(lc[0])
            
    return [logged_caches, found_caches]
    
def main():
    geocaches = []               # alle Caches aus GC*.gpx-Dateien in PATH\GPX auslesen und in Liste geocaches speichern
    GPX_PATH = os.path.join(PATH, "GPX")
    for datei in glob.glob(os.path.join(GPX_PATH,"GC*.gpx")):
        geocaches.append(Geocache(datei))
        
    found_exists = False         # alle Caches aus Logdatei in logged_caches und alle gefundenen in found_caches speichern
    warning = False
    if os.path.isfile(os.path.join(PATH, "geocache_visits.txt")):
        [logged_caches, found_caches] = get_logged_and_found_caches(os.path.join(PATH, "geocache_visits.txt"))
        if len(found_caches) > 0:
            found_exists = True
        if len(found_caches) < len(logged_caches):
            warning = True

    while True:                  # Hauptmenue
        print "\nWas moechtest du als naechstes tun?"
        print "1: Alle auf dem Geraet gespeicherten Geocaches sortieren und anzeigen"
        print "2: Beschreibung fuer einen bestimmten Cache anzeigen (GC-Code erforderlich)"
        print "3: Geocaches durchsuchen"
        if found_exists:
            print "4: Alle gefundenen Caches anzeigen"
            print "5: Programm verlassen"
        else:
            print "4: Programm verlassen"
        eingabe = raw_input(">> ")

        if eingabe == "1":
            alle_anzeigen(geocaches)
        elif eingabe == "2":
            geocaches = einen_anzeigen(geocaches)
        elif eingabe == "3":
            geocaches = suchen(geocaches)
        elif eingabe == "4" and found_exists:
            [geocaches, found_exists] = gefundene_anzeigen(found_caches, geocaches, warning, found_exists)
        elif eingabe == "4" and not found_exists:
            break
        elif eingabe == "5" and found_exists:
            break

def test():
    #Geocache("D:\Verschiedenes\Python\Geocaching\gpx_test\GC5FEV4.gpx")   # einen Beispielcache erstellen    
    Geocache("C:\Users\Susanne\Dateien\Verschiedenes\Geocaching\gpx_test\GC5FEV4.gpx")   # einen Beispielcache erstellen  
           
if __name__ == "__main__":
    main()
    #test()
    

BlackJack

@SOS: `xml_anschauen()` ist fehlerhaft. `t` wird nicht immer zugewiesen was zu einer Ausnahme führt wenn das erste `x` kein ”wahres” `text`-Attribut besitzt und andernfalls den falschen Text für `x`-Werte ohne ein solches Attribut liefert.

`Geocache` hat immer noch die trivialen Getter-Methoden. Und trotzdem wird von ausserhalb (sortieren()) auf die Attribute mit dem Unterstrich zugegriffen.

Bei `kurzinfo()` könnte man überlegen ob das ein Kandidat für `__str__()` wäre.

`sortieren()` bekommt komische Argumente. Warum nicht einfach den Namen des Attributs nach dem sortiert werden soll und `True` oder `False` für die Richtung? Insbesondere das die Richtung als Zeichenkette übergeben wird und ausgerechnet '2' für absteigend sortieren steht, sieht total willkürlich aus.

Ab `loeschen()` bekomme ich massive Probleme einzelne Funktionen zu verstehen. Was Du da an Argumenten übergibst ist total unübersichtlich und macht den Eindruck als hättest Du Entscheidungen mit Hilfe von mehreren Flags über verschiedene Funktionen verteilt, die dadurch nicht mehr *eine* Sache machen, sondern Teilaufgaben die da nicht hinein gehören. Mögliche Ursache ist an der Stelle wahrscheinlich, dass Du die Geschäftslogik mit der Benutzerinteraktion vermischst. Eine Funktion zum löschen von Einträgen (auf Ebene der Geschäftslogik) sollte nicht wissen müssen woher die Liste mit den Einträgen kommt und anhand dessen unterschiedliche Ausgaben machen Benutzereingaben erwarten. So eine Funktion sollte einfach nur die Einträge löschen.

`aktionen_cacheauswahl()` gibt mal eine Liste mit zwei Elementen zurück (die eigentlich ein Tupel sein sollte) und mal nur das erste Element alleine. Das ist eine schlechte API. Offenbar hängt es davon ab welche Argumente übergeben wurden und die wiederrum hängen davon ab von wo die Funktion aufgerufen wurde. Das ist alles sehr schwer durchschaubar. Gib einfach *immer* das Tupel zurück. Ein Aufrufer der den zweiten Wert nicht benötigt, ignoriert ihn dann halt einfach. Dann muss man innerhalb der Funktion weniger Fallunterscheidungen machen deren Grundlage ja sowieso eigentlich vom Aufrufer kommen.

Auch auf der anderen Seite, dort wo diese Listen die eigentlich Tupel sein sollten empfangen werden, sollte man das nicht als Liste schreiben. Das ist zwar möglich, aber extrem ungewöhnlich. Ich wette damit kann man nicht nur Python-Anfänger verwirren.

`get_logged_and_found_caches()`: Iiiiiih. Du versuchst da einen UTF-16 kodierten Text ”umzuwandeln” in dem Du die Nullbytes auf eine möglichst umständliche Weise mit Umweg über das Hexkodieren jedes einzelnen Bytes zum Vergleichen entfernst. Mal davon abgesehen dass das wirklich unglaublich umständlich und ineffizient gemacht wird, ist das falsch, weil das Ergebnis Datenmüll wird, sobald nicht tatsächlich jedes zweite Byte ein Nullbyte ist (abgesehen vom BOM am Anfang).

Wenn das UTF-16 ist, dann ist der richtige Umgang damit es einfach zu dekodieren. Beziehungsweise dekodieren zu lassen, denn dafür gibt es natürlich schon etwas.

Code: Alles auswählen

        # Original:

        for linenumber,line in enumerate(visits):
            line_new = ""
            for buchstabe in line:
                if buchstabe.encode("hex") != "00":
                    line_new = line_new + buchstabe
            if linenumber == 0 and line_new[0].encode("hex") == "ff" and line_new[1].encode("hex") == "fe":
                line_new = line_new[2:]     # skip UTF-16 byte order mark
            logged_caches_raw.append(line_new)

        # => Kürzer, effizienter, aber immer noch falsch:

        for linenumber, line in enumerate(visits):
            line = line.replace('\x00', '')
            if linenumber == 0 and line.startswith('\xff\xfe'):
                line = line[2:]
            logged_caches_raw.append(line)

    # => Richtig:

    with io.open('visits_file.txt', encoding='utf16') as lines:
        # und hier dann gleich mit dem Code aus dem zweiten Block weitermachen.
S0S
User
Beiträge: 50
Registriert: Samstag 9. Februar 2013, 18:59

Hallo BlackJack und vielen Dank für deine Anmerkungen.

xml_anschauen() war ja nur dafür, um den Aufbau der xml-Datei zu verstehen, und kein Teil des Programms, weshalb ich diese Funktion jetzt gelöscht habe.

Die trivialen Getter-Methoden und die Unterstriche bei den Attributnamen habe ich ebenfalls entfernt.

Bezüglich dem Rest habe ich mich mal daran versucht, die Benutzerinteraktion von der Programmlogik zu trennen, wie das immer wieder empfohlen wird, allerdings konnte ich mir lange nicht vorstellen, wie das aussehen soll. Ich denke aber, jetzt ist es alles ein bisschen übersichtlicher.

Außerdem habe ich mich etwas in Git bzw. Github eingearbeitet und das Programm dort hochgeladen. Weitere Änderungen werde ich dann auch dort einarbeiten und hier im Forum nur noch fragen, wenn ich irgendwo nicht weiterkomme. Magst du (oder jemand anders) sich das vielleicht noch einmal anschauen, ob man sich die Trennung von Userinteraktion und Programm ungefähr so vorstellen kann, wie ich das gemacht habe? Das ist das Verzeichnis: https://github.com/S0S-90/geoHelper

Vielen Dank euch allen für eure Hilfe!
Antworten