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.
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!
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.
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.
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.
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!
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 }
Michael Schilliarbeitet 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. |