Schnell ein Skript geschrieben – und schon steht der Programmierer vor dem typischen Problem: Wie ein HTTP-Dokument vom Web holen, welche Sprache macht es am einfachsten?
Kaum eine Programmieraufgabe zeigt die Unterschiede zwischen den gängigen Sprachen so deutlich wie das Einholen eines Dokuments vom Web. Admins greifen bei Shellskripten oft auf das Utility »curl« zurück, das die Daten hinter einer URL ohne viel Federlesen überträgt und auf der Standardausgabe ausgibt.
Doch was ist, wenn die 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 Suchformular, sondern nur einen Hinweis, dass die gewünschte Seite wohl auf »www.google.com« zu finden ist. Mit der Option »-L« folgt »curl« hingegen dem Verweis und pumpt dann im Anschluss 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 alles auf einen Rutsch runterschlingt? Klappt bei einer HTTPS-URL die Verschlüsselung mit dem SSL-Protokoll automatisch, und prüft das 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.
Go, Hipster go!
Dem relativ neuen Go liegt der Websupport 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 einen Ehepartner oder für einen der Editoren »vi« oder »emacs«.
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 verschlungene Pfade des Internets ein Fehler auftreten, der zum Abbruch des Datenstroms führt. In Go geben Funktionen daher gern zwei Parameter zurück, ein Ergebnis und eine Fehlerstruktur, deren Wert auf »nil« gesetzt ist, falls alles glattgegangen ist.
Listing 1
http-get.go
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 }
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. Die 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 tatsächlich beendet ist und Go die Daten entsorgen kann. Das zugehörige Kommando steht schon in Zeile 19, wird aber mit dem eleganten Schlüsselwort »defer« bis zum Ende der gerade ausgeführten Funktion verzögert.
Schlankes Python
Anders dagegen die Python-3-Implementierung in Listing 2 mit der modernen Bibliothek »requests«: Sie ist kompakter, weil Python Exceptions wirft, die Entwickler zentral prüfen können. Ob der Request ein nicht existierendes Protokoll spezifiziert (»abc://«), die Serverzertifikats-Prü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 in einem Aufwasch prüft.
Listing 2
http-get.py
01 #!/usr/bin/python3
02 import requests
03
04 try:
05 r = requests.get("http://google.de");
06 if r.status_code == 200:
07 r.encoding = 'utf-8'
08 print(r.text)
09 else:
10 print(r.status_code)
11 except Exception as exp:
12 print("Error: " + str(exp))
Das ist 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 kommt. Auch die Lesbarkeit des Codes leidet, denn es ist nicht ganz klar, welche Zeile im »try«-Block die Exception ausgelöst hat. 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 erscheint. Erst manuelles Umschwenken auf »utf-8« in Zeile 7 veranlasst das folgende »r.text«, den Inhalt UTF-8-kodiert auszugeben. Ein Statuscode ungleich 200 wirft von Haus aus keine Exception und will manuell geprüft sein. Wer es noch kompakter mag, kann mit der Methode »raise_for_status()« eine Exception werfen, falls der Server etwas anderes als 200 gemeldet hat.
Allesfänger Ruby
Auch Ruby kommt mit einem eingebauten HTTP-Modul namens »net/http« daher, allerdings muss es vor dem Absetzen des Requests die URL noch mit einer weiteren Klasse »URL« analysieren. Das ist für die Ruby-on-Rails-Hüpfer bereits zu viel Arbeit, die Entwickler haben deshalb schon einige Ruby-Gems geschaffen, die das Ganze in einem Aufwasch erledigen. Wie Python wirft Ruby Exceptions aus den Untiefen des Codes, deshalb kann der Entwickler auftretende Fehler zentral abfangen.
Listing 3 zeigt ab der Zeile 6 den Abfang-Block, der mit »begin« beginnt, ab »rescue« Fehler abfängt und schließlich mit »end« endet. Redirects folgt die Standardbibliothek in ihrer Grundeinstellung nicht. Die Webseite von Google.de interpretiert sie dann als »ASCII-8BIT«, was in diesem Fall wohl der von Python erkannten ISO-8859-1-Kodierung entsprechen dürfte.
Listing 3
http-get.rb
01 #!/usr/bin/ruby
02 require 'net/http'
03
04 url = URI.parse('http://www.google.com')
05
06 begin
07 rsp = Net::HTTP.get(url)
08 puts rsp
09 rescue Exception => e
10 puts "Error: " + e.message
11 end
Funktionales Node.js
Wer in einem Javascript-Snippet im Browser eine URL einholen oder Ähnliches im Node.js-Code auf der Server-Seite bewerkstelligen möchte, zum Beispiel auf einem Amazon-Lamda-Server [2], der muss sein Gehirn auf funktionalen Programmierstil umstellen. Denn 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.
Der 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 Softwarekomponenten, damit diese dann auch gut wartbar zusammenspielen.
Die gefürchtete Pyramide der Verdammnis [3] von verschachtelten Callbacks lässt sich mit einigen Konstrukten aufdröseln. Node 7.6 bringt neuerdings sogar Support für die Schlüsselwörter »async« und »await« mit, die asynchronen Code zur optischen Aufhübschung in ein synchrones Korsett pressen [4].
Listing 4 zeigt einen »get«-Call des HTTP-Moduls in Node.js. Neben der URL auf das gewünschte Webdokument 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«.
Listing 4
http-get.js
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 request.on('error', function(err) {
11 console.log( err );
12 });
13
14 res.on('end', function() {
15 console.log( content );
16 });
17 });
18 }
Ersteren springt die Eventschleife 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 Node.js-Bibliothek »/http« automatisch.
Gutes altes Perl
Das gute alte Perl holt Webdokumente 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 Open-SSL-Installation und einer Liste von Rootzertifikaten 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 UTF-8-String zurück. Das ist gut so, denn die Weiterverarbeitung der Daten im Programmcode setzt oft UTF-8 voraus und führt anderweitig zu unschönen Problemen.
Listing 5
http-get.pl
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;
Um aber einen UTF-8-String dann auch als solchen mit »print« unmodifiziert auszugeben, muss das Skript die Standardausgabe zuerst mit Hilfe von »binmode« auf den UTF-8-Modus einnorden. Diese eher umständliche Vorgehensweise ist der Kompatibilität geschuldet und stellt immerhin sicher, dass alte Skripte aus der Anfangszeit von Perls UTF-8-Support mit den neuen Perl-Versionen nicht gleich ausflippen.
Jaja, das Alter, es ist eben kein Zuckerschlecken, wenn’s in allen Gelenken knackt und derweil die jungen Kerle wilde Kapriolen schlagen!
Online PLUS
Im Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/Ausgaben/2017/06/plus
Infos
- Listings zu diesem Artikel:https://www.linux-magazin.de/static/listings/magazin/2017/06/snapshot/
- Michael Schilli, “Auf mehr als ein Wort”: Linux-Magazin 04/17, S. 96.,https://www.linux-magazin.de/Ausgaben/2017/04/Snapshot
- Michael Schilli, “Pyramide der Verdammnis”: Linux-Magazin 12/14, S. 114, https://www.linux-magazin.de/Ausgaben/2017/04/Snapshot
- Sergio De Simone, “Node 7.6 Brings Default Async/Await Support”: https://www.infoq.com/news/2017/02/node-76-async-await







