Schnell ein Skript geschrieben und schon steht der Programmierer vor dem typischen Problem: Wie ein HTTP-Dokument vom Web holen, welche Sprache macht's am einfachsten?
Kaum eine Programmieraufgabe zeigt die Unterschiede zwischen den
gängigen Sprachen so deutlich wie das simple Einholen eines Dokuments vom Web.
Sysadmins greifen bei Shell-Skripts oft auf die Utility curl
zurück, die die Daten hinter einem URL ohne viel Federlesens überträgt
und auf der Standardausgabe herausgibt.
Doch der Teufel steckt wie so oft im Detail, was ist, wenn der URL ins Nirwana
zeigt? Der Server den Zugriff verweigert? Was passiert, falls der Server einen
Redirect zurückgibt? So liefert curl http://google.com
nicht die erwartete
HTML-Seite mit dem berühmten Suchformular, sondern nur einen kurzen Hinweis,
dass die wohl gewünschte Seite statt dessen auf www.google.com
zu finden
ist. Mit der Option -L
folgt curl
hingegen dem Verweis und pumpt dann die
Daten aus der dort gefundenen Quelle. Oder was passiert bei einer Riesendatei
wie einem 4K-Film, geht dem Prozess der RAM-Speicher aus, weil er sich alles
auf einen Rutsch runterschlingt? Klappt bei einem https-URL die Verschlüsselung mit
dem SSL-Protokoll automatisch, und prüft die Utility das Zertifikat des Servers
ordnungsgemäß, damit sie nicht auf einen Man-in-the-Middle-Angriff hereinfällt?
Wie das gute alte curl
bieten populäre Programmiersprachen all dies,
wenngleich auch oft nur als Zusatzpaket und oft mit eigenwilligem Ansatz.
Abbildung 1: Das simple curl-Kommando leistet Erstaunliches hinter den Kulissen. |
Dem relativ neuen Go liegt der Web-Support schon von Haus aus mit dem Paket
"net/http" bei, mustergültig inklusive SSL-Support. Go-Programmierer behandeln
eventuell auftretende Fehler sofort nach Funktionsaufrufen, und können sich
nicht darauf hinausreden, dass geworfene Exceptions schon irgendwo abgehandelt
werden, und wenn es erst als schwer leserlicher Stack-Trace beim Abbruch des
Programms ist. Zweifellos eine Philosophie-Frage mit ähnlicher Tragweite wie
die Entscheidung für eine Religion, einen Ehepartner oder für einen der
Editoren vi
oder emacs
: Es kann nur eine(n) geben.
01 package main 02 import "fmt" 03 import "net/http" 04 import "io/ioutil" 05 06 func main() { 07 resp, err := http.Get("http://google.com") 08 09 if err != nil { 10 fmt.Printf("%s\n", err) 11 return 12 } 13 14 if resp.StatusCode != 200 { 15 fmt.Printf("Status: %d\n", resp.StatusCode) 16 return 17 } 18 19 defer resp.Body.Close() 20 body, err := ioutil.ReadAll(resp.Body) 21 if err != nil { 22 fmt.Printf("I/O Error: %s\n", err) 23 return 24 } 25 26 fmt.Printf("%s\n", body) 27 }
Listing 1 zeigt gleich drei verschiedene Fehlerprüfungen, denn sowohl die Erzeugung
des Requests kann schiefgehen (nicht unterstütztes Protokoll), der Server kann einen
Fehlerstatus-Code liefern (404 bei nicht gefundenem Dokument) und schließlich kann
beim Einholen der Daten über die verschlungenen Pfade des Internets ein Fehler
auftreten, der zum Abbruch des Datenstromes führt. In Go geben Funktionen deswegen
gern zwei Parameter zurück, ein Ergebnis und eine Fehler-Struktur, deren Wert auf
nil
gesetzt ist, falls alles glatt gegangen ist.
Interessant ist auch, dass das Paket "net/http" zuerst den Request ausführt, in Zeile
14 dann den Statuscode des Servers empfangen hat, sich aber mit dem Abpumpen der
Body-Daten noch Zeit lässt. Diese schaufelt ReadAll()
aus dem Paket io/ioutil
erst
später weg und die Close()-Methode auf das Body-Objekt zeigt abschließend an, dass
die Request-Abarbeitung nun tatsächlich beendet ist und Go die Daten entsorgen kann.
Das zugehörige Kommando steht schon in Zeile 19, wird aber mit dem super-eleganten
Schlüsselwort defer
bis zum Ende der gerade ausgeführten Funktion verzögert.
Ganz anders dagegen die Python3-Implementierung in Listing 2 mit der modernen
Bibliothek requests
: Sie kommt kompakter daher, weil Python Exceptions wirft, die
der Entwickler später zentral prüfen kann. Ob der Request ein nicht
existierendes Protokoll spezifiziert ("abc://"), die Serverzertifikatsprüfung
bei SSL fehlschlägt, oder ein I/O-Fehler auftritt, alles löst eine
entsprechende Exception aus, die der Code entweder gesondert oder wie in Zeile
11 gezeigt in einem Aufwasch prüft. Sicher bequemer, doch die eingesparte
Tipparbeit kann sich später rächen, wenn eine Exception aus den Untiefen des
Codes hochgespült wird, und keiner mehr weiß woher sie eigentlich kam. Auch die
Lesbarkeit des Codes leidet, denn es ist nicht ganz klar, welche Zeile im
try-Block die Exception eigentlich ausgelöst hat.
01 #!/usr/bin/python 02 import requests 03 04 try: 05 r = requests.get("http://google.de"); 06 if r.status_code == 200: 07 print(r.text.encode('utf-8')) 08 else: 09 print(r.status_code) 10 except Exception as exp: 11 print("Error: " + str(exp))
Wie gesagt, das Thema hat schon zahllose schwer schlichtbare Endlosdiskussionen
angezettelt. Lustig ist auch, dass r.encoding
nach einem Request auf
google.de
angibt, dass die Seite in <ISO-8859-1> daherkommt. Erst ein
manuelles Umschwenken auf utf-8
in Zeile 7 veranlasst das nachfolgende
r.text
, den Inhalt utf-8-kodiert auszugeben. Ein Status Code ungleich
200 wirft von Haus aus keine Exception und muss manuell geprüft werden. Wer's
noch kompakter mag, kann aber mit der Methode raise_for_status()
eine
Exception werfen, falls der Server etwas anderes als 200 gemeldet hat.
Auch Ruby kommt mit einem eingebauten HTTP-Modul namens net/http
daher, allerdings
muss es vor dem Absetzen des Requests den URL noch mit einer weiteren Klasse
URL
analysieren. Das ist für die Ruby-on-Rails-Hüpfer bereits zuviel Arbeit,
es wurden deshalb schon einige Ruby-Gems geschaffen, die das Ganze in einem
Aufwasch erledigen. Wie Python wirft Ruby Exceptions aus den Untiefen des Codes
und deshalb kann der Entwickler auftretende Fehler zentral abfangen. Listing 3
zeigt ab Zeile 6 den Abfang-Block der mit begin
los geht, ab rescue
Fehler
abfängt und mit end
endet. Redirects folgt die Standard-Bibliothek in der
Grundeinstellung nicht und die Seite von google.de interpretiert es als "ASCII-8BIT",
was wohl der von Python erkannten ISO-8859-1-Kodierung entspricht.
01 #!/usr/bin/ruby 02 require 'net/http' 03 04 url = URI.parse('http://www.google.de') 05 06 begin 07 rsp = Net::HTTP.get(url) 08 puts rsp.force_encoding('ISO-8859-1').encode('UTF-8') 09 rescue Exception => e 10 puts "Error: " + e.message 11 end
Wer in einem JavaScript-Snippet im Browser einen URL einholen oder ähnliches im NodeJS-Code auf der Server-Seite bewerkstelligen möchte, wie zum Beispiel auf einem Amazon-Lamda-Server ([2]), der muss sein Gehirn auf funktionalen Programmierstil umstellen. Event-basierte Systeme ticken ja nicht nach dem Motto "mach dies, warte bis es fertig ist, dann mach das", sondern wollen ihre Instruktionen in der Form "mach dies, dann das, dann das ... und los geht's" erhalten.
Grund dafür ist die Eventschleife, die immer nur kurze Callbacks
ausführen kann, die Kontrolle zurück will und dann wieder vorbeischneit, falls
langsam eintrudelnde Daten endlich von externen Schnittstellen eingetroffen sind.
Diese Struktur erschwert die Lesbarkeit des Codes und erfordert viel Erfahrung
beim Design von Software-Komponenten, damit diese gut wartbar zusammenspielen.
Die gefürchtete "Pyramide der Verdammnis" ([3]) von verschachtelten Callbacks
lässt sich mit einigen Konstrukten aufdröseln und Node 7.6 bringt neuerdings sogar
Support für die Schlüsselworte async
und await
mit, die asynchronen Code
zur optischen Aufhübschung in ein synchrones Korsett pressen ([4]).
01 var http = require('http'); 02 03 http.get("http://google.com", function(res) { 04 var content = ''; 05 06 res.on('data', function(data) { 07 content += data; 08 }); 09 10 res.on('error', function(err) { 11 console.log( err ); 12 }); 13 14 res.on('end', function() { 15 console.log( content ); 16 }); 17 });
Listing 4 zeigt einen get
-Call des http-Moduls in NodeJS. Neben der URL
auf das gewünschte Web-Dokument nimmt es eine Funktion entgegen. Diese wird
später mit einem Response-Objekt aufgerufen und definiert eine Closure
mit einer Variable (content
) und drei Callbacks auf die Events "data"
,
"error"
und "end"
. Ersteren springt die Event-Schleife jedes Mal an, wenn ein Schub
Daten vom Server zurückkommt. Er sammelt die Datenpakete der Reihe
nach ein und hängt sie in der Variablen content
aneinander. Im Falle eines
Fehlers kommt der "error"
-Callback an die Reihe und schreibt in Zeile 11
die Ursache ins Log.
Signalisiert der Server das Ende der Übertragung, springt die Eventschleife den
"end"
-Callback an, der den Inhalt von content
ausgibt, wo sich zu diesem Zeitpunkt
die gesamten Body-Daten der HTTP-Antwort befinden. Redirects folgt die NodeJS-Bibiothek
/http
automatisch.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use LWP::UserAgent; 04 05 my $ua = LWP::UserAgent->new; 06 07 my $resp = $ua->get( "http://google.de" ); 08 if( $resp->is_error ) { 09 die "Error: ", $ua->message; 10 } 11 binmode STDOUT, ":utf8"; 12 print $resp->decoded_content;
Das gute alte Perl holt Web-Dokumente traditionsgemäß mit dem CPAN-Modul
LWP::UserAgent vom Netz. SSL-Support ist nicht automatisch dabei, kommt aber
magisch hinzu, falls der Admin das CPAN-Modul LWP::Protocol::https nachinstalliert,
das sich an eine verfügbare openssl-Installation und einer Liste von
Root-Zertifikaten hängt.
Listing 5 zeigt neben vorschriftsmäßiger Fehlerbehandlung auch noch eine
Eigenheit: Wie manch andere hier vorgestellte Bibliothek folgt es redirects automatisch,
erkennt ISO-8859-1
als Kodierung von google.de, aber liefert
aus decoded_content()
(im Gegensatz zu content()
) einen utf8-String zurück.
Das ist gut so, denn die Weiterverarbeitung der Daten im Programmcode setzt oft
utf8
voraus und führt andersweitig zu unschönen Problemen.
Um aber einen utf8-String dann auch als solchen mit print
unmodifiziert
auszugeben, muss das Skript die Standardausgabe erst mit binmode
auf den
utf8-Modus einnorden. Dieses umständliche Gebahren ist der Kompatibilität
geschuldet und stellt sicher, dass alte Skripts aus der Anfangszeit von Perls
utf8-Support nicht mit neuen Perl-Versionen ausflippen. Jaja, das Alter, es ist
kein Zuckerschlecken, wenn's in allen Gelenken knackt, und derweil die jungen
Kerle wilde Kapriolen schlagen!
Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2017/06/snapshot/
Michael Schilli, "Auf mehr als ein Wort": Linux-Magazin 04/17, S.96, <U>http://www.linux-magazin.de/Ausgaben/2017/04/Snapshot
Michael Schilli, "Pyramide der Verdammnis": Linux-Magazin 12/14, S.114, <U>http://www.linux-magazin.de/Ausgaben/2017/04/Snapshot
"Node 7.6 Brings Default Async/Await Support", Sergio De Simone https://www.infoq.com/news/2017/02/node-76-async-await