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

Hallo,

ich weiß nicht, ob hier noch jemand dieses Hobby teilt, aber ich gehe sehr gerne und recht regelmäßig geocachen. Seit kurzem habe ich ein neues GPS-Gerät, was wirklich super ist, aber ein Manko hat: Man kann einmal darauf gespeicherte Caches nicht vom Gerät aus löschen. Will man also ein Zumüllen des Geräts vermeiden, muss man die dazugehörigen .gpx-Dateien am PC von Hand aus löschen. Dazu muss man entweder den GC-Code des Caches kennen oder in den Text jeder Datei einzeln schauen, um zu erkennen, um welchen Cache es sich handelt.

Da das etwas kompliziert ist, dachte ich, bastel ich mir ein kleines Programm, das diese Aufgabe vereinfacht. Im Moment kann es die auf dem Gerät gespeicherten Caches anzeigen, durchsuchen und löschen.

Weitere Features sind geplant, zunächst einmal eine Einbeziehung weiterer Informationen wie Schwierigkeit, Terrain-Wertung, Größe, ... in die Suche und eine Möglichkeit, die Geocaches bei der Anzeige nach einer dieser Infos zu ordnen.

Anregungen und Verbesserungsvorschläge wie immer willkommen!

Code: Alles auswählen

import os

#PATH = "C:\Users\Susanne\Dateien\Verschiedenes\Geocaching\gpx_test" # Testpfad (mit Geocaches)
PATH = "F:\Garmin\GPX"                                              # eigentlicher Pfad zu den .gpx-Dateien auf dem Geraet

class Geocache:

    def __init__(self, gccode):
        self.__gccode = gccode
        
        dateiname = "{}\{}.gpx".format(PATH,self.__gccode) # .gpx-Datei einlesen
        with open(dateiname) as gc:
            text = gc.read()
            
        start = text.find("<groundspeak:name>") + 18      # Name aus der Datei auslesen und Speichern
        end = text.find("</groundspeak:name>")
        self.__name = text[start:end]

        #weitere Eigenschaften auslesen und speichern

    def get_gccode(self):
        return self.__gccode

    def get_name(self):
        return self.__name

    def kurzinfo(self):                                  # Kurzinfo in einer Zeile
        print self.__gccode, ":", self.__name

    def longinfo(self):                                  # ausfuehrliche Info (im Moment noch nicht vorhanden)
        print self.__gccode, ":", self.__name

def alle_anzeigen(caches):  # Kurzinfo zu allen Caches in der Liste "caches" anzeigen
    for c in caches:
        c.kurzinfo()
    if len(caches) == 0:
        print "keine Geocaches auf dem Geraet"

def einen_anzeigen(caches):  # ausfuehrliche Info zu einem bestimmten Cache anzeigen und ggf. weitere Aktionen damit durchfuehren (z.B. loeschen)
    gc = raw_input("Gib den GC-Code ein: ")
    cache = "empty"
    for c in caches:
        if gc == c.get_gccode():
            cache = c
            
    if cache == "empty":
        print "dieser GC-Code existiert nicht"
    else:
        cache.longinfo()

        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)
        elif eingabe == "2":
            pass
    return caches

def loeschen(del_liste, cacheliste):                       # den oder die Caches in del_liste aus cacheliste loeschen
    eingabe = raw_input("\nWillst du den / die ausgewaehlten Cache(s) wirklich loeschen? (y/n) ")

    if eingabe == "y":
        for d in del_liste:
            dateiname = "{}\{}.gpx".format(PATH, d.get_gccode())
            os.remove(dateiname)
            for c in cacheliste:
                if d.get_gccode() == c.get_gccode():
                    cacheliste.remove(c)
    return cacheliste

def suchen(caches):                    # Liste "caches" nach bestimmter Eigenschaft durchsuchen -> found_caches
    found_caches = [] 
    print "\nWonach willst du suchen?"
    print "1: Name"
    # hier noch weitere Suchmoeglichkeiten
    eingabe = raw_input(">> ")

    if eingabe == "1":                      # Suche im Cachenamen
        suchbegriff = raw_input("Suche nach... ")
        for c in caches:
            if c.get_name().find(suchbegriff) != -1:
                c.kurzinfo()
                found_caches.append(c)

    # andere Suchmoeglichkeiten

    go = True
    if len(found_caches) == 0:
        print "keine Geocaches gefunden"
        go = False

    while go == True:
        print "\nWas moechtest du als naechstes tun?"
        print "1: alle gefundenen Caches erneut anzeigen (bei evtl. Loeschen nicht aktualisiert)"
        print "2: alle gefundenen Caches loeschen"
        print "3: Beschreibung fuer einen der gefundenen Caches anzeigen"
        print "4: zurueck"
        eingabe = raw_input(">> ")

        if eingabe == "1":
            for f in found_caches:
                f.kurzinfo()
        elif eingabe == "2":
            caches = loeschen(found_caches, caches)
        elif eingabe == "3":
            caches = einen_anzeigen(caches)
        elif eingabe == "4":
            go = False
            
    return caches

def main():
    dateien = os.listdir(PATH)         # alle Caches aus GCxxxxx.gpx-Dateien in PATH auslesen und in Liste geocaches speichern
    geocaches = []
    for d in dateien:
        if len(d) == 11 and d[:2] == "GC" and d[7:]:
            geocaches.append(Geocache(d[:7]))

    go = True
    while go == True:                  # Hauptmenue
        print "\nWas moechtest du als naechstes tun?"
        print "1: alle auf dem Geraet gespeicherten Geocaches anzeigen"
        print "2: Beschreibung fuer einen bestimmten Cache anzeigen (GC-Code erforderlich)"
        print "3: Geocaches durchsuchen"
        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":
            go = False

if __name__ == "__main__":
    main()
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@SOS: ein paar Anmerkungen

Zeile 4: \ sollten immer escaped werden oder der string als r(aw) markiert werden.
Zeile 9/17: Doppelte Unterstriche sind hier falsch (da es keine Mehrfachvererbung gibt)
Zeile 11: Pfade sollten mit os.path.join zusammengesetzt werden
Zeile 12ff: gpx-Dateien sind XML-Dateien. Sie als Text-Dateien zu behandeln ist falsch. Lies sie mit den passenden Bibliotheken (ElementTree).
Zeile 21/24: getter-Methoden sind unnötig. Mach gccode und name zu öffentlichen Attributen
Zeile 27/30: kurzinfo und langinfo sollten Strings zurückgeben und nichts ausgeben. Eventuell ist es auch sinnvoller, statt dessen __str__ oder __format__ zu überschreiben.
Zeile 36: kürzer "if caches:"
Zeile 41: für Nicht-Vorhanden gibt es in Python das Schlüsselwort None. "empty" könnte leicht ein gültiger Wert sein
Zeile 60: was soll der Rückgabewert hier?
Zeile 63: Bei einem deutschen Programm "y" für ja?
Zeiel 67: hier wird zum zweiten Mal ein Dateinname erzeugt. Das heißt, die Information ist wichtig, sollte also im Objekt gespeichert werden.
Zeile 71: aus einer Liste, die gerade durchlaufen wird, darf man nichts rauslöschen.
Zeile 72: entweder Du veränderst die Liste oder gibts eine veränderte Liste zurück, nicht beides! (siehe z.B. sort vs. sorted)
Zeile 84: statt find ist hier "in" richtig.
Zeile 91: siehe Zeile 36
Zeile 95: auf True nicht explizit prüfen. Besser Endlosschleife und mit break verlassen
Zeile 118: d ist ein schlechter Name
Zeile 119: es gibt startswith und endswith. Wenn Du nach bestimmten Dateien filtern willst, ist "glob.glob('GC*.gpx')" besser.
Zeile 120: warum schneidest Du hier die Endung ab, um sie in Goecache.__init__ wieder anzufügen. Besser hier den kompletten Pfad übergeben.
Zeile 123: siehe Zeile 95

Ich komme (ungetestet) bei soetwas raus:

Code: Alles auswählen

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

#PATH = "C:\Users\Susanne\Dateien\Verschiedenes\Geocaching\gpx_test" # Testpfad (mit Geocaches)
PATH = r"F:\Garmin\GPX"                                              # eigentlicher Pfad zu den .gpx-Dateien auf dem Geraet

class Geocache:
    def __init__(self, filename):
        basename = os.path.basename(filename)
        if not (basename.startswith('GC') and basename.endswith('.gpx')):
            raise AssertionError("wrong filename format")
        self.filename = filename
        self.gccode = os.path.splitext(basename)[0]
        self.data = et.ElementTree(file=filename)
        self.name = self.data.findtext(".//{http://www.topografix.com/GPX/1/1}name")

    def get_shortinfo(self):
        return "{}:{}".format(self.gccode, self.name)

    def get_longinfo(self):
        return "{}:{}".format(self.gccode, self.name)

def alle_anzeigen(caches):  # Kurzinfo zu allen Caches in der Liste "caches" anzeigen
    if not caches:
        print "keine Geocaches auf dem Geraet"
    else:
        for c in caches:
            print c.get_shortinfo()

def einen_anzeigen(caches):
    gc = raw_input("Gib den GC-Code ein: ")
    cache = next((gc == c.gccode for c in caches), None)
    if not cache:
        print "dieser GC-Code existiert nicht"
        return []
    else:
        print cache.get_longinfo()
    return [cache]

def loeschen(del_liste, caches):
    eingabe = raw_input("\nWillst du den / die ausgewaehlten Cache(s) wirklich loeschen? (j/n) ")

    if eingabe != "j":
        return caches
    for d in del_liste:
        os.remove(d.filename)
    return [c for c in cacheliste if c not in del_liste]

def suchen(caches):
    print "\nWonach willst du suchen?"
    print "1: Name"
    # hier noch weitere Suchmoeglichkeiten
    eingabe = raw_input(">> ")

    if eingabe == "1":
        suchbegriff = raw_input("Suche nach... ")
        found_caches = [c for c in caches if suchbegriff in c.name]
    else:
        found_caches = [] 

    if not found_caches:
        print "keine Geocaches gefunden"
    else:
        for c in caches:
            print c.get_shortinfo()
    return found_caches

def main():
    geocaches = [Geocache(filename) for filename in glob.glob(os.path.join(PATH, "GC*.gpx")]
    selected = []
    while True:
        print "\nWas moechtest du als naechstes tun?"
        print "1: alle auf dem Geraet gespeicherten Geocaches anzeigen"
        print "2: Beschreibung fuer einen bestimmten Cache anzeigen (GC-Code erforderlich)"
        print "3: Geocaches durchsuchen"
        print "4: Ausgewaehlte Caches loeschen"
        print "5: Programm verlassen"
        eingabe = raw_input(">> ")

        if eingabe == "1":
            alle_anzeigen(geocaches)
        elif eingabe == "2":
            selected = einen_anzeigen(geocaches)
        elif eingabe == "3":
            selected = suchen(geocaches)
        elif eingabe == "4":
            geocaches = loeschen(selected, geocaches)
        elif eingabe == "5":
            break

if __name__ == "__main__":
    main()
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Gerade bei einem etablierten Format sollte man vielleicht auch mal schauen ob man es überhaupt selbst parsen muss. Auf PyPI findet sich zu GPX schon einiges, ein Blick über ein paar dieser Libraries macht da sicherlich Sinn.
S0S
User
Beiträge: 50
Registriert: Samstag 9. Februar 2013, 18:59

Vielen Dank, Sirius, für deine Anmerkungen. Ich habe sie jetzt soweit eingearbeitet.

Ich hätte noch ein paar Fragen zum Auslesen der XML-Dateien:
1.) Warum ist bei dem Tag immer noch eine url dabei und nicht einfach nur "name", wie es in der Originaldatei in den spitzen Klammern steht?
2.) Warum steht vor dem Tag immer noch ".//"? Das war auch in sämlichen Beispielen, die ich im Internet gefunden habe, so, aber es wurde in der Erklärung nie darauf eingegangen.

Kann es denn zu Problemen kommen, wenn man die XML-Dateien, so wie ich es zuvor gemacht hatte, als Textdateien behandelt? Mir ist klar, dass es eleganter ist, sie so auszulesen als über die Indizes der Tags nach denen man sucht, aber du hast geschrieben, letzteres sei "falsch", was für mich impliziert, dass es manchmal (meist?) nicht funktioniert.

Und noch mal danke für die Antwort! Ich habe jedenfalls beim Verbessern eine Menge gelernt :-)

Code: Alles auswählen

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

#PATH = r"C:\Users\Susanne\Dateien\Verschiedenes\Geocaching\gpx_test" # Testpfad (mit Geocaches)
PATH = r"D:\Verschiedenes\Python\Geocaching\gpx_test"                # anderer Testpfad
#PATH = r"F:\Garmin\GPX"                                              # eigentlicher Pfad zu den .gpx-Dateien auf dem Geraet

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

class Geocache(object):

    def __init__(self, dateiname_path):
        self._dateiname_path = dateiname_path
        self._gccode = os.path.basename(dateiname_path)[:-4]  # GC-Code
        
        geocache_tree = ElementTree.parse(dateiname_path)    # .gpx-Datei einlesen 
        #xml_anschauen(geocache_tree)
        
        self._name = geocache_tree.find(".//{http://www.groundspeak.com/cache/1/0}name").text.encode("utf-8") # Name auslesen
        #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):                                  # Kurzinfo in einer Zeile
        return "{} : {}".format(self._gccode,self._name)

    def longinfo(self):                                  # ausfuehrliche Info (im Moment noch nicht vorhanden)
        return "{} : {}".format(self._gccode,self._name)


def alle_anzeigen(caches):  # Kurzinfo zu allen Caches in der Liste "caches" anzeigen
    for c in caches:
        print c.kurzinfo()
    if len(caches) == 0:
        print "keine Geocaches auf dem Geraet"

def einen_anzeigen(caches):  # ausfuehrliche Info zu einem bestimmten Cache anzeigen und ggf. weitere Aktionen damit durchfuehren (z.B. loeschen)
    gc = raw_input("Gib den GC-Code ein: ")
    cache = None
    for c in caches:
        if gc == c.get_gccode():
            cache = c
            
    if not cache:
        print "dieser GC-Code existiert nicht"
    else:
        print cache.longinfo()

        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)
        elif eingabe == "2":
            pass
    return caches # Rueckgabe notwendig, falls Cache geloescht wurde

def loeschen(del_liste, cacheliste):                       # den oder die Caches in del_liste aus cacheliste loeschen
    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())
            for c in cacheliste:
                if d.get_gccode() == c.get_gccode():
                    cacheliste.remove(c)
                    break
    return cacheliste

def suchen(caches):                    # Liste "caches" nach bestimmter Eigenschaft durchsuchen -> found_caches
    found_caches = [] 
    print "\nWonach willst du suchen?"
    print "1: Name"
    # hier noch weitere Suchmoeglichkeiten
    eingabe = raw_input(">> ")

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

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

    else:
        while True:
            print "\nWas moechtest du als naechstes tun?"
            print "1: alle gefundenen Caches erneut anzeigen (bei evtl. Loeschen nicht aktualisiert)"
            print "2: alle gefundenen Caches loeschen"
            print "3: Beschreibung fuer einen der gefundenen Caches anzeigen"
            print "4: zurueck"
            eingabe = raw_input(">> ")

            if eingabe == "1":
                for f in found_caches:
                    print f.kurzinfo()
            elif eingabe == "2":
                caches = loeschen(found_caches, caches)
            elif eingabe == "3":
                caches = einen_anzeigen(caches)
            elif eingabe == "4":
                break
    return caches   # Rueckgabe notwendig, falls Cache(s) geloescht wurden

def main():
    geocaches = []               # alle Caches aus GC*.gpx-Dateien in PATH auslesen und in Liste geocaches speichern
    for datei in glob.glob(os.path.join(PATH,"GC*.gpx")):
        geocaches.append(Geocache(datei))
        
    while True:                  # Hauptmenue
        print "\nWas moechtest du als naechstes tun?"
        print "1: alle auf dem Geraet gespeicherten Geocaches anzeigen"
        print "2: Beschreibung fuer einen bestimmten Cache anzeigen (GC-Code erforderlich)"
        print "3: Geocaches durchsuchen"
        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":
            break

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

BlackJack

@SOS: Ad 1.) Weil GPX einen XML-Namensraum hat und der auch in der Datei angegeben wird und man den deswegen auch angeben muss. Denn es könnte ja sein, dass man GPX in einem anderen Format einbettet oder andere Daten mit einem anderen Namensraum mit in dem GPX-Dokument unterbringt und wenn es dort auch ein <name>-Tag gibt, könnte man die nicht auseinanderhalten.

Ad 2.) Das ist XPath-Syntax für „von diesem Knoten aus ('.') alle die darunter liegen ('//')“ und nicht nur von dem aktuellen Knoten die direkten Kindknoten. Wenn man nicht die Kurzschreibweise verwendet (und `lxml` statt `xml.etree`) könnte man es auch in lang schreiben: 'self::node()/descendant-or-self::node()/{http…}name'.

XML-Namensräume sind zum Beispiel ein Problem wenn man die Datei einfach als Text verarbeitet, denn der Namensraum von dem <name>-Element wird weiter oben im Elementbaum angegeben und 'groundspeak' ist eine Variable. Das muss nicht so heissen, das könnte auch 'punkrock' heissen und wäre immer noch ein gültiges XML-Dokument mit dem gleichen Inhalt. Ein anderes Problem ist, dass dasselbe XML-Dokument sehr verschieden als XML-Datei serialisiert werden kann. Alles in einer Zeile, oder Text in unterschiedlichen Formaten und Kodierungen. Das bekommt man mit einfachen Textoperationen nicht robust verarbeitet.
S0S
User
Beiträge: 50
Registriert: Samstag 9. Februar 2013, 18:59

Danke schön.
Ca^^el
User
Beiträge: 1
Registriert: Sonntag 21. August 2016, 21:06

Hallo liebe Cacher und Python-fans,

erst mal danke für diesen Post. Ich habe dein Programm nicht getestet und wollte diesen Thead nur nutzen um mein Script zu teilen.
Ich war einige Zeit auf der Suche nach einer Möglichkeit, die Caches mit vielen Favorite-Punkten in einer gpx-Datei für mein Garmin zu speichern.
Dann habe ich die Seite http://project-gc.com gefunden. Leider gibt es auch dort keine export-Möglichkeit. Darum habe ich selbst ein kleines Skript geschieben. Du must einfach deine Pocket Query runterladen und die entsprechenden Variablen anpassen und schon sucht er nach den Cachen mit den höchten Log-Favorit-Verhältnis und löscht alle schlechteren aus der gpx-Datei (bzw. legt eine neue Datei an)-

Viel Spaß damit. Hier ist der Code, an dem ich heute gut 5 Stunden saß:

Code: Alles auswählen

#/usr/bin/python3

# sudo apt-get install python3-pip
# sudo pip3 install pycaching
# sudo pip3 install bs4
# sudo pip3 install lxml

import pycaching
from pycaching import Point
from pycaching.cache import Type
from bs4 import BeautifulSoup
from math import sqrt


user = "Ca^^el"
passwd = "meinPasswort"
maxCaches = 2000
minWilson = 0.3
geocode = "Berlin"
inputfilename = 'pq.gpx'
outputfilename = 'pq-best.gpx'

def getTotalLogs(wp):
	root = geocaching._request("seek/cache_details.aspx", params={"wp": wp})
	totalLogs = root.find('div', class_='InformationWidget Clear').find('h3').text.split()[0]
	totalLogs = int(totalLogs.replace(',',''))
	return totalLogs


# Source1: http://stackoverflow.com/questions/1002 ... e-interval
# Source2: https://medium.com/hacking-and-gonzo/ho ... .ria6ecei9
def calculateWilson(ups, downs):
    n = ups + downs
    if n == 0:
        return 0

    z = 1.281551565545 #1.44 = 85%, 1.96 = 95%
    phat = float(ups) / n
    return ((phat + z*z/(2*n) - z * sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n))


def searchForTheBestCaches(maxCaches, minWilson):
	print("Browsing " + str(maxCaches) + " Caches lokkinh for a Wilson-score greater than " + str(minWilson * 100) + "%. This will take some time...")

	cachesNumber = 0
	favcodes = []
	total = 0
	positives = 0
	negative = 0

	for cache in geocaching.search(point, limit=maxCaches):
		cachesNumber += 1
		if cache.type == Type.traditional or cache.type == Type.multicache and cache.state :
			total = getTotalLogs(cache.wp)
			positives = cache.favorites
			negatives = total - positives
			wilson = calculateWilson(positives, negatives)
			if wilson > minWilson:
				print('#' + str(cachesNumber) + ' ' + str(cache.wp) + ' - ' + cache.name + ' - Favoritpoints: ' + str(positives) + ' - Total Logs:' + str(total) + ' - Wilson-score: ' + str(wilson))
				favcodes.append(str(cache.wp))
	return favcodes

def parsePocketQuery(inFile, outFile, favcodes):
	tree = BeautifulSoup(open(inFile), "lxml")
	bestCaches = []
	for wpt in tree.gpx.findAll('wpt'):
		if wpt.find('name').text in favcodes:
			bestCaches.append(wpt.find('name').text)
		else:
			wpt.decompose()
	
	print(bestCaches)
	strToSave = str(tree)
	strToSave = strToSave.replace('<html><body>','')
	strToSave = strToSave.replace('</body></html>','')
	
	with open(outFile, "wt", newline='\r\n') as file:
		file.write(strToSave)
	

# ################ START HERE ###################
print("Login in to geocaching.com...")
geocaching = pycaching.login(user, passwd)
# Alternative: 
# geocaching = pycaching.login()  # assume the .gc_credentials file is presented

point = geocaching.geocode(geocode)
# Alternative: 
# point = Point(52.52083, 13.4091) # Berlin beim Telespargel

favcodes = searchForTheBestCaches(maxCaches, minWilson)
# For testing
#favcodes = ['GC3Y23E', 'GCNB2R', 'GC64V4J']
print(favcodes)
geocaching.logout()

print("Parsing pq.gpx file and removing Caches that aren't in the list and saving the result to pq-best.gpx......")
parsePocketQuery(inputfilename, outputfilename, favcodes)

S0S
User
Beiträge: 50
Registriert: Samstag 9. Februar 2013, 18:59

Hallo Ca^^el,

freut mich, dass es noch mehr Geocaching-Interessierte hier gibt. Leider kann ich dein Programm nicht testen, da man für eine Pocket-Query Premium-Mitglied sein muss, was ich nicht bin. Aber das Programm sieht interessant aus, v.a. das Modul pycaching werde ich mir für weitere Versionen meines Programms vielleicht auch mal anschauen.

Ich habe inzwischen mein Programm dahingehend weiterentwickelt, dass man die Caches ordnen kann (bisher nur nach Name und GC-Code) und dass das Programm mit Umlauten umgehen kann. Für letzteres ist es notwendig, in der Windows-Konsole, über die das Skript gestartet wird, die Codierung auf cp1252 umzustellen. Dazu gibt man einfach "chcp 1252" in die Kommandozeile ein und drückt Enter.

Der aktualisierte Code:

Code: Alles auswählen

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

#PATH = r"C:\Users\Susanne\Dateien\Verschiedenes\Geocaching\gpx_test" # Testpfad (mit Geocaches)
#PATH = r"D:\Verschiedenes\Python\Geocaching\gpx_test"                # anderer Testpfad
PATH = r"F:\Garmin\GPX"                                              # eigentlicher Pfad zu den .gpx-Dateien auf dem Geraet

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):
    neu = ""
    i = 0
    while i < len(string):
        if ord(string[i]) == 226 and ord(string[i+1]) == 128 and ord(string[i+2]) == 147:  # ersetze Gedankenstrich u2013 durch normales -
            neu = neu + "-"
            i = i+2
        else:
            neu = neu + string[i]
        i = i+1
    return neu

class Geocache(object):

    def __init__(self, dateiname_path):
        self._dateiname_path = dateiname_path
        self._gccode = os.path.basename(dateiname_path)[:-4]  # 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.encode("utf-8") # Name auslesen
        self._name = zeichen_ersetzen(name).decode("utf-8")
        
        #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):                                  # Kurzinfo in einer Zeile
        return u"{} : {}".format(self._gccode,self._name)

    def longinfo(self):                                  # ausfuehrliche Info (im Moment noch nicht vorhanden)
        return u"{} : {}".format(self._gccode,self._name)


def sortieren(cacheliste, kriterium, richtung):    # sortiert die Cacheliste nach dem angegebenen Kriterium
    if richtung == "2":
        rev = True
    else:
        rev = False
        
    if kriterium == "1":     # GC-Code
        return sorted(cacheliste, key = lambda geocache: geocache.get_gccode().lower(), reverse = rev)
    elif kriterium == "2":   # Name
        return sorted(cacheliste, key = lambda geocache: geocache.get_name().lower(), reverse = rev)
    else:
        return cacheliste    # bei ungueltiger Eingabe: Rueckgabe der unsortierten Liste
           
def alle_anzeigen(caches):  # Kurzinfo zu allen Caches in der Liste "caches" anzeigen
    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):  # ausfuehrliche Info zu einem bestimmten Cache anzeigen und ggf. weitere Aktionen damit durchfuehren (z.B. loeschen)
    gc = raw_input("Gib den GC-Code ein: ")
    cache = None
    for c in caches:
        if gc == c.get_gccode():
            cache = c
            
    if not cache:
        print "dieser GC-Code existiert nicht"
    else:
        print cache.longinfo()

        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)
        elif eingabe == "2":
            pass
    return caches # Rueckgabe notwendig, falls Cache geloescht wurde

def loeschen(del_liste, cacheliste):                       # den oder die Caches in del_liste aus cacheliste loeschen
    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())
            for c in cacheliste:
                if d.get_gccode() == c.get_gccode():
                    cacheliste.remove(c)
                    break
    return cacheliste

def suchen(caches):                    # Liste "caches" nach bestimmter Eigenschaft durchsuchen -> found_caches
    found_caches = [] 
    print "\nWonach willst du suchen?"
    print "1: Name"
    # hier noch weitere Suchmoeglichkeiten
    eingabe = raw_input(">> ")

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

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

    else:
        while True:
            print "\nWas moechtest du als naechstes tun?"
            print "1: alle gefundenen Caches erneut anzeigen (bei evtl. Loeschen nicht aktualisiert)"
            print "2: alle gefundenen Caches loeschen"
            print "3: Beschreibung fuer einen der gefundenen Caches anzeigen"
            print "4: zurueck"
            eingabe = raw_input(">> ")

            if eingabe == "1":
                for f in found_caches:
                    print f.kurzinfo()
            elif eingabe == "2":
                caches = loeschen(found_caches, caches)
            elif eingabe == "3":
                caches = einen_anzeigen(caches)
            elif eingabe == "4":
                break
    return caches   # Rueckgabe notwendig, falls Cache(s) geloescht wurden

def main():
    geocaches = []               # alle Caches aus GC*.gpx-Dateien in PATH auslesen und in Liste geocaches speichern
    for datei in glob.glob(os.path.join(PATH,"GC*.gpx")):
        geocaches.append(Geocache(datei))
        
    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"
        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":
            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()
    
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@SOS: Was machst Du Dir soviel Mühe? Intern solltest Du immer mit Unicode-Strings arbeiten und nicht ständig hin und her konvertieren.

Aus

Code: Alles auswählen

def zeichen_ersetzen(string):
    neu = ""
    i = 0
    while i < len(string):
        if ord(string[i]) == 226 and ord(string[i+1]) == 128 and ord(string[i+2]) == 147:  # ersetze Gedankenstrich u2013 durch normales -
            neu = neu + "-"
            i = i+2
        else:
            neu = neu + string[i]
        i = i+1
    return neu

name = geocache_tree.find(".//{http://www.groundspeak.com/cache/1/0}name").text.encode("utf-8") # Name auslesen
self._name = zeichen_ersetzen(name).decode("utf-8")
wird einfach

Code: Alles auswählen

name = geocache_tree.findtext(".//{http://www.groundspeak.com/cache/1/0}name")
self._name = name.replace(u"\u2013", "-")
Die ganzen get_xxx-Methode kannst Du ersatzlos streichen, da Du auch direkt per geocache.gccode zugreifen kann.
BlackJack

@SOS: Viele Zeilen sind deutlich zu lang. Oft durch Kommentare an Zeilenenden. Die sollte man dann besser über den kommentierten Code schreiben, statt ”daneben”, was sehr unleserlich wird wenn daneben kein Platz mehr ist und das alles umgebrochen wird. Zum Beispiel im Terminal, in E-Mails, hier im Forum, oder wo man das noch überall anzeigen könnte, wo aber selten mehr als die üblichen 80 Zeichen horizontaler Platz vorgesehen ist.

Bei den Kommentaren an Funktionen bietet es sich ausserdem an das nicht als Kommentar, sondern als Docstring zu setzen.

Beim ermitteln des GC-Codes sollte man die magische -4 durch eine Operation ersetzen die besser beschreibt was dort passiert: Entfernen der Dateinamenserweiterung.

`longinfo` und `kurzinfo`? Warum mal englisch und mal deutsch? Und es gibt so einige unschöne, teilweise nur einbuchstabige, Abkürzungen.

Wenn man aufgrund einer Bedingung einer Variablen `True` oder `False` zuweist, dann kann man auch gleich die Bedingung verwenden, denn die ergibt ja schon einen Wahrheitswert. Also:

Code: Alles auswählen

    if richtung == '2':
        reverse = True
    else:
        reverse = False

    # ->

    reverse = richtung == '2'
Benutzerinteraktion und Programmlogik ist zu stark vermischt. Die `sortieren()`-Funktion könnte und sollte man beispielsweise von irgendwelchen kryptischen Ziffern als Zeichenketten befreien, dann kann man die auch ohne die Textoberfläche verwenden. Ausserdem sollte man bei ungültiger Eingabe nicht einfach gar nicht sortieren, sondern eine Ausnahme auslösen.

Bei der linearen suche nach einem Cache sollte man abbrechen wenn man ein Ergebnis gefunden hat. Dann kann man den ”nicht gefunden”-Fall auch leicht mit einem ``else`` zum ``for`` behandeln und mit nur einem Namen für den Cache.

Das lineare Suchen ist zudem zweimal im Code vorhanden, das kann/sollte man in eine Funktion heraus ziehen.

`loeschen()` verändert die Cacheliste was nicht zum ansonsten funktionalen Verhalten der anderen Funktionen passt. Da das mit dem `remove()` sowieso ineffizient ist, weil es den gerade aktuellen cache noch mal vom Anfang der Liste sucht, sollte man das so anpassen wie es in Python üblich ist: löschen aus einer Liste wird durch erstellen einer Liste ohne das entsprechende Element gelöst.

Ungetestetes Zwischenergebnis:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division #, print_function
import glob
import os
from xml.etree import ElementTree
 
PATH = r"F:\Garmin\GPX"  # Pfad zu den .gpx-Dateien auf dem Gerät.
PROMPT = '>> '


def xml_anschauen(tree):
    """Zeige Tags, Attribute und Text eines XML-Baumes an (zum
    Herausfinden der Tags).
    """
    for element in tree.iter():
        if element.text:
            text = element.text.encode('utf-8')
        print element.tag, element.attrib, text

 
class Geocache(object):
 
    def __init__(self, dateiname_path):
        self.dateiname_path = dateiname_path
        self.gc_code = os.path.splitext(os.path.basename(dateiname_path))[0]
       
        tree = ElementTree.parse(dateiname_path)  # .gpx-Datei einlesen
        #xml_anschauen(tree)
        self.name = tree.find(
            './/{http://www.groundspeak.com/cache/1/0}name'
        ).text.replace(u'\u2013', '-')
        # 
        # TODO Weitere Eigenschaften auslesen und speichern.
        # 

    def kurzinfo(self):
        return u'{0.gc_code} : {0.name}'.format(self)
 
    langinfo = kurzinfo
 
 
def sortieren(caches, key_attribute, reverse):
    """Sortiere die Caches nach dem angegebenen Kriterium."""
    return sorted(
        caches,
        key=lambda cache: getattr(key_attribute).lower(),
        reverse=reverse
    )



def alle_anzeigen(caches):
    """Kurzinfo zu allen Caches in der Liste "caches" anzeigen."""
    print '\nWonach sollen die Geocaches sortiert werden?'
    print '1: GC-Code'
    print '2: Name'
    eingabe_kriterium = raw_input(PROMPT)
    print 'In welche Richtung sollen die Caches sortiert werden?'
    print '1: aufsteigend'
    print '2: absteigend'
    eingabe_richtung = raw_input(PROMPT)
    caches = sortieren(
        caches,
        {'1': 'gc_code', '2': 'name'}[eingabe_kriterium],
        eingabe_richtung == '2'
    )
    for cache in caches:
        print cache.kurzinfo()
    if not caches:
        print 'keine Geocaches auf dem Geraet'


def cache_suchen(caches, gc_code):
    for cache in caches:
        if gc_code == cache.gc_code:
            return cache
    raise KeyError('cache {0!r} not found'.format(gc_code))
    

def einen_anzeigen(caches):
    """Ausfuehrliche Info zu einem bestimmten Cache anzeigen und
    ggf. weitere Aktionen damit durchfuehren (z.B. loeschen).
    """
    gc_code = raw_input('Gib den GC-Code ein: ')
    try:
        cache = cache_suchen(caches, gc_code)
    except KeyError:
        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(PROMPT)
 
        if eingabe == '1':
            caches = loeschen(caches, [cache])
    return caches  # Rueckgabe notwendig, falls Cache geloescht wurde.


def loeschen(caches, caches_to_delete):
    """Den oder die Caches in caches_to_delete aus cacheliste loeschen."""
    eingabe = raw_input(
        '\nWillst du den / die ausgewaehlten Cache(s) wirklich loeschen? (y/n) '
    )
    if eingabe == 'y':
        for cache in caches_to_delete:
            os.remove(cache.dateiname_path)

        gc_codes = set(c.gc_code for c in caches_to_delete)
        caches = [c for c in caches if c.gc_code not in gc_codes]

    return caches


def suchen(caches):
    """Liste "caches" nach bestimmter Eigenschaft durchsuchen
    -> found_caches.
    """
    found_caches = list()
    print '\nWonach willst du suchen?'
    print '1: Name'
    # 
    # TODO Hier noch weitere Suchmoeglichkeiten.
    # 
    eingabe = raw_input(PROMPT)
 
    if eingabe == '1':
        # 
        # TODO Kodierung nicht hart kodieren sondern ”erraten” und dem
        #   Nutzer die Möglichkeit geben sie als Option beim Start
        #   anzugeben.
        # 
        suchbegriff = raw_input('Suche nach... ').decode('cp1252')
        for cache in caches:
            if suchbegriff in cache.name:
                print cache.kurzinfo()
                found_caches.append(cache)
    # 
    # TODO Andere Suchmoeglichkeiten.
    # 
    else:
        return caches        # Ende der Funktion bei ungueltiger Eingabe
 
    if not found_caches:
        print 'keine Geocaches gefunden'
    else:
        while True:
            print '\nWas moechtest du als naechstes tun?'
            print '1: alle gefundenen Caches erneut anzeigen (bei evtl.',
            print 'Loeschen nicht aktualisiert)'
            print '2: alle gefundenen Caches loeschen'
            print '3: Beschreibung fuer einen der gefundenen Caches anzeigen'
            print '4: zurueck'
            eingabe = raw_input(PROMPT)
 
            if eingabe == '1':
                for cache in found_caches:
                    print cache.kurzinfo()
            elif eingabe == '2':
                caches = loeschen(caches, found_caches)
            elif eingabe == '3':
                caches = einen_anzeigen(caches)
            elif eingabe == '4':
                break

    return caches  # Rueckgabe notwendig, falls Cache(s) geloescht wurden


def main():
    caches = map(Geocache, glob.glob(os.path.join(PATH, 'GC*.gpx')))
       
    while True:
        print '\nWas moechtest du als naechstes tun?'
        print '1: Alle auf dem Geraet gespeicherten Geocaches sortieren und',
        print 'anzeigen'
        print '2: Beschreibung fuer einen bestimmten Cache anzeigen (GC-Code',
        print 'erforderlich)'
        print '3: Geocaches durchsuchen'
        print '4: Programm verlassen'
        eingabe = raw_input(PROMPT)
 
        if eingabe == '1':
            alle_anzeigen(caches)
        elif eingabe == '2':
            caches = einen_anzeigen(caches)
        elif eingabe == '3':
            caches = suchen(caches)
        elif eingabe == '4':
            break


def test():
    Geocache(
        r'C:\Users\Susanne\Dateien\Verschiedenes\Geocaching\gpx_test'
        r'\GC5FEV4.gpx'
    )

           
if __name__ == '__main__':
    main()
Programmlogik und Benutzerinteraktion sind hier noch recht stark vermischt und der Menücode kann als Daten und Funktionen herausgezogen werden. Da wiederholt sich zu viel Code in den einzelnen Funktionen und besonders robust gegen Fehleingaben ist er auch nicht.
S0S
User
Beiträge: 50
Registriert: Samstag 9. Februar 2013, 18:59

Vielen Dank für eure Anmerkungen!

@Sirius: So kompliziert hatte ich das gemacht, weil ich ewig mit allen möglichen Codierungen und Decodierung herumprobiert habe, bis ich generkt habe, dass im Namen dieses Caches nicht das "ä" das Problem ist, sondern der Bindestrich: https://www.geocaching.com/geocache/GC1 ... rchenstuhl - deshalb habe ich, als ich dann erst mal was Funktionierendes hatte, nicht mehr groß überlegt, ob es auch einfacher ginge. Cool, dass es auch so leicht geht ;-)

@BlackJack: Was meinst du mit der linearen Suche, die mehrfach implementiert ist? Ich nehme an, die Suche nach dem entsprechenden GC-Code, die du in deinem Vorschlag auch in eine eigene Funktion ausgelagert hast. Die wird aber doch nur einmal verwendet bzw. aufgerufen, nämlich in der Funktion einen_anzeigen.

Mein aktueller Code:

Code: Alles auswählen

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

PATH = r"C:\Users\Susanne\Dateien\Verschiedenes\Geocaching\gpx_test" # Testpfad (mit Geocaches)
#PATH = r"D:\Verschiedenes\Python\Geocaching\gpx_test"                # anderer Testpfad
#PATH = r"F:\Garmin\GPX"                                              # eigentlicher Pfad zu den .gpx-Dateien auf dem Geraet

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)
        elif eingabe == "2":
            pass
    return caches # Rueckgabe notwendig, falls Cache geloescht wurde

def loeschen(del_liste, cacheliste):                       
    """loescht den oder die Caches in del_liste aus cacheliste -> gibt cacheliste ohne geloeschte Caches zurueck"""

    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]

    return cacheliste

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

    found_caches = [] 
    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("cp1252")
        for c in caches:
            if suchbegriff in c.get_name():
                print c.kurzinfo()
                found_caches.append(c)
    # TODO: andere Suchmoeglichkeiten
    else:
        return caches        # Ende der Funktion bei ungueltiger Eingabe

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

    else:   # Aktionen mit den gefundenen Caches
        while True:
            print "\nWas moechtest du als naechstes tun?"
            print "1: alle gefundenen Caches erneut anzeigen (bei evtl. Loeschen nicht aktualisiert)"
            print "2: alle gefundenen Caches loeschen"
            print "3: Beschreibung fuer einen der gefundenen Caches anzeigen"
            print "4: zurueck"
            eingabe = raw_input(">> ")

            if eingabe == "1":
                for f in found_caches:
                    print f.kurzinfo()
            elif eingabe == "2":
                caches = loeschen(found_caches, caches)
            elif eingabe == "3":
                caches = einen_anzeigen(caches)
            elif eingabe == "4":
                break
    return caches   # Rueckgabe notwendig, falls Cache(s) geloescht wurden

def main():
    geocaches = []               # alle Caches aus GC*.gpx-Dateien in PATH auslesen und in Liste geocaches speichern
    for datei in glob.glob(os.path.join(PATH,"GC*.gpx")):
        geocaches.append(Geocache(datei))
        
    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"
        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":
            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: Du hattest die lineare Suche noch mal in `loeschen()`, was ich dann anders gelöst habe, weshalb die Funktion nur einmal aufgerufen wird. Die Trennung macht trotzdem Sinn, weil das Suchen eines Eintrags allgemein nützlich ist und nicht in einer Funktion für die Benutzerinteraktion stecken sollte.
Benutzeravatar
noisefloor
User
Beiträge: 3843
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

also ich mach' das immer händisch, die GPX-Dateien auf meinem GPSMAP64s verwalten :-)

Aber wenn ich das gleiche Problem hätte, würde ich es komplett anders angehen:
Wenn man auf dem GPSr einen Cache als "gefunden" loggt, wird die GC-Nummer in eine txt-Datei geschrieben, die auch zugänglich auf dem Gerät liegt. In der Datei liegen AFAIR die GC-Nummer einfach zeilenweise vor, also pro Zeile ein GCxxxxx.

Um besuchte und gefunden Caches zu löschen würde also einfach die txt-Datei lesen, schauen, ob die Datei GCxxxxx.gpx noch da ist und dann löschen.

Gruß, noisefloor
S0S
User
Beiträge: 50
Registriert: Samstag 9. Februar 2013, 18:59

Das wusste ich nicht, dass die gefundenen und gelöschten Caches in eine eigene txt-Datei geschrieben werden. Ich hatte zu Beginn gehofft, das gpx-file selbst würde irgendwie verändert, leider war das aber nicht der Fall. Das muss ich aber gleich mal nachschauen!
S0S
User
Beiträge: 50
Registriert: Samstag 9. Februar 2013, 18:59

Hab's gefunden. Das eröffnet natürlich ganz andere Möglichkeiten!
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