Sonntag, 6. September 2015

Anzahl der Fund, DNFs, Wartungen etc der eigenen Caches auslesen

Statistiken gibt es beim Geocaching viele in jeglicher Ausprägung. Allerdings sind diese fast immer auf (eigene) Funde fokussiert.

Wer als Cacheowner z.B. wissen will, wie viele Funde die eigenen gelegten Caches haben, der schaut erst Mal in die Röhre. Jedenfalls gibt es keine uns bekannte Möglichkeit, dies zu sehen.
Wer "nur" eine handvoll Caches gelegt hat, kann dies natürlich noch relativ einfach und schnell von Hand erledigen. Für Cacheowner mit mehr als ein paar Caches, würde dies händisch viel zu lange dauern, etliche Dutzend Listings von Hand aufzurufen.

Da uns diese Zahlen aber interessieren, wurde - selbst ist der Geocacher - ein Python-Skript (für Python 3) geschrieben, welches die Anzahl der "Found it", "Didn't find it", "Write note", "Need Maintenance", "Owner Maintenance", "Temporarily Disable Listing", "Enable Listing" und "Needs Archived" Logs aus den Cachelistings extrahiert, zusammenrechnet und am Ende am Bildschirm ausgibt.

Die gewünschten Daten sind in jedem Listing bei geocaching.com zu finden und für jeden sichtbar, d.h. man muss nicht auf der Webseite eingelogt sein.

Was das Skript macht ist folgendes:
Es wird zuerst eine Datei cache_list.txt (die im selben Verzeichnis liegen muss die das Skript selbst) eingelesen, in der zeilenweise die GC-Nummer der abzurufenden Caches hinterlegt sind. Die Struktur der Datei ist also

GC2X3F0
GC3M3R8
GC3PGDD
GC3PM4E
...

Für jeden dieser Caches lädt das Skript die Webseite des Listing in den Speicher, parst das HTML der Webseite und extrahiert daraus die Logzahlen.
Während das Skript arbeitet werden die Daten für den jeweiligen Cache auf dem Bildschirm ausgegeben:

...
reading: GC4DRB0
Found it:  166 - Write note:  1 - Owner Maintenance:  1 -
---------------------------------------
reading: GC4EA6J
Found it:  99 - Didn't find it:  1 - Write note:  2 - Owner Maintenance:  2 -
----------------------------------------
...

Nachdem alle Listings abgerufen und geparst wurden, wird das Ergebnis ausgegeben.

Der komplette Python-Code des Skripts sieht so aus:

from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from html.parser import HTMLParser
from datetime import datetime


class MyHTMLParser(HTMLParser):

    def __init__(self):
        HTMLParser.__init__(self)
        self.recording = 0
        self.cache_data = {"Found it": 0,
                           "Didn't find it": 0,
                           "Write note": 0,
                           "Owner Maintenance": 0,
                           "Needs Maintenance": 0,
                           "Temporarily Disable Listing": 0,
                           "Enable Listing": 0,
                           "Needs Archived": 0}
        self.cur_value = None

    def handle_starttag(self, tag, attrs):
        if tag == "img":
            for name, value in attrs:
                if name == "title" and value == "Found it":
                    self.cur_value = "Found it"
                elif name == "title" and value == "Owner Maintenance":
                    self.cur_value = "Owner Maintenance"
                elif name == "title" and value == "Didn't find it":
                    self.cur_value = "Didn't find it"
                elif name == "title" and value == "Write note":
                    self.cur_value = "Write note"
                elif name == "title" and value == "Needs Maintenance":
                    self.cur_value = "Needs Maintenance"
                elif name == "title" and value == "Temporarily Disable Listing":
                    self.cur_value = "Temporarily Disable Listing"
                elif name == "title" and value == "Enable Listing":
                    self.cur_value = "Enable Listing"
                elif name == "title" and value == "Needs Archived":
                    self.cur_value = "Needs Archived"
                self.recording = 1

    def handle_data(self, data):
        if self.recording:
            if self.cur_value == "Owner Maintenance":
                self.cache_data["Owner Maintenance"] = \
                    self.cache_data["Owner Maintenance"] + int(data)
                print("{}: {} - ".format(self.cur_value, data), end="")
            elif self.cur_value == "Needs Maintenance":
                self.cache_data["Needs Maintenance"] = \
                    self.cache_data["Needs Maintenance"] + int(data)
                print("{}: {} - ".format(self.cur_value, data), end="")
            elif self.cur_value == "Found it":
                self.cache_data["Found it"] = \
                    self.cache_data["Found it"] + int(data)
                print("{}: {} - ".format(self.cur_value, data), end="")
            elif self.cur_value == "Didn't find it":
                self.cache_data["Didn't find it"] = \
                    self.cache_data["Didn't find it"] + int(data)
                print("{}: {} - ".format(self.cur_value, data), end="")
            elif self.cur_value == "Write note":
                self.cache_data["Write note"] = \
                    self.cache_data["Write note"] + int(data)
                print("{}: {} - ".format(self.cur_value, data), end="")
            elif self.cur_value == "Needs Archived":
                self.cache_data["Needs Archived"] = \
                    self.cache_data["Needs Archived"] + int(data)
                print("{}: {} - ".format(self.cur_value, data), end="")
            elif self.cur_value == "Temporarily Disable Listing":
                self.cache_data["Temporarily Disable Listing"] = \
                    self.cache_data["Temporarily Disable Listing"] + int(data)
                print("{}: {} - ".format(self.cur_value, data), end="")
            elif self.cur_value == "Enable Listing":
                self.cache_data["Enable Listing"] = \
                    self.cache_data["Enable Listing"] + int(data)
                print("{}: {} - ".format(self.cur_value, data), end="")
            self.cur_value = None
            self.recording = 0

p = MyHTMLParser()
print("Starting at {}".format(datetime.now()))
with open("cache_list.txt", "r") as f:
    for cache in f:
        print("reading: {}".format(cache), end="")
        req = Request("http://coord.info/{}".format(cache))
        try:
            response = urlopen(req)
        except HTTPError as e:
            if e.code == 404:
                print("...is an invalid GC-number!", end="")
            else:
                print("Got HTTPError code {} when trying to fetch data for {}"
                      .format(e.code, cache))
        else:
            html = response.read().decode("utf-8")
            p.feed(html)
        print()
        print("-"*40)
print("Data on caches:")
print("Found it logs: {}".format(p.cache_data["Found it"]))
print("Didn't find it logs: {}".format(p.cache_data["Didn't find it"]))
print("Write note logs: {}".format(p.cache_data["Write note"]))
print("Need Maintenance logs: {}".format(p.cache_data["Needs Maintenance"]))
print("Owner Maintenance logs: {}".format(p.cache_data["Owner Maintenance"]))
print("Needs Archived logs: {}".format(p.cache_data["Needs Archived"]))
print("Temporarily Disable Listing: {}"
      .format(p.cache_data['Temporarily Disable Listing']))
print("Enable Listing logs: {}".format(p.cache_data["Enable Listing"]))
p.close()
print("Finished at {}".format(datetime.now()))

Das Skript arbeitet die Liste der GC-Nummer übrigens "stumpf" ab, d.h. es erfolgt keinerlei Prüfung, ob die GC-Nummer auch wirklich ein eigener Cache ist - von daher sollte man beim Anlegen der Datei mit den GC-Nummern sorgfältig sein.
Sollte die Liste eine ungültige (=nicht existierende) GC-Nummer enthalten, so gibt es Skript einen Hinweis am Bildschirm aus und fährt dann mit dem nächsten Cache fort.

Das Ergebnis sieht dann so aus (heutiger Stand für unsere 89 aktiven Caches):

Data on caches:
Found it logs: 8094
Didn't find it logs: 22
Write note logs: 176
Need Maintenance logs: 16
Owner Maintenance logs: 118
Needs Archived logs: 0
Temporarily Disable Listing: 11
Enable Listing logs: 11

Das ganze dauert übrigens ein bisschen, da die Server von Groundspeak ja nicht so schnell sind (und wir außerdem zu Hause noch eine relative lahme Internetanbindung haben). Für die 89 Cache wurde ca. 11 Minuten benötigt.

Was aber nicht wirklich schlimm ist, weil man diese Statistik ja nicht so häufig benötigt. Außerdem könnte man die Abrufe der Webseiten bei Bedarf noch parallelisieren.

Anzumerken ist auch noch, dass Web Scraping nicht zur "feinen englische Art" gehört - auch, wenn hier "nur" die Daten zu eigenen Caches abgerufen werden.
Wenn also jemand eine bessere / "feinere" Art kennt, an die Daten zu kommen, kann das gerne in den Kommentaren beschrieben werden.

Und auch bei Fragen / Anmerkung zum Skript bitte direkt an uns wenden.

Kommentare:

  1. Habt ihr schonmal daran gedacht, diese unsäglichen Perl Dinger OCprop und Geolog in Python umzuwandeln?? Da kann man dann auch irgendwann eine nette Gui dazu bauen oder in eure Cachekeeper Anwendung integrieren.

    Gruß
    Kaeschkaefer

    AntwortenLöschen
    Antworten
    1. Nee, wir kennen die beiden Tools auch nicht.
      Alles, was wir so programmieren, ist ja auch in erster Linie für uns. Wenn andere es nützen können und wollen ist das gut. Aber wir haben nicht genug Zeit, eine "vollwertige" Applikation für die Öffentlichkeit dauerhaft zu pflegen.

      Löschen