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. |