Steht keine API für Webinformationen zur Verfügung, hilft oft Perl mit der Brechstange des Screen Scraping. Seit neuestem überwindet es sogar mit JavaScript aufgestellte Hürden.
Nicht weniger als drei ehrwürdige Linksys-Router schaufeln die Ethernetpakete im Wohnbereich der Perlmeister-Labs umher. Auf allen dreien tut die Tomato-Firmware ([2]) seit Jahren ohne jeglichen Störfall ihren Dienst.
![]() |
Abbildung 1: Tomatos Übersichtsseite listet unter anderem die "Uptime" des Routers in Tagen und Stunden auf. |
Da Tomatos Admin-Webseite nicht nur allerlei nützliche Einstellungen erlaubt, sondern auch noch informative Statusdaten anzeigt, lag es nahe, einen Screen-Scraper zu schreiben, um die Daten in regelmäßigen Abständen auf den Heimrechner zu holen, in einer Datenbank zu speichern und bei auffälligen Ausreißern Alarme auszulösen.
Doch, oh weh! Beim Einholen der mit Basic Auth gesicherten Seite mit
wget http://root:passwort@192.162.0.1
zeigte sich, dass Tomato die Felder der Anzeige mittels JavaScript
auffrischt und einfache Webscraper wie das Perl-Modul WWW::Mechanize
statt der begehrten Uptime-Zeit lediglich JavaScript-Code runterladen.
![]() |
Abbildung 2: Durch einfaches Einholen der Webseite lässt sich der Uptime-Wert nicht extrahieren. |
Damit die Seite die Daten richtig anzeigt, muss auf der Client-Seite ein JavaScript-Engine anlaufen, der den Code interpretiert und gemäß den darin enthaltenen Anweisungen die DOM (Document Object Model) der im Browser dargestellten Seite auffrischt. Screenscraper in Rohform tun das nicht, verhalten sich wie Browser mit abgeschaltetem JavaScript und erhalten darum nicht das gewünschte Ergebnis.
Die herkulische Aufgabe, diese Browseraktionen in Perl zu implementieren hat das CPAN-Modul WWW::Scripter erledigt. Zusammen mit dem Plugin WWW::Scripter::Plugin::Ajax für Serverrückrufe, der DOM-Schnittstelle HTML::DOM und dem Pure-Perl ECMAScript (JavaScript) Engine JE stellt es alle notwendigen Funktionen bereit. Wenn man darüber nachdenkt, wieviele DOM-spezifische Browserunterschiede es allein zwischen Internet Explorer und Firefox gibt, lässt sich erahnen, wie viel Arbeit in den Modulen steckt. Außerdem verhält sich das Modul wie ein weiterer Browser, Unterschiede zwischen seiner Implementierung und dem sonst verwendeten Desktopbrowser sind unvermeidlich. Eine weitere Möglichkeit, einen javascriptgesteuerten Scriptclient zu implementieren, wäre der Einsatz einer Browserfernsteuerung wie Selenium ([3]).
Listing 1 zieht zunächst WWW::Scripter herein und
lädt den separat erhältlichen Ajax-Plugin mit der Methode use_plugin()
.
Die Klasse ist von WWW::Mechanize und damit auch von LWP::UserAgent
abgeleitet und unterstützt demnach die Methode get
zum Einholen
von Webseiten. Da der Router beim HTTP-Zugang nach einem Passwort
für den root
-Account fragt, stellt das Skript dieses mittels der
ebenfalls ererbten Methode credentials()
zur Verfügung.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use WWW::Scripter; 04 use Sysadm::Install qw(:all); 05 use HTML::TreeBuilder::XPath; 06 07 my $w = new WWW::Scripter; 08 $w->use_plugin('Ajax'); 09 10 my $pw = slurp "pw.txt"; 11 chomp $pw; 12 $w->credentials( "root", $pw ); 13 $w->get('http://192.168.0.1'); 14 15 $w->wait_for_timers( max_wait => 1 ); 16 17 my $tree= HTML::TreeBuilder::XPath->new; 18 $tree->parse( $w->content() ); 19 my $uptime = 20 $tree->findvalue( 21 '/html/body//tr[@id="uptime"]/td[@class="content"]'); 22 23 print "uptime: $uptime\n";
Damit das Passwort nicht im Skript hartkodiert ist, liest es die
Funktion slurp
aus der Datei pw.txt
im aktuellen Verzeichnis
ein. Die Datei enthält nur eine Zeile mit dem Passwort und sollte
gegen unberechtigten Lese- oder gar Schreibzugriff geschützt sein.
Ganz astrein ist diese Lösung freilich nicht, doch irgendwie müssen
wir den Schlüssel unter der Fußmatte verstecken, wenn das Skript
automatisch laufen soll und der User nicht jedes Mal das Passwort
tippt.
Holt get
die Seite vom Webinterface des Routers, enthält diese
noch keine Daten, sondern nur den eingebetteten JavaScript-Code.
Der Aufruf wait_for_timers()
startet nun den JavaScript-Engine
und lässt ihn auf dem Seiteninhalt herumfuhrwerken. Die Methode
würde nun solange blocken, bis auch der letzte JavaScript-Timer
im Code aufgehört hätte zu laufen, was aber bei vielen Webseiten
einfach unendlich lange dauern würde. Der Parameter max_wait
gibt
deshalb vor, nicht länger als eine Sekunde zu warten. Diese
Zeitspanne reicht der Routerseite erfahrungsgemäß, um die
dynamischen Felder zu befüllen. Ein anschließender Aufruf von content()
gibt das mit den Daten aufgefrischte HTML zurück.
![]() |
Abbildung 3: Der JavaScript-Engine hat die Uptime-Daten eingefüllt, nachdem der JavaScript-Engine mittels WWW::Scripter gelaufen ist. |
Wie in Abbildung 3 ersichtlich, steht der gesuchte Uptime-Wert in einem
Wirrwarr von HTML-Tags, und man könnte ihn entweder mit regulären
Ausdrücken oder einem HTML-Parser herausfieseln. Listing 1 wählt mit
dem XPath-Parser HTML::TreeBuilder::XPath vom CPAN die wohl bequemste Methode,
Der Pfadausdruck in Zeile 21 steigt in der Hierarchie des HTML-Dokuments
erst zum Body-Tag herunter, und sucht dann wegen dem doppelten Schrägstrich
in beliebigen Tiefen nach den weiter rechts spezifizierten Tags.
Zum Ziel führt ein TR-Tag mit der dem id
-Attribut "uptime", das ein
TD-Tag mit dem class
-Attribut "content" einschließt, wie Abbildung 3
zeigt. Die Methode findvalue
fördert den darin begrabenen Text
zutage und es bleibt dem Skript nur noch, den gefundenen Wert auf
der Standardausgabe auszugeben.
Eine etwas dynamischere Aufgabe stellt die Tomato-Seite mit den
aktuellen Bandbreitenwerten. Unter dem Pfad /bwm-realtime.asp
erscheint der Graph in Abbildung 4, der eine mittels JavaScript erstellte
Grafik mit den Schwankungen während der letzten 24 Stunden. In der
Tabelle darunter stehen die aktuellen Werte für empfangene Daten
(RX) in kbit/sec sowie gesendete Daten (TX). Neben dem aktuell
gemessenen Wert listet Tomato hier Maximalwerte (Peak) auf, Durchschnittswerte
(Avg), sowie die Summe transferierter Bits seit dem Start der Messung.
![]() |
Abbildung 4: Der Tomato-Router frischt die aktuellen Bandbreitenwerte regelmäßig mit JavaScript auf. |
Beim ersten Laden der Seite stehen alle Werte auf Null und erst nach einigen
Sekunden füllen sich die Tabellenreihen mit interessanten Werten. Listing
2 wartet aus diesem Grund in der Funktion rounds()
beim ersten
Aufruf in Zeile 17 geschlagene fünf Testdurchläufe ab, während deren
es jeweils den Skripter mit der Methode check_timers()
dazu auffordert,
die Timer im JavaScript-Code laufen zu lassen. Anschließen legt es in
Zeile 27 eine einsekündige Pause ein. Nach Ablauf aller vorgeschriebenen
Runden ruft Zeile 30 den rounds()
beim Aufruf hereingereichten Callback
auf, was während der Proberunden aus Zeile 17 eine leere Funktionshülse
ist, aber im Realbetrieb ab Zeile 18 die ab Zeile 34 definierte
Funktion extract_bandwidth
.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use Sysadm::Install qw(:all); 04 use WWW::Scripter; 05 use HTML::TableExtract; 06 use YAML qw(Dump); 07 08 my $w = new WWW::Scripter; 09 $w->use_plugin('Ajax'); 10 11 my $pw = slurp "pw.txt"; 12 chomp $pw; 13 14 $w->credentials( "root", $pw ); 15 $w->get('http://192.168.0.1/bwm-realtime.asp'); 16 17 rounds( $w, 5, sub { } ); 18 rounds( $w, 1, \&extract_bandwidth ); 19 20 ########################################### 21 sub rounds { 22 ########################################### 23 my( $w, $rounds, $callback ) = @_; 24 25 for( 1 .. $rounds ) { 26 $w->check_timers(); 27 sleep( 1 ); 28 } 29 30 $callback->( $w->content ); 31 } 32 33 ########################################### 34 sub extract_bandwidth { 35 ########################################### 36 my( $html ) = @_; 37 38 my $te = HTML::TableExtract->new( ); 39 $te->parse( $html ); 40 41 my $ts = $te->first_table_found(); 42 43 my %bw = (); 44 45 foreach my $row ($ts->rows) { 46 my @cols = map { /(\S+)/ } @$row; 47 48 $bw{ $cols[0] } = 49 { avg => $cols[3], 50 peak => $cols[5], 51 }; 52 } 53 54 print Dump( \%bw ); 55 }
Als Parser für die in HTML-Tabellen versteckte Information nutzt das Skript
das CPAN-Modul HTML::TableExtract. Dessen Methode parse()
nimmt in
Zeile 39 den von JavaScript vorher modifizierten HTML-Code der Seite
entgegen und formt einen Syntaxbaum daraus. Die Methode
first_table_found()
sucht dann die erste HTML-Tabelle, die auf
Tomatos Bandwidth-Seite tatsächlich die gesuchten Daten enthält. Während
der Testphase, während der Entwickler noch nicht weiß, welche Tabelle
welche Informationen enthält, hilft die Methode tables()
des gleichen
Moduls, das alle gefundenen Tabellen als Objekte zurückgibt. Deren
Lage und hierarchische Verschachtelung gibt coords()
an und ihren
Inhalt schüttet rows()
zeilenweise aus. Die Manualseite erklärt deren
Verwendung ausführlich.
![]() |
Abbildung 5: WWW::Scripter saugt Informationen von einer JavaScript-getriebenen Webseite. |
Tomato gibt zu einem kbit/sec (Kilo-Bit) auch noch einen Wert für
KB/sec (Kilo-Byte) aus, da sich diese jedoch lediglich um einen konstanten
Faktor unterscheiden, filtert der reguläre Ausdruck in der map-Anweisung
in Zeile 46 letzteren Wert jedoch aus, indem er alles nach dem ersten
Leerzeichen abschneidet. Die erste Spalte in @cols ist so entweder
"RX" oder "TX", gefolgt vom aktuellen Transferwert. In der 4. und 6.
Spalte stehen die Werte für die durchschnittliche Bandbreite und
Spitzenwerte. Zeile 48 schiebt sie als Hash mit den Schlüsseln "avg"
und "peak" in einen weiteren Hash unter "RX" bzw. "TX". Damit sich die
Ausgabe des Skripts machinell leicht nachverarbeiten lässt, druckt Zeile
54 den resultierenden Hash mittels der Methode Dump
des am Programmkopf
geladenen YAML-Moduls aus. Abbildung 5 zeigt das Ergebnis auf der
Kommandozeile. Ein weiterverarbeitendes Skript kann die Ausgabe
mittels der Load
-Methode des gleichen Moduls in den Speicher laden
und gleich als Hash weiterverarbeiten.
Die dynamischen Greiferskripte benötigen außer dem Webscraper WWW::Mechanize auch noch die CPAN-Module WWW::Scripter und WWW::Scripter::Plugin::Ajax, die sich auf Ubuntu mit einer CPAN-Shell installieren lassen. Den in Listing 1 verwendeten XPath-Parser, HTML::TreeBuilder::XPath, und den Tabellenparser HTML::TableExtract aus Listing 2 gibt es ebenfalls auf dem CPAN.
Natürlich könnte man mit dem Tomato-Router besser und sicherer über ssh kommunizieren und eventuell sogar ein neues Image flashen, das eine Web-API bereitstellt. Die gezeigten Tricks lassen sich aber auch auf allerlei interessanten bekannten Seiten im Web anwenden. Der engagierte Scraper-Hacker muss lediglich darauf achten, dass seine Erzeugnisse nicht gegen die TOS (Terms of Service) der Anbieter verstoßen, die Scraping oft nicht gerne sehen.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/11/Perl
Tomato Firmware für Linksys-Router: http://www.polarcloud.com/tomato
"Browser ferngesteuert", Michael Schilli, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2006/10/Browser-ferngesteuert
![]() |
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. |