Google Web Service (Linux-Magazin, Oktober 2002)

Wer linkt zu meiner Website? Statt die Webserver-Logdatei durchzuwühlen, kann man auch automatische Anfragen an Google abfeuern und in den Ergebnissen herumbohren.

Gibt man ein Stichwort ins Webformular des Google-Suchdienstes ein, findet der bekannte Search-Engine nicht nur haufenweise Webseiten mit Treffern, sondern sortiert diese dank schlauer Technik auch noch so, dass die wirklich relevanten obenauf liegen.

Um herauszufinden, welche der populärsten Webseiten auf dem Internet zur eigenen Homepage linken, kann man einfach Google nach Erwähnungen passender Begriffe suchen lassen, den URLs der Treffer nachgehen, die entsprechenden Seiten einholen, deren eingebettete Links untersuchen und diejenigen herausfiltern, die auf die eigene Homepage verweisen.

Google über SOAP traktieren

Um Google automatisch zu traktieren, könnte man freilich mit Perls libwww-Bibliothek einen Browser simulieren, automatische Web-Requests abfeuern und die HTML-Ergebnisse mit Modulen wie HTML::Parser oder HTML::TableExtract nach Ergebnissen durchforsten. Doch es geht auch einfacher: Wie immer erkannten die cleveren Googler die Zeichen der Zeit und bieten seit einiger Zeit auf [2] eine SOAP-Schnittstelle für ihren famosen Suchdienst an. Damit nicht Heerscharen wildgewordener Skripthacker den Google-Service mit zu vielen automatischen Anfragen bombardieren und lahmlegen, verlangen die Google-Menschen allerdings, dass man sich auf [2] mit einer Email-Adresse registriert. Dafür bekommt man dann einen Lizenz-Schlüssel, der jeder automatischen SOAP-Abfrage beiliegen muss. Google erlaubt so nicht-kommerziellen Nutzern bis zu 1000 Anfragen am Tag -- fair enough!

SOAP in Perl -- Kein Problem

Wie in [3] schon einmal erörtert, schreiben sich SOAP-Anfragen in Perl ganz einfach mit Pavel Kulchenkos SOAP::Lite-Modul. Aber es geht sogar noch billiger: Mit Net::Google liegt von Aaron Straup Cope eine schöne objektorientierte Abstraktion des Google-Webservices vor, die unter der Haube freilich SOAP::Lite nutzt.

Listing googledrill zeigt ein Perlskript, das Google nach einem in der Konfigurationsvariablen $GOOGLE_SEARCH festgelegten Begriff suchen lässt und aus den Treffer-URLs diejenigen entfernt, die der reguläre Ausdruck $HIT_EXCL_PATTERN ausfiltert. Allen übrigbleibenden URLs geht es nach, holt das entsprechende Dokument vom Web, untersucht das zurückkommende HTML nach Links, filtert die auf den regulären Ausdruck $LINK_PATTERN passenden aus und eliminiert nebenbei noch alle Doppelten.

Am Ende ergibt sich eine Liste mit URLs, die irgendwo in die Tiefen der Homepage zeigen, sortiert nach Häufigkeit. Und zu jeder dieser Homepage-URLs erhält man eine Liste von Websites, die sie nutzen.

Mit den im Skript googledrill gezeigten Konfigurationsdaten sucht Google zunächst nach allen Seiten, die Schilli enthalten, ignoriert aber die Treffer auf perlmeister.com, schließlich interessieren nur externe Websites. Diese holt googledrill dann einzeln ein, untersucht sie auf Links, die auf perlmeister.com verweisen, merkt sie sich und bereitet das Ergebnis auf:

    http://perlmeister.com/ (12)
        http://pwo.de/links/People.html
        ...
    http://www.perlmeister.com/ (5)
        http://www.schweizr.com/perl/
        ...
    http://www.perlmeister.com/cgi/article-search.pl (1)
        http://xlab.net/jochen/perl/perl-9.html
        ...

12 Seiten linken also unter dem Stichwort ``Schilli'' zu perlmeister.com, 5 zu www.perlmeister.com, einer gar dreist zu meiner Artikelsuche unter /cgi/article-search.pl und so weiter. Wer hätte das gedacht! googledrill macht's möglich.


Implementierung

googledrill schaltet zunächst mit use strict und use warnings strenge Programmierkonventionen und Warnungen an, wie allgemein in der Profi-Liga üblich. Es benötigt Net::Google für die Google-Kommunikation, LWP::Simple für Webzugriffe und HTML::TreeBuilder bzw. URI::URL für HTML-Analyse und URL-Extraktion.

$RESULTS_PER_PAGE legt fest, wieviele Treffer Google per Ergebnisseite anzeigen soll. Das von Google festgelegte Maximum für diesen Wert liegt bei 100, aber auch tieferbohrende Suchabfragen laufen problemlos bis zum in $RESULTS_TOTAL eingestellten Wert, indem einfach solange 100er Seiten abgefragt werden, bis $RESULTS_TOTAL erreicht ist.

Die Zeilen 21 und 22 legen den von Google ausgehändigten Lizenzschlüssel fest, einen 32-stelligen Base64-kodierten String. Statt eines Skalars nutzt das Skript die use constant-Syntax, mit der sich einmal gesetzte und zukünftig unmodifizierbare Konstanten definieren lassen. LOCAL_GOOGLE_KEY (wichtig: ohne das $-Zeichen liefert den nach [2] gesetzten Google-Schlüssel als String zurück.

Zeile 24 initialisiert das Net::Google-Objekt, das den Google-Service abstrahiert. Zeile 28 definiert mit %links_seen einen Hash, der als Schlüssel die auf externen Webseiten stehenden Link-URLs führt und als Wert eine Referenz auf einen Array hält, der die URLs dieser Webseiten als Elemente enthält.

Der Skalar $hits_seen_total zählt mit, wieviel Google-Treffer das Skript bereits verarbeitet hat. Das hilft später, wenn wir Google mitteilen müssen, den wievielten Hunderterpack von Treffern es an unserer SOAP-Rampe anliefern soll.

Zeile 31 beherbergt eine while-Schleife, die solange geschaftelt, bis die in $RESULTS_TOTAL festgelegte Zahl von Google-Hits erreicht ist. Vorzeitig wird nur in Zeile 62 abgebrochen, falls Google keine weiteren Ergebnisse mehr liefern kann.

Zeile 33 holt von Net::Google mit der search-Methdode ein Such-Objekt und legt es (oder genauer: eine Referenz darauf) in $session ab. Die in Zeile 36 aufgerufene query-Methode setzt den später an Google zu sendenden Suchbegriff. Die in Zeile 39 aufgerufene results()-Methode kontaktiert den Google Web-Service und bekommt per SOAP das Ergebnis in Form einer Referenz auf einen Array zurück, dessen erstes Element wiederum eine Referenz auf einen Array mit Treffer-Objektreferenzen ist. (Wofür die anderen Elemente gut sind, wissen nur die Google-Menschen).

Die for-Schleife in Zeile 42 klaubt diese der Reihe nach auseinander. Die URL-Methode auf ein Ergebnis-Objekt ($hit) fördert dessen URL als String zutage. Die ab Zeile 108 definierte Funktion norm_url normiert diesen dann mit Hilfe der canonical()-Methode des URI::URL-Moduls, macht also zum Beispiel aus http://AoL.cOM wieder http://aol.com, um zu verhindern, dass derselbe URL zweimal verarbeitet wird, weil er zweimal in verschiedenen Erscheinungsformen auftauchte.

Falls der URL auf den in $HIT_EXCL_PATTERN gelegten regulären Ausdruck passt, verwirft ihn Zeile 46 sofort und fährt mit dem nächsten Treffer fort. Zeile 51 ruft die ab Zeile 79 definierte Funktion get_links auf, die einen URL entgegennimmt, mit Hilfe von LWP::Simple das dahinter liegende Dokument vom Web holt, dessen HTML untersucht, die dort mit <A HREF=...> definierten Links herausfiltert und als Liste zurückliefert. Zeile 51 iteriert darüber. Der reguläre Ausdruck in Zeile 54 elimiert die nicht auf $LINK_PATTERN passenden und stellt so sicher, dass wir nur Links auf unsere eigene Website untersuchen.

Zeile 57 hängt den Link an den Hash-Eintrag unter dem Schlüssel der Treffer-URL an. $links_seen{$link} enthält eine Referenz auf einen Array, den @{...} als solchen entbloßt und der push-Funktion übergibt. Falls unter $link noch gar kein Hash-Eintrag existiert, sorgt Perls ``Auto-Vivification''-Mechanismus dafür, dass hinter den Kulissen ein leerer anonymer Array entsteht.

Falls Google weniger Treffer als $RESULTS_PER_PAGE lieferte, bricht Zeile 62 die in Zeile 31 begonnene while-Schleife ab. Zeile 63 erhöht die Anzahl der bisher gefundenen Treffer um die in der gegenwärtigen Trefferseite aufgeführten.

Die Ausgabe der Links ab Zeile 67 iteriert über die im Hash %links_seen geführten Schlüssel und sortiert sie nach der Anzahl der zugeordneten URLs, die jedem Eintrag als Arrayreferenz anhängen. @{$links_seen{$a}} evaluiert in skalarem Kontext zur Anzahl der Elemente, die im Hash %links_seen unter dem Schlüssel $a als Arrayreferenz hängen. Da in der Sortroutine $b links und $a rechts vom numerischen Vergleicher <=> stehen, wird numerisch absteigend sortiert -- die URLs mit den meisten Treffern kommen also zuerst.

Die ab Zeile 79 definierte Funktion get_links() nimmt einen URL entgegen, holt das entsprechende Dokument mit der von LWP::Simple automatisch exportierten get()-Funktion vom Web und liefert dessen HTML als String zurück. Im Fehlerfall kommt undef zurück, was die if-Bedingung in Zeile 87 abfängt, eine Warnung aussendet und einen leeren Links-Array ans Hauptprogramm zurückgibt.

Zeile 94 füttert das gefundene HTML an ein neu erzeugtes HTML::TreeBuilder-Objekt, das einer Unterklasse von HTML::Parser entstammt. Die in Zeile 95 aufgerufene extract_links()-Methode gibt wegen der ihr übergebenen Liste, die nur den Tag-Namen 'a' enthält, nur diejenigen Links im Dokument zurück, die aufgrund eines <A HREF=...> Tags auf ein anderes Dokument verweisen (<IMG> und Konsorten werden ausgeschlossen). Falls extract_links() ohne Probleme durchging, enthält $ref eine Referenz auf einen Array mit URLs. Zeile 97 wird sie alle mit der ab Zeile 108 definierten Funktion norm_url() kanonisieren und als Elemente im Array @links ablegen. Auf relative URLs brauchen wir keine Rücksicht zu nehmen, da wir nur an externen Links interessiert sind. Der return-Befehl in Zeile 104 ruft noch schnell den grep-Befehl auf, um mit Hilfe des %dupes-Arrays Duplikate aus dem @links-Array auszufiltern und gibt anschließend eine Liste mit Link-URLs ans Hauptprogramm zurück.

Das ab Zeile 108 definierte Funktion norm_url() nimmt einen URL als String entgegen, erzeugt daraus ein URI::URL-Objekt, ruft dessen canonical()-Methode auf, um ihn in zu normalisieren, und gibt ihn dann als String zurück.


Installation

Von [2] ist zunächst der kostenlose Lizenzschlüssel für die Google-Zugriffe abzuholen und in Zeile 22 des Skripts googledrill einzutragen. Das verwendete Perl-Modul Net::Google erfordert SOAP::Lite, das wiederum MIME::Parser und MIME::Simple nutzt -- alle Komponenten sind natürlich wie immer frei auf dem CPAN erhältlich. Die CPAN-Shell wird mit

    perl -MCPAN -e'install Net::Google'

automatisch die notwendigen Schritte einleiten.


Mehr

Die Anwendungsmöglichkeiten der Google-SOAP-Schnittstelle sind praktisch unbegrenzt: Wie wäre es zum Beispiel mit einem einmal täglich laufenden Cron-Job, der die ersten hundert Treffer zu einem Thema abholt, die Treffer-URLs in einer DBM-Datei speichert und per Email Alarm schlägt, falls sich ein neuer URL einstellt? Oder einmal in der Woche auf Google nach den Namen von verschollenen Ex-Schulkameraden sucht und bekanntgibt, wenn diese endlich eine eigene Website aufziehen und auf dem Internet erscheinen? Erfindet fleißig neue Google-Abfragen!

Listing 1: googledrill

    001 #!/usr/bin/perl
    002 ###########################################
    003 # googledrill - Explore and follow Google 
    004 #               results
    005 # Mike Schilli, 2002 (m@perlmeister.com)
    006 ###########################################
    007 use warnings;
    008 use strict;
    009 
    010 use Net::Google;
    011 use HTML::TreeBuilder;
    012 use LWP::Simple;
    013 use URI::URL;
    014 
    015 my $GOOGLE_SEARCH    = 'Schilli';
    016 my $HIT_EXCL_PATTERN = qr(perlmeister\.com);
    017 my $LINK_PATTERN     = qr(perlmeister\.com); 
    018 my $RESULTS_PER_PAGE = 100;
    019 my $RESULTS_TOTAL    = 500;
    020 
    021 use constant LOCAL_GOOGLE_KEY => 
    022  "XXX_INSERT_YOUR_OWN_GOOGLE_KEY_HERE_XXX";
    023 
    024 my $service = Net::Google->new(
    025     key   => LOCAL_GOOGLE_KEY,
    026 );
    027 
    028 my %links_seen       = ();
    029 my $hits_seen_total  = 0;
    030 
    031 while($hits_seen_total < $RESULTS_TOTAL) {
    032         # Init search
    033     my $session = $service->search(
    034         max_results => $RESULTS_PER_PAGE,
    035         starts_at   => $hits_seen_total);
    036     $session->query($GOOGLE_SEARCH);
    037 
    038         # Contact Google for results
    039     my @hits = @{($session->results())[0]};
    040 
    041         # Iterate over results
    042     for my $hit (@hits) {
    043         my $url = norm_url($hit->URL());
    044 
    045             # Eliminate unwanted sites
    046         next if $url =~ $HIT_EXCL_PATTERN;
    047 
    048             # Follow hit, retrieve site
    049         print "Getting $url\n";
    050 
    051         for my $link (get_links($url)) {
    052 
    053             # Ignore self-links
    054           next if $link !~ $LINK_PATTERN;
    055 
    056             # Count link and push referrer
    057           push @{$links_seen{$link}}, $url;
    058         }
    059     }
    060 
    061     # Not enough results to continue?
    062     last if @hits < $RESULTS_PER_PAGE;
    063     $hits_seen_total += $RESULTS_PER_PAGE;
    064 }
    065 
    066     # Print results, highest counts first
    067 for my $link (sort { @{$links_seen{$b}} <=> 
    068                      @{$links_seen{$a}} 
    069                    } keys %links_seen) {
    070     print "$link (" .
    071        scalar @{$links_seen{$link}}, ")\n";
    072 
    073     for my $site (@{$links_seen{$link}}) {
    074         print "    $site\n";
    075     }
    076 }
    077 
    078 ###########################################
    079 sub get_links {
    080 ###########################################
    081     my($url) = @_;
    082 
    083     my @links = ();
    084 
    085         # Retrieve remote document
    086     my $data = get($url);
    087     if(! defined $data) {
    088         warn "Cannot retrieve $url\n";
    089         return @links;
    090     }
    091 
    092        # Extract <A HREF=...> links
    093     my $tree = HTML::TreeBuilder->new();
    094     $tree->parse($data);
    095     my $ref = $tree->extract_links(qw/a/);
    096     if($ref) {
    097         @links = map { norm_url($_->[0]) 
    098                      } @$ref;
    099     }
    100     $tree->delete();
    101 
    102        # Kick out dupes and return the list
    103     my %dupes;
    104     return grep { ! $dupes{$_}++ } @links;
    105 }
    106 
    107 ###########################################
    108 sub norm_url {
    109 ###########################################
    110     my($url_string) = @_;
    111 
    112     my $url = URI::URL->new($url_string);
    113     return $url->canonical()->as_string();
    114 }

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2002/11/Perl

[2]
Die API-Seite auf google.com: http://www.google.com/apis/

[3]
``Daily Soap'' (Webservices mit SOAP), Michael Schilli, Linux-Magazin 07/2002, http://www.linux-magazin.de/ausgabe/2002/07/perl/perl.html

[4]
Gutes Buch zur Perl-Web-Client-Programmierung: ``Perl & LWP'', Sean M. Burke, O'Reilly 2002, ISBN 0596001789

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.