Ausgezeichnete Abenteuer (Linux-Magazin, Oktober 2012)

Mikroformate zeichnen herkömmliche HTML-Seiten mit allgemein anerkannten Tags wie Geo-Koordinaten oder sozialen Netzwerkverbindungen aus und erlauben so automatischen Auswertern, sie zu sammeln und grafisch ansprechend darzustellen.

Herbstzeit, Wanderzeit! Das warme Wetter Ende September bietet ideale Voraussetzungen, in die USA zu fliegen, einen der 58 Nationalparks aufzusuchen, zu bestaunen und dort Abenteuer zu erleben (Abbildung 1). Die englischsprachige Wikipediaseite "List of national parks of the United States" ([2]) listet die Parks in alphabetischer Reihenfolge auf, beschreibt die Hauptattraktionen und fügt außerdem im HTML-Text die geografische Breite und Länge als Geo-Koordinaten bei.

Abbildung 1: Autor mit Büffel auf freier Wildbahn im Yellowstone-Nationalpark.

Quasi-Standard Mikroformate

Damit nicht nur der geneigte Web-Surfer der Wikipedia-Seite in den Genuss dieser Daten kommt, sondern auch automatisch ablaufende Applikationen, haben sich sogenannte Mikroformate eingebürgert. HTML-Markup kodiert ja traditionell nicht direkt semantische Informationen, sondern konzentriert sich hauptsächlich auf die Darstellung. Steht so zum Beispiel auf einer Webseite die Adresse einer Firma, sind Name, Straße, Ort und Telefonnummer oft nur durch Zeilenumbrüche getrennt und Suchmaschinen müssen erraten, ob es sich überhaupt um eine Adresse handelt und anschließend die einzelnen Bestandteile herausfieseln.

Das in Abbildung 2 gezeigte HTML-Snippet des Tabelleneintrags für den Nationalpark "Bryce Canyon" auf der Wikipedia-Seite definiert ein span-Tag der Klasse vcard, einer sogenannten Root-Klasse, die eine Instanz des Mikroformats hCard ausweist ([3]).

Abbildung 2: Der HTML-Code für den Bryce-Canyon auf Wikipedia enthält auch die geografische Länge und Breite des Standorts.

Weiter unten, tief verschachtelt innerhalb der vcard-Struktur, findet sich eine Klasse "fn org", die den Namen ("name, formatted/full") der beschriebenen Firma oder Organisation angibt. Im Fall des Bryce Canyon ist dies der Name des Nationalparks. Vier Zeilen weiter oben steht die "geo"-Klasse, die mit 37.57 und -112.18 die geografische Breite und Länge eines zentralen Punkts im Park angibt.

Modischer Scraper

Diese Angaben mit einem HTML-Parser zu extrahieren ist nicht schwer und so nimmt es nicht Wunder, dass auf dem Web bereits Applikationen wie microform.at bereit stehen, die URLs engegennehmen, die entsprechenden Seiten einholen, die semantischen Tags extrahieren und als XML zurückliefen. Den angezeigten URL auf das XML-Ergebnis wiederum kann der geneigte User wie in [4] beschrieben dann in das Suchfeld auf Google Maps plumpsen lassen. Dort sieht er dann eine lange Textliste mit den Namen der Nationalparks, und klickt er auf einen Eintrag, erscheint auf der rechts daneben angezeigten Amerikakarte eine Sprechblase an den Geo-Koordinaten des entsprechenden Parks. Allerdings versieht Google wegen den von microform.at gewählten Style-Definitionen die Landkarte nicht mit digitalen Reißnägeln aller Parks. Möchte ein Tourist aber zum Beispiel möglichst viele Parks in einem bestimmten Bundesstaat abklappern, wäre diese Information hilfreich.

Abbildung 3: Die Applikation auf http://microform.at nimmt den URL der Wikipediaseite entgegen, extrahiert die Microformat-Informationen und formatiert sie als XML.

Zum Glück ist das Auslesen der vcard-Informationen mit einem in Perl geschriebenen Web-Scraper nicht weiter schwierig. Wie immer folgt der Perl-Snapshot den neuesten Modetrends und wählt zur Ausführung den modernen Scraper Web::Scraper vom CPAN. Der vom Tool Scrapi aus der Ruby-Welt inspirierte Scraper nutzt eine Art Domain Specific Language (DSL), um in die Tiefen der HTML-Tags vorzudringen und deren Inhalt schön formatiert in Perl-Strukturen zurück zu liefern.

Listing 1: parks-scraper

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use Web::Scraper;
    04 use URI;
    05 use Template;
    06 use CGI qw( :all );
    07 
    08 print header( 
    09  -type => 
    10     'application/vnd.google-earth.kml+xml',
    11  -content_location => 'microformat.kml',
    12  -access_control_allow_origin => '*',
    13  -expires                     => '-1d',
    14 );
    15 
    16 my $url = "http://en.wikipedia.org/wiki/" .
    17           "Us_national_parks";
    18 
    19 my $coords = scraper {
    20   process 'span.vcard', 'vcards[]' => 
    21     scraper {
    22         process '.geo',  geo => 'TEXT';
    23         process '.org', name => 'TEXT';
    24     }
    25 };
    26 
    27 my $res = $coords->scrape( 
    28     URI->new( $url ) );
    29 
    30 my @parks = ();
    31 
    32 for my $vcard ( @{ $res->{ vcards } } ) {
    33   next unless exists $vcard->{ geo };
    34   my( $lon, $lat ) = 
    35       split /\s*;\s*/, $vcard->{ geo };
    36   push @parks, { name => $vcard->{ name },
    37                  lat  => $lat,
    38                  lon  => $lon };
    39 }
    40 
    41 my $tmpl = Template->new();
    42 my $data = join( '', <DATA> );
    43 
    44 binmode STDOUT, ":utf8";
    45 
    46 $tmpl->process( \$data, { parks => 
    47         \@parks } ) || die $tmpl->error();
    48 
    49 __DATA__
    50 <?xml version="1.0" encoding="UTF-8"?>
    51 <kml 
    52  xmlns="http://earth.google.com/kml/2.0">
    53   <Folder>
    54     <name>US National Parks</name>
    55     [% FOR park IN parks %]
    56     <Placemark>
    57       <name>[% park.name %]</name>
    58       <Point>
    59         <coordinates>
    60         [% park.lat %], [% park.lon %], 0
    61         </coordinates>
    62       </Point>
    63     </Placemark>
    64     [% END %]
    65   </Folder>
    66 </kml>

Das CGI-Skript in Listing 1 setzt in Zeile 16 den URL der einzuholenden Wikipedia-Webseite mit den Nationalparks. Der in Zeile 19 definierte Scraper sucht mit dem Funktionsaufruf process zunächst nach allen <span>-Tags mit einem Attribut class="vcard" und legt sie wegen des Parameters vcard[] in einem Array innerhalb des Ergebnis-Hashes ab.

Daten herausfieseln

Wird der Scraper fündig, definert der erneute, verschachtelte Aufruf der scraper-Funktion einen weiteren Scraper in den Tiefen der vcard-Struktur. Dieser sucht wegen des Aufrufs process ".geo" nach Tags mit dem Attribut class="geo", extrahiert wegen des 'TEXT'-Parameters deren Textinhalt (also zum Beispiel "37.57; -112.18") und legt ihn im Arrayeintrag des Nationalparks unter dem Schlüssel geo ab.

Ähnlich verfährt der Scraper mit den Microformat-Einträgen im Format fn org, die den Namen des aktuell bearbeiteten Nationalparks enthalten. Mittels der Anweisung ".org" findet process die Tags, und fieselt aufgrund des TEXT-Parameters den als Klartext im Tag stehenden Namen des Parks heraus und legt ihn unter "name" in der Ergebnisstruktur ab.

Die for-Schleife ab Zeile 32 braucht dann nur noch alle abgelegten vcard-Einträge durchzurattern, und zu prüfen, ob sich dort eine geo-Klasse findet. Falls ja, splittet Zeile 35 die Geodaten-Information am trennenden Semikolon in die geografische Länge und Breite und legt sie in den Variablen $lon und $lat ab. Zusammen mit dem Namen des Parks unter dem Schlüssel name hängt Zeile 36 die extrahierten Werte als weiteren Eintrag im Array @parks an.

XML mit Template

Zur Formatierung der Daten in XML nutzt Listing 1 den Template-Engine Template vom CPAN und das ab der __DATA__-Anweisung folgende XML-Rohgerüst am Ende des Scripts. Dieses erhält in Zeile 46 mit dem Methodenaufruf process() den Array mit den Parkdaten unter dem Schlüssel "parks" in einem Hash. In Zeile 55 definiert das Template mit [% FOR ... %] eine for-Schleife bis zur Markierung [% END %], iteriert über die Daten aller Parks und gibt für jeden Name und Geo-Koordinaten in einer XML-Struktur namens Placemark aus. Während der Name eines Parks in <name>-Tags eingeschlossen ist, finden sich die Koordinaten in einer <Point<gt>-Struktur, die ihrerseits wiederum Tags mit der Bezeichnung <coordinates<gt> und den Geodaten enthalten. Die Koordinaten liegen dort als vorzeichenbehaftete geografische Breite, Länge und Höhe über dem Meeresspiegel vor. Letzerer Wert steht nicht auf der Wikipedia-Seite, und deshalb setzt ihn das Skript auf 0, was bei der später verwendeten zweidimensionalen Google-Maps-Darstellung nicht weiter auffällt.

Alle Placemarks finden in einem Container mit dem Namen Folder Platz ab. Zeile 52 zeigt an, dass es sich um XML-Daten im Google-Earth-Format handelt, die Google Maps später klaglos akzeptieren und anzeigen wird. Wichtig ist noch der binmode-Aufruf in Zeile 44, der die Standardausgabe des Skripts auf utf8 umstellt. Schließlich liegen die Werte der in das Template einzufügenden Variablen utf8-kodiert vor und das ausgespuckte XML sollte ebenfalls als utf8 daher kommen, denn die erste Zeile im XML-Template im __DATA__-Bereich legt als encoding "UTF-8" fest.

Da es sich bei Listing 1 um ein später auf einem Web-Server aufgerufenes CGI-Skript handelt, gibt es vor dem Ergebnis-XML noch einige HTTP-Header aus. Abbildung 4 zeigt die Ausgabe des Skripts, wenn man parks-scraper von der Kommandozeile aus startet. Als Typ legt Zeile 9 im Skript das Google-Earth-Format fest, gefolgt vom Header Content-location mit dem Namen einer Pseudo-Datei, den Google Maps anscheinend benötigt. Zur späteren Nutzung auf Google Maps ist es außerdem wichtig, Access-control-allow-origin auf '*' zu setzen, damit der Browser nicht die Kommunikation zwischen XML-Lieferant und Google-Maps wegen der Same-Origin-Policy unterbindet. Der Expire-Header steht mit -1d auf "gestern", sorgt also dafür, dass Google Maps später jedes Mal frische Ergebnisse vom Geodaten-Lieferant abholt, statt sie zu cachen. Dies ist insbesondere in der Debugging-Phase des Skripts wichtig.

Abbildung 4: Das CGI-Skript extrahiert die Geodaten aus der Wikipedia-Seite und spuckt sie als XML aus.

Einpflanzen in Google Maps

Läuft das Skript dann endlich auf einem öffentlich zugänglichen Webserver (oder gerne auch auf dem letztens vorgestellten Service auf Heroku.com), pflanzt der geneigte Wanderfreund den URL einfach ins Suchfeld von Google Maps ein (Abbidlung 5) und erhält auf der Landkarte für jeden Park einen digitalen Reißnagel.

Abbildung 5: Der Park-Scraper beliefert Google Maps mit Koordinaten für die Nationalparks.

Hinter den Kulissen springt Google Maps das auf perlmeister.com hinterlegte CGI-Skript an, welches wiederum die Wikipedia-Seite mit den Parks einholt, den Scraper anwirft, die Geodaten extrahiert und als XML zurück gibt. Google Maps wiederum analysiert das XML-Format, holt die Geodaten hervor, ermittelt die digitalen Positionen der Reißnägel auf der Landkarte und stellt sie dar.

Der Wanderfreund könnte den Daten nun noch mit künstlicher Intelligenz zu Leibe rücken ([5]) und zum Beispiel herausfinden, in welchen Gegenden der USA die meisten Nationalparks liegen. Oder vielleicht lassen sich ja mit fünf Flügen 70% aller Nationalparks abdecken, wenn man den Aktionsradius richtig wählt und die Parks intelligent in Cluster zusammenfasst? Aber das sparen wir uns für einen zukünftigen Snapshot auf.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2012/10/Perl

[2]

Wikipedia-Eintrag zu den amerikanischen Nationalparks, http://en.wikipedia.org/wiki/Us_national_parks

[3]

Das Mikroformat "hCard 1.0", http://microformats.org/wiki/hcard

[4]

"Mining the Social Web", Matthew A. Russell, 2011, O'Reilly

[5]

"Machine Learning for Hackers", Drew Conway and John Myles White, 2012, O'Reilly

Michael Schilli

arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.