In einem früheren Kapitel haben wir HTML zur Beschreibung von Webseiten kennengelernt und später auch dynamische Webseiten mit Hilfe von Javascript definiert, die im Browser ausgeführt werden. Neben dieser client-seitigen Webprogrammierung gibt es auch server-seitige Webprogrammierung. Dabei werden die dynamischen Anteile nicht im Browser ausgeführt sondern auf dem Webserver, so dass Webseiten aus dort (zum Beispiel in einer Datenbank) gespeicherten Daten generiert werden können.
Wir wollen im Folgenden einen Webserver in Python implementieren. Zunächst sehen wir uns dazu an, wie wir HTML-Quelltext in Python generieren können. Da HTML-Quelltext Text ist, können wir ihn in Python als Zeichenkette darstellen. Zum Beispiel liefert die folgende Funktion ein <h1>
-Tag mit übergebenem Inhalt dargestellt als Zeichenkette zurück:
def heading(title):
return f'<h1>{title}</h1>'
Wir können diese Funktion im Python-Terminal mit dem folgenden Aufruf testen:
>>> heading('Hallo')
'<h1>Hallo</h1>'
Wenn wir als Argument der heading
-Funktion HTML-Quelltext übergeben, wird dieser unverändert in das Ergebnis eingebaut:
>>> heading('</h1><script>fire_missiles();</script><h1>')
'<h1></h1><script>fire_missiles();</script><h1></h1>'
Dies kann in Kombination mit Benutzereingaben zu Sicherheitsproblemen führen, den sogenannten Script Injections: Das bedeutet, es ist möglich, potenziell bösartigen Javascript-Code in eine HTML-Seite einzubauen, der beim Abrufen der Seite im Webbrowser des Opfers ausgeführt wird. Später sehen wir, was wir dagegen tun können.
Zunächst wollen wir ein etwas komplexeres HTML-Fragment definieren. Die folgende Funktion erzeugt aus einer übergebenen Liste von Zeichenketten eine ungeordnete Liste mit entsprechenden Einträgen:
def unordered_list(items):
result = '<ul>'
for item in items:
result = result + f'<li>{item}</li>'
result = result + '</ul>'
return result
Das Ergebnis wird hier schrittweise in der Variablen result
zusammengebaut und am Ende des Funktionsrumpfes zurückgegeben. Der folgende Aufruf dokumentiert die Verwendung der definierten Funktion:
>>> unordered_list(['essen', 'lesen'])
'<ul><li>essen</li><li>lesen</li></ul>'
Schließlich wollen wir nun die beiden definierten Funktionen verwenden, um ein komplettes HTML-Dokument zu generieren:
todo_list = ['essen', 'lesen']
def todo_page():
return '''<!DOCTYPE html>
<html>
<head>
<title>Todo</title>
<meta charset="utf-8">
</head>
<body>
''' + heading('Todo-Liste') + unordered_list(todo_list) + '''
</body>
</html>'''
Das erzeugte Dokument sieht mit print(todo_page())
ausgegeben wie folgt aus:
<!DOCTYPE html>
<html>
<head>
<title>Todo</title>
<meta charset="utf-8">
</head>
<body>
<h1>Todo-Liste</h1><ul><li>essen</li><li>lesen</li></ul>
</body>
</html>
Gegenüber einer Definition in HTML wirken die gezeigten Funktionen vergleichsweise kompliziert und unhandlich. Zum einen kommt das daher, dass sie durch die Parametrisierung allgemeiner sind als der konkrete HTML-Quelltext, der durch einen Aufruf der definierten Funktion entsteht. Aber auch das explizite Hantieren mit Zeichenketten verkompliziert die Definitionen. Wir werden später sehen, wie sich das dynamische Generieren von HTML mittels HTML-Templates vereinfachen lässt.
In den Vorlesungen zur Weiterbildung wird dieser Abschnitt nicht behandelt. Stattdessen werden HTML-Templates verwendet, um HTML-Quelltext in Python zu erzeugen.
Mit Hilfe des Python-Pakets dominate
können wir die gezeigten Funktionen leserlicher definieren. Um das Paket zu nutzen, installieren wir es über die Paketverwaltung und binden es anschließend zu Beginn unseres Python-Programms ein:
from dominate import *
Die Funktion zum Erzeugen der Überschrift sieht mit diesem Paket zum Beispiel wie folgt aus:
def heading_dom(title):
result = tags.h1(title)
return result
Die Klasse tags.h1
stellt ein <h1>
-Element dar. Für jedes denkbare HTML-Element ist im Modul tags
eine entsprechende Klasse definiert, die wir verwenden können, um HTML-Elemente zu erzeugen und zusammenzusetzen.
Als Rückgabewert wird nun ein Objekt zurückgegeben, das wir mit der str
-Funktion in eine Zeichenkette umwandeln können. Hier ist ein Beispielaufruf der definierten Funktion:
>>> str(heading_dom('Hallo'))
'<h1>Hallo</h1>'
Durch Verwendung des dominate
-Pakets wird nun HTML-Quelltext im Argument der Funktion anders behandelt als bei unserer vorherigen Definition:
>>> str(heading_dom('</h1><script>fire_missiles();<script><h1>'))
'<h1></h1><script>fire_missiles();<script><h1></h1>'
Alle Sonderzeichen werden HTML-spezifisch so umgewandelt, dass die Überschrift später im Browser genau so aussieht, wie die Zeichenkette, die wir übergeben haben.
Die Funktion zum Erzeugen einer Liste können wir wie folgt anpassen:
def unordered_list_dom(items):
result = tags.ul()
for item in items:
result += tags.li(item)
return result
Hier wird erst ein <ul>
-Element erzeugt (eine unsortierte Liste) und dann in einer for
-Wiederholung mehrere Listenelemente <li>
erzeugt und zu dieser Liste hinzugefügt. Wir können also beliebige Python-Konstrukte verwenden, um Anweisungen zur HTML-Generierung zu definieren. Die Implementierung mit Hilfe des dominate
-Pakets stellt sicher, dass alle Elemente korrekt geschachtelt sind und dass schließende Tags zu den entsprechenden öffnenden Tags passen.
Auch Elemente mit Attributen lassen sich auf diese Weise erzeugen. Dazu passen wir die Zeile in der for
-Wiederholung wie folgt an:
result += tags.li(tags.a(item, href='#' + item))
Attribute werden also als benannte Argumente übergeben. Der Aufruf unordered_list_dom(['essen','lesen'])
erzeugt jetzt ein Objekt, das mit print
die folgende Ausgabe erzeugt (inkl. Zeilenumbrüchen und Einrückungen):
<ul>
<li>
<a href="#essen">essen</a>
</li>
<li>
<a href="#lesen">lesen</a>
</li>
</ul>
Schließlich wollen wir nun die beiden definierten Funktionen verwenden, um ein komplettes HTML-Dokument zu generieren:
todo_list = ['essen', 'lesen']
def todo_page():
doc = document(title='Todo')
doc.head += tags.meta(charset='utf-8')
doc.body += heading_dom('Todo-Liste')
doc.body += unordered_list_dom(todo_list)
return str(doc)
Der Aufruf document(title='Todo')
erzeugt ein HTML-Dokument mit dem als Argument angegebenen Titel. Dieses Dokument enthält Header- und Body-Elemente, auf die wir über die Attribute head
und body
zugreifen können.
Zum Header wird noch ein <meta>
-Element mit dem charset
-Attribut hinzugefügt. Der Operator +=
fügt ein HTML-Element als neues Kindelement in ein anderes HTML-Element ein, in diesem Fall die Rückgaben der Funktionsaufrufe heading_dom
und unordered_list_dom
. Das erzeugte Dokument sieht mit print
ausgegeben wie folgt aus:
<!DOCTYPE html>
<html>
<head>
<title>Todo</title>
<meta charset="utf-8">
</head>
<body>
<h1>Todo-Liste</h1>
<ul>
<li>
<a href="#essen">essen</a>
</li>
<li>
<a href="#lesen">lesen</a>
</li>
</ul>
</body>
</html>
Um ein Python-Programm zu schreiben, das HTTP-Anfragen mit generiertem HTML-Quelltext beantwortet, verwenden wir das Webentwicklungs-Framework bottle
.
Nachdem wir das Paket installiert haben1 und es mit from bottle import *
in unser Programm eingebunden haben, sorgt die folgende Anweisung dafür, dass Anfragen an den gestarteten Server mit der oben definierten Seite beantwortet werden, wenn der Pfad der zugehörigen URL /
ist (also auf das Wurzelverzeichnis bzw. die Homepage der Webanwendung zugegriffen wird).
@get('/')
def get_index():
return 'Hallo!'
Die Zeile @get('/')
sorgt dafür, dass das Programm HTTP-GET-Anfragen an den Pfad /
beantwortet.2 Bei jeder solchen Anfrage wird die Funktion ausgeführt, die direkt nach der Zeile @get
definiert ist. In diesem Fall ist das die Funktion get_index
(der Funktionsname kann beliebig gewählt werden), die als Ergebnis den Text “Hallo!” zurückgibt. Dieses Ergebnis des Funktionsaufrufs wird als Antwort an den Client geschickt, der die HTTP-GET-Anfrage gestellt hat.
Die so definierten Funktionen zum Beantworten von HTTP-Anfragen werden im Folgenden als Rückruffunktionen (engl. request callback) bezeichnet.
In einer Webanwendung sind üblicherweise mehrere solcher Funktionen mit @get
mit verschiedenen Pfaden definiert. Bei einer Anfrage an eine URL wird die Rückruffunktion mit dem passenden Pfad aufgerufen, sofern eine vorhanden ist. Anderenfalls wird vom Server eine Fehlerseite als Antwort zurückgegeben (HTTP-Fehlercode 404: “Nicht gefunden”).
Im obigen Beispiel werden einfache Textdaten als Ergebnis zurückgegeben. Dieser Text kann natürlich auch HTML-Quelltext sein, der von der Rückruffunktion zusammengestellt wird (z. B. mit der Funktion todo_page()
aus den obigen Beispielen) oder aus einer Datei gelesen wird.
Um den HTTP-Server zu starten, muss am Ende des Programms noch die Funktion run
aufgerufen werden.
run(host='localhost', port=8080)
In diesem Beispiel wird über die beiden benannten Argumente host
und port
angegeben, dass der HTTP-Server nur lokal erreichbar ist und über den Port 8080 Nachrichten empfängt.
Wir können das Programm wie folgt im Terminal starten, wenn wir es in einer Datei server.py
abspeichern:
$ python server.py
Dieser Aufruf startet einen HTTP-Server auf Port 8080. Wir können also in einem Webbrowser über die URL localhost:8080
eine Anfrage stellen und bekommen dann die Todo-Liste angezeigt.3
Während der Entwicklung von Webanwendungen empfehlen wir, den Bottle-Server im Debug-Modus mit “Auto Reloading” zu starten: In diesem Modus startet der Server im laufenden Betrieb automatisch neu, wenn der Quellcode des Programms geändert und gespeichert wird, stellt umfangreichere Informationen zum Debugging (im Terminal-Log und in Fehlermeldungen) bereit, und deaktiviert Optimierungen, die während der Entwicklung störend sein können (z. B. Caching). Dazu wird das Programm mit dem folgenden Aufruf gestartet:
run(host='localhost', port=8080, debug=True, reloader=True)
Statt eines Strings, der HTML beschreibt, kann eine mit @get
gekennzeichnete Funktion auf eine Anfrage auch eine statische Datei als Antwort zurückschicken, die auf dem Server liegt. Dazu wird die Funktion static_file
aus dem Bottle-Modul aufgerufen, die einen Dateinamen und einen lokalen Ordnernamen als Argumente erwartet:
@get('/help')
def get_help():
return static_file('help.html', 'static')
Nun liefert eine GET-Anfrage an den Pfad /help
die HTML-Datei help.html als Antwort zurück, die im Ordner static (relativ zur Programmdatei) liegt.
Diese Methode lässt sich auch verwenden, um statische Dateien wie Bilder oder CSS-Dateien (Stylesheets) vom Server abzurufen:
@get('/static/logo.png')
def get_logo():
return static_file('logo.png', 'static')
Es lassen sich auch Funktionen definieren, die nicht nur auf Anfragen an einen konkreten Pfad wie /
, /help
oder /static/logo.png
antworten, sondern auf Pfade mit Platzhaltern. Solche Platzhalter werden im Pfad von @get
in spitzen Klammern geschrieben und der dazugehörigen Rückruffunktion als Funktionsparameter hinzugefügt:
@get('/greeting/<name>')
def get_greeting(name):
return 'Hallo, ' + name + '!'
Beim Aufruf der Funktion wird der konkrete Wert des Platzhalters als Argument übergeben und kann so im Funktionsrumpf verarbeitet werden. Hier wird jede GET-Anfrage an Pfade der Form /greeting/
Zeichenkette durch die oben definierte Funktion beantwortet, wobei die Parametervariable name
die Zeichenkette, die auf /greeting/
folgt, als Wert hat. Eine Anfrage an den Pfad /greeting/Alice
liefert beispielsweise als Antwort den Text “Hallo, Alice!”.
Das ist besonders nützlich, um statische Dateien mit beliebigen Dateinamen abrufbar zu machen:
@get('/static/<filename>')
def get_static_file(filename):
return static_file(filename, 'static')
Hier wird jede GET-Anfrage an Pfade der Form /static/
Dateiname durch die oben definierte Funktion beantwortet, indem die Datei namens Dateiname aus dem lokalen Ordner static zurückgeschickt wird.
Die so definierte Rückruffunktion get_static_file
erlaubt es uns, dass sich alle Dateien im Ordner static vom Webserver abrufen lassen, ohne dass wir für jede Datei eine separate Rückruffunktionen definieren müssen. Das ist beispielsweise hilfreich, wenn unsere Webanwendung viele Bilder enthält, die wir so einfach nur zum Ordner static hinzufügen müssen. Mit dem HTML-Element <a src="/static/
Dateiname">
wird das Bild dargestellt.
Viele Webentwicklung-Framework stellen die Funktion bereit, HTML-Seiten dynamisch auf Grundlage von HTML-Vorlagen, sogenannten Templates zu erstellen. Ein HTML-Template in Bottle ist eine Datei, die neben HTML auch Python-Ausdrücke enthalten kann. Diese Datei wird nicht direkt zurückgegeben, sondern zuerst werden alle enthaltenen Ausdrücke ausgewertet und durch die Ergebnisse ersetzt. Dieser Prozess, aus dem Template den endgültigen HTML-Text zu erstellen, wird als “Rendering” bezeichnet.
Ein HTML-Template in Bottle kann außerdem sogar Python-Anweisungen enthalten, die beim Rendering ausgewertet werden – beispielsweise eine for
-Wiederholung, um Teile des Templates mehrmals nacheinander zu rendern.
Diese Vorgehensweise bietet mehrere Vorteile gegenüber dem manuellen Zusammenstellen der HTML-Texte im Python-Code (wie oben): Zu einen sehen die HTML-Templates zu großen Teilen bereits aus wie der fertige HTML-Quellcode und lassen sich daher einfacher entwickeln. Zum anderen können wir so den Python-Code der Webanwendung und den HTML-Quellcode der Benutzeroberfläche in verschiedene Dateien aufteilen.
Zur Demonstration schreiben wir zuerst ein HTML-Template, das unsere Todo-Liste aus dem obigen Beispiel beschreibt:
<!DOCTYPE html>
<html>
<head>
<title>Todo</title>
<meta charset="utf-8">
</head>
<body>
<h1>{{title}}</h1>
<ul>
% for item in items:
<li>{{item}}</li>
% end
</ul>
</body>
</html>
Bottle erwartet, dass die Template-Dateien im selben Verzeichnis liegen wie das Server-Programm oder in einem Unterordner namens views. Der Übersichtlichkeit halber legen wir einen neuen Ordner namens views in dem Verzeichnis an, in dem unsere Datei server.py liegt, und speichern das HTML-Template in einer Datei namens index.tpl im Ordner views. Als Dateiendung für HTML-Templates ist .tpl oder .html üblich. Wir verwenden im Folgenden .tpl zur Unterscheidung der Templates von reinen HTML-Dateien.
Bis auf zwei Besonderheiten sieht das Template aus wie regulärer HTML-Quellcode: Wir erkennen zum einen eingebettete Python-Ausdrücke title
und item
, sowie eine Wiederholung mit for
, die zur Unterscheidung vom HTML-Quellcode mit bestimmten Sonderzeichen markiert werden ({{
… }}
um den Ausdruck bzw. %
am Zeilenanfang der Anweisung), worauf wir später detaillierter eingehen werden.
Das Python-Programm server.py passen wir nun wie folgt an:
from bottle import *
todo_list = ['essen', 'lesen']
@get('/')
def get_index():
return template('index.tpl', title='Todo-Liste', items=todo_list)
run(host='localhost', port=8080)
Der Aufruf der Funktion template
aus dem Bottle-Modul “rendert” die angegebene Template-Datei (hier index.tpl) und schickt den resultierenden HTML-Text als Antwort zurück.Über zusätzliche benannte Argumente (hier: title='Todo-Liste'
und items=todo_list
) können Variablen definiert werden, die beim Rendering verwendet werden, um Ausdrücke im Template auszuwerten. Auf diese Weise lassen sich dynamische Inhalte in die Seite einbauen.
Um einen Python-Ausdruck in ein Template einzubetten, wird dieser in doppelten geschweiften Klammern geschrieben, z. B.:
<h1>{{title}}</h1>
Hier wird der Text im Absatz aus der Variablen title
übernommen. Diese Variable muss entweder innerhalb des Templates definiert werden oder dem Template beim Aufruf der template
-Funktion als benanntes Argument mitgegeben werden, z. B.:
@get('/')
def get_index():
return template('index.tpl', title='Todo-Liste', items=todo_list)
Beim Rendern des HTML-Templates wird der Ausdruck ausgewertet, hier also durch den Text “Todo-Liste” ersetzt, da der Variablen title
beim Aufruf die Zeichenkette 'Todo-Liste'
als Wert zugewiesen wird.
Dabei werden alle Sonderzeichen im Auswertungsergebnis HTML-spezifisch so umgewandelt, dass die Überschrift später im Browser genau so aussieht, wie die Zeichenkette, die wir übergeben haben.
Um Python-Anweisungen in einem Template anzugeben, muss die entsprechende Zeile mit dem Zeichen %
beginnen. Hier wird z. B. eine Variable num
innerhalb des Templates definiert und anschließend in einem eingebetteten Ausdruck verwendet:
% num = len(items)
<p>Die Liste enthält {{num}} Elemente.</p>
Auf diese Weise lassen sich auch Kontrollstrukturen angeben, mit denen Teile des HTML-Templates mehrmals wiederholt vorkommen (while
, for
) oder in Abhängigkeit von einer Fallunterscheidung vorkommen oder weggelassen werden (if
, elif
, else
).
Ein typischer Anwendungsfall ist, dass wir für jedes Element einer Liste ein HTML-Element erzeugen möchten. Das folgende Beispiel erzeugt für jedes Element der Liste items
einen Listeneintrag mit dem entsprechenden Element als Text:
<ul>
% for item in items:
<li>{{item}}</li>
% end
</ul>
Wenn beim Aufruf des Templates die Liste items=['eins', 'zwei', 'drei']
übergeben wurde, sieht das gerenderte Ergebnis folgendermaßen aus:
<ul>
<li>eins</li>
<li>zwei</li>
<li>drei</li>
</ul>
Wir können hier zusätzlich eine Fallunterscheidung im Template verwenden, um statt der HTML-Liste einen Warnhinweis darzustellen, falls die Liste items
keine Elemente enthält:
% if len(items) == 0:
<p>Die Liste ist leer.</p>
% else:
<ul>
% for item in items:
<li>{{item}}</li>
% end
</ul>
% end
Nun wird entweder der Teil <p>Die Liste ist leer.</p>
oder die Liste <ul> ... </ul>
gerendert, je nachdem ob die beim Aufruf des Templates übergebene Liste items
leer ist oder nicht.
Im Gegensatz zu “echtem” Python-Code wird bei den Python-Anweisungen in den HTML-Templates die Einrückung ignoriert. Damit trotzdem klar ist, wo ein Block endet, muss jeder Block hier explizit mit der Zeile % end
abgeschlossen werden. Es empfiehlt sich aber der Übersichtlichkeit halber, Blöcke trotzdem wie gewohnt einzurücken.
HTML-Templates müssen nicht unbedingt vollständige HTML-Seiten enthalten, sondern können auch HTML-Teile enthalten, die sich in andere Templates einbetten lassen. Die folgenden Zeile in einem Template fügt beispielsweise an dieser Stelle den Inhalt aus einem anderen Template ein:
% include('sub.tpl')
Der Aufruf der speziellen Funktion include
innerhalb eines Templates rendert das angegebene Untertemplate (hier sub.tpl) und fügt das Ergebnis an dieser Stelle in das aktuelle Template ein. Optional können hier (wie beim Aufruf der Funktion template
) benannte Argumente angegeben werden, die als Variablen im Untertemplate vorkommen.
Auf diese Weise lassen sich Templates auf mehrere Dateien aufteilen, was die Übersichtlichkeit der einzelnen Template-Dateien erleichtert. Außerdem ist es so möglich, mit Hilfe von Templates wiederverwendbare Komponenten zu definieren. Wir können beispielsweise ein Template card.tpl schreiben, das den HTML-Code für eine “Karten”-Komponente mit einem Titel und Text enthält (hier mit CSS gestaltet):
<div style="border: 1px solid black; border-radius: 0.5rem; padding: 1rem;">
<h5>{{card_title}}</h5>
<p>{{card_text}}}}</p>
</div>
Diese Komponente kann nun mittels include
in anderen Templates als Baustein verwendet werden. Dem Aufruf der Funktion include
müssen hier noch die im inkludierten Template zusätzlich verwendeten Variablen (hier: card_title
und card_text
) als benannte Argumente übergeben werden, z. B.:
% include('card.tpl', card_title='Aufgabe #1', card_text='HTML-Templates in Komponenten aufteilen')
Andersherum kann sich ein Template auch selbst in ein anderes Template einbetten, sich also mit dem Inhalt eines anderen Templates “umgeben”. Dazu wird zu Beginn des Templates die spezielle Funktion rebase
aufgerufen:
% rebase('super.tpl')
Beim Rendern des Templates wird nun auch das angegebene äußere Template (hier super.tpl) gerendert, und das Template an einer bestimmten Stelle dort eingefügt – nämlich dort, wo im äußeren Template {{!base}}
steht. Genauer gesagt: Nachdem das Template selbst gerendert wurde, wird das Ergebnis nicht sofort zurückgegeben, sondern dem angegebenen äußeren Template (hier super.tpl) in einer Variablen namens base
übergeben. Das äußere Templates wird nun gerendet (wobei der eingebettete Ausdruck {{!base}}
durch das Ergebnis des inneren Templates ersetzt wird) und liefert das Ergebnis.
Der Standard-Verwendungszweck für rebase
besteht darin, das HTML-Grundgerüst, das in jeder HTML-Seite der Webanwendung gleich ist, nur einmal in einem Template festzulegen und in allen anderen Templates nur noch den eigentlichen Seiteninhalt – das folgende Beispiel sollte diese Vorgehensweise klarer machen:
Wir schreiben eine Template-Datei namens base.tpl
mit dem HTML-Grundgerüst als Inhalt:
<!DOCTYPE html>
<html>
<head>
<title>Todo</title>
<meta charset="utf-8">
</head>
<body>
{{!base}}
</body>
</html>
Nun können wir in allen Templates mit Seiteninhalten das Grundgerüst weglassen und verwenden stattdessen rebase
, um anzugeben, dass der äußere HTML-Code aus dem Template base.tpl
übernommen werden soll. Der Inhalt der Templates selbst wird an der Stelle {{!base}}
eingefügt. (Das Ausrufezeichen ist hier notwendig, damit der eingefügte HTML-Code unverändert bleibt und Sonderzeichen nicht durch die entsprechenden HTML-Escape-Sequenzen ersetzt werden, z. B. <h1>
durch <h1>
.)
% rebase('base.tpl')
<h1>{{title}}</h1>
<ul>
% for item in items:
<li>{{item}}</li>
% end
</ul>
Wie bei include
können beim Aufruf von rebase
auch Variablen zum Rendern des äußeren Templates als benannte Argumente angegeben werden. Soll beispielsweise der Dokumententitel für jede Seite unserer Webanwendung unterschiedlich sein, ersetzen wir die entsprechende Zeile im Grundgerüst base.tpl durch:
<title>{{doc_title}}</title>
und geben beim Aufruf von rebase
den Dokumententitel mit an, der zum Rendern der Seite verwendet werden soll, z. B.:
% rebase('base.tpl', doc_title='Todo - Details')
Im Kapitel HTML-Formulare für Benutzereingaben haben wir Formulare in HTML kennengelernt. Im Folgenden beschäftigen wir uns damit, wie sich Formulardaten serverseitig in Bottle verarbeiten lassen.
Zur Erinnerung: HTML-Formulare werden durch form
-Bereiche definiert, die Eingabeelemente enthalten. Das Element input
mit type="text"
beschreibt ein Texteingabefeld. Ein Formular enthält in der Regel einen Button (Element button
oder input
mit type="submit"
) zum Abschicken der Formulardaten:
<form action="/newitem" method="post">
<label>Beschreibung</label>
<input type="text" name="itemtext">
<button type="submit">Speichern</button>
</form>
Wird ein HTML-Formular abgeschickt, werden die im Formular eingegebenen Werte als Anfrageparameter mit der HTTP-Anfrage an den Webserver mitgeschickt. Die Parameternamen entsprechen den Namen der Formular-Eingabeelemente (name
-Attribut). Ziel der Anfrage ist die URL bzw. der Pfad, der im action
-Attribut angegeben ist. Das Attribut method
legt hier fest, dass eine POST-Anfrage (statt einer GET-Anfrage) verschickt wird.
In der Rückruffunktion (mit @post
markiert) für diesen Pfad können wir über das Objekt request.params
auf die Anfrageparameter zugreifen:
@post('/newitem')
def save_new_item():
new_todo = request.params.itemtext
todo_list.append(new_todo)
return template('index.tpl', title='Todo-Liste', items=todo_list)
Hier lesen wir den Wert des Texteingabefeldes und fügen ihn als neuen Eintrag in die Todo-Liste ein. Als Antwort wird die HTML-Seite mit der Todo-Liste zurückgegeben.
Das Objekt request.params
enthält für jeden Anfrageparameter eine gleichnamige Attributvariable, auf die wir mit der Punkt-Schreibweise zugreifen können. Mit request.params.itemtext
können wir also den Wert des Texteingabefeldes namens “itemtext” (im HTML-Formular mit name="itemtext"
definiert) abfragen, der mit der Anfrage übermittelt wurde.
Falls es keinen Anfrageparameter mit dem angegebenen Namen geben sollte, wird die leere Zeichenkette ''
als Wert zurückgegeben. Der zurückgegebene Wert ist immer eine Zeichenkette, unabhängig davon, ob Text oder Zahlenwerte in das Formular eingegeben wurden. Gegebenenfalls muss er also noch mit int
oder float
in einen Zahlenwert umgewandelt werden.
Auch die Werte von Auswahllisten, Radiobuttons und Checkboxen lassen sich auf diese Weise lesen. Bei all diesen Eingabeelementen gilt, dass im HTML-Quellcode ein Parametername mit dem Attribut name
für das Element festgelegt wird, der innerhalb des Formulars eindeutig ist. Mit dem Attribut value
wird der Wert festgelegt, den der Anfrageparameter beim Abschicken des Formulars erhalten soll.
Auswahllisten haben einen Namen und je einen Wert für jede Auswahloption:
<select name="status">
<option value="open">offen</option>
<option value="doing">in Bearbeitung</option>
<option value="done">erledigt</option>
</select>
Radiobuttons stellen eine Alternative zur Auswahlliste dar:
<input type="radio" name="status" value="open"> offen
<input type="radio" name="status" value="doing"> in Bearbeitung
<input type="radio" name="status" value="done"> erledigt
Wenn das Formular abgeschickt wird, hat der Anfrageparameter request.params.status
entweder den Wert 'open'
, 'doing'
oder 'done'
, je nachdem, welche Option beim Abschicken ausgewählt war, oder ''
, falls keine Option ausgewählt war.
Bei einer Checkbox wird meistens nur der Name festgelegt, das Attribut value
ist hier optional (wenn es fehlt wird standardmäßig der Wert “on” verwendet):
<input type="checkbox" name="urgent"> dringend
Wenn das Formular abgeschickt wird, hat der Anfrageparameter request.params.urgent
den Wert 'on'
(oder den mit value
angegebenen Wert), falls die Checkbox angekreuzt war, sonst ''
.
Wird das boolesche Attribut required
(“benötigt”) zu einem Eingabefeld hinzugefügt, wird beim Abschicken des Formulars über den Submit-Button in der Regel client-seitig durch den Webbrowser überprüft, ob das Feld leer. Ist das der Fall, wird ein Warnhinweis angezeigt und das Formular nicht abgeschickt.
<input type="text" name="itemtext" required>
Während type="text"
für allgemeine Texteingaben verwendet wird, lassen sich auch Eingabefelder für spezielle Datentypen beschreiben, die meistens ein anderes Erscheinungsbild haben. Die geläufigsten Typen sind:
type="password"
: Password (eingegebene Zeichen werden als • dargestellt)type="number"
: Ganzzahl (Minimum und Maximum lassen sich mit zusätzlichen Attributen min
und max
angeben)type="date"
: Datum (Eingabe über kleinen Kalender möglich)type="email"
: E-Mail-AdresseDie eingegebenen Werte werden in der Regel außerdem client-seitig auf korrekte Syntax überprüft, bevor das Formular abgeschickt wird. Die Eingabe von “keine Angabe” in ein Feld vom Typ “E-Mail” führt etwa zu einer Warnung, da die Eingabe keine syntaktisch korrekte E-Mail-Adresse darstellt. Da diese Validierung allerdings nur client-seitig stattfindet, nicht von jedem Webbrowser unterstützt wird und leicht manipuliert werden kann, sollte die Serveranwendung sich nicht unbedingt darauf verlassen, dass die empfangenen Daten das richtige Format haben, und das Format sicherheitshalber auch server-seitig überprüfen.
Ber der Webprogrammierung mit Formularen können einige Fehler auftreten, die oft auf Inkosistenzen zwischen den Informationen im HTML-Quellcode und im Python-Code beruhen. Die folgende Tabelle listet typische Fehler auf und bietet Lösungen an:
Problem | Lösung |
---|---|
Der Anfrageparameter request.params.xyz in der Rückruffunktion ist leer, obwohl ich etwas in das Formularfeld eingegeben habe. | Stimmt der Parametername “xyz” mit dem name -Attribut des Formularelements überein?Stimmt die Route der Rückruffunktion, in der die Abfrage stattfindet, mit dem action -Attribut des Formulars überein? |
Wenn ich das Formular abschicke, erhalte ich die Fehlermeldung “405 Method Not Allowed”. | Stimmt die HTTP-Methode der Rückruffunktion (@get oder @post ) mit der Methode des Formulars (action="get" oder "post" ) überein? |
Obwohl ich in das Formular eine Zahl eingebe / ein Zahlen-Eingabefeld <input type="number" name="xyz"> nutze, liefert der Abfrageparameter request.params.xyz eine Zeichenkette zurück. | Anfrageparameterwerte sind immer Zeichenketten. Verwende die Funktion int oder float , um sie in der Rückruffunktion in eine Zahl umzuwandeln. |
Es treten seltsame Zeichen auf: Wenn ich im Formular beispielsweise „Tschüß“ eingebe, erhalte ich in der Rückruffunktion die Zeichenkette „Tschü÷“. | Anfrageparameter sollten immer mit request.params.xyz oder request.params.getunicode('xyz') abgefragt werden, nicht mit request.params.get('xyz') oder request.params['xyz'] – was auch möglich ist, aber ggf. eine andere Zeichencodierung verwendet, als von Python angenommen. |
In diesem Abschnitt finden Sie jeweils zwei Versionen des Python-Codes: In der ersten Variante werden SQL-Anfragen direkt mit sqlite3
gestellt. Die zweite Variante basiert auf den Datenbank-Klassen, die wir im Kapitel Datenbankprogrammierung in Python entworfen haben, um SQL-Anfragen zu kapseln.
Wir wollen nun eine Webanwendung schreiben, die es erlaubt, auf unsere im vorigen Kapitel entwickelte Filmdatenbank zuzugreifen. Zur Erinnerung: Diese Datenbank enthält drei Tabellen (siehe auch untenstehendes Diagramm):
Movie
hat die Attribute id
, title
, year
und directed_by
, wobei das letzte Attribut einen Fremdschlüssel in die Tabelle Person
darstellt (beschreibt die Person, die im Film Regie führt).Person
hat die Attribute id
und name
.Is_Actor_In
hat die Attribute id
, person
und movie
, wobei die beiden letzten Attribute Fremdschlüssel in die entsprechenden Tabellen darstellen (beschreibt, dass eine Person in einem Film mitspielt).Unser Hauptprogramm server.py
beginnt wie folgt:
from bottle import *
import sqlite3
db = sqlite3.connect('movies.db')
from bottle import *
from database_sqlite import Db
db = Db('movies.db')
Zum Zugriff auf die Datenbank verwenden wir die früher entwickelte Datei database_sqlite.py
mit Hilfsobjekten zum Zugriff auf SQLite-Datenbanken.
In der Variablen db
wird ein Objekt zum Zugriff auf die Filmdatenbank erzeugt, das wir später verwenden, um Anfragen an den Server zu beantworten. Anfragen an den Wurzelpfad beantwortet unsere Anwendung mit einem Link zur Liste aller Filme.
@get('/')
def get_index():
return template('index')
Das Template index.tpl sieht folgendermaßen aus (das Template base.tpl mit dem Grundgerüst ist wie wie oben definiert):
% rebase('base.tpl')
<h1>Filmdatenbank</h1>
<p><a href="/movies">zur Liste aller Filme</a></p>
Damit HTTP-GET-Anfragen an den Pfad /movies
von unserer Anwendung beantwortet werden, fügen wir ihr den folgenden Aufruf hinzu.
@get('/movies')
def get_movies():
cur = db.execute('SELECT * FROM Movie;')
all_movies = cur.fetchall()
return template('movie_list', movies=all_movies)
@get('/movies')
def get_movies():
all_movies = db.table('Movie').all()
return template('movie_list', movies=all_movies)
Das Template movie_list.tpl stellt die Liste der Filmdatensätze dar, die von der Datenbank abgefragt und beim Rendern des Templates über die Variable movies
übergeben werden:
% rebase('base.tpl')
<h1>Alle Filme</h1>
<ul>
% for movie in movies:
<li>
<a href="/movies/{{movie['id']}}">{{movie['title']}} ({{movie['year']}})</a>
% include('delete_button', movie_id=movie['id'])
</li>
% end
</ul>
<h3>Neuen Film hinzufügen</h3>
<form action="/movies" method="post">
<input type="text" name="title" placeholder="Titel">
<input type="text" name="directed_by" placeholder="Regisseur/in">
<input type="number" name="year">
<input type="submit" value="Speichern">
</form>
Jeder Eintrag ist verlinkt zu einer Detail-Ansicht für Filme, deren Pfad die id
des entsprechenden Films enthält. Zusätzlich wird hinter jedem Eintrag ein Knopf zum Löschen eingefügt, der im Untertemplate delete_button.tpl beschrieben wird, das wir später besprechen. Am Ende der Film-Liste definieren wir ein Formular zur Eingabe der Stammdaten eines neuen Filmes.
Der Link zur Detailansicht, der Knopf zum Löschen und das Formular zum Anlegen neuer Filme lösen alle neue HTTP-Anfragen aus, die wir mit unserem Programm beantworten müssen. Zunächst sehen wir uns die GET-Anfrage zur Detailansicht von Filmen an.
@get('/movies/<movie_id>')
def get_movie(movie_id):
cur = db.execute('SELECT * FROM Movie WHERE id = ?;', [movie_id])
movie = cur.fetchone()
cur = db.execute('SELECT * FROM Person WHERE id = ?;', [movie['directed_by']])
movie['directed_by'] = cur.fetchone()
movie['actors'] = db_get_movie_actors(movie)
return template('movie_details', movie=movie)
@get('/movies/<movie_id>')
def get_movie(movie_id):
movie = db.table('Movie').get(movie_id)
movie['directed_by'] = db.table('Person').get(movie['directed_by'])
movie['actors'] = db_get_movie_actors(movie)
return template('movie_details', movie=movie)
Hier steht im Pfad ein sogenannter Platzhalter <movie_id>
. Dieses sogenannte Pfad-Muster passt also auf viele verschiedene Pfade. Die übergebene id
wird der behandelnden Funktion als Variable movie_id
übergeben. Diese Funktion erzeugt zunächst ein Dictionary movie
, das Daten enthält, die später in der Detailansicht angezeigt werden sollen. Der Datensatz, der aus der Tabelle Movie
gelesen wurden, wird dazu durch weitere Anfragen um Person
-Datensätze für Regisseur/in und Schauspieler/innen erweitert.
Das Template movie_details.tpl stellt die Information des übergebenen Film-Datensatzes dar:
% rebase('base.tpl')
<h1>{{movie['title']}} ({{movie['year']}})</h1>
<p>von: {{movie['directed_by']['name']}}</p>
<p>mit:</p>
<ul>
% for actor in movie['actors']:
<li>{{actor['name']}}</li>
% end
</ul>
<p><a href="/movies">zurück zur Filmliste</a></p>
Die Funktion db_get_movie_actors
liefert eine Liste von Person
-Datensätzen der Schauspieler/innen eines Films:
def db_get_movie_actors(movie):
actors = []
cur = db.execute('SELECT * FROM Is_Actor_In WHERE movie = ?;', [movie['id']])
for row in cur.fetchall():
cur = db.execute('SELECT * FROM Person WHERE id = ?;', [row['person']])
actors.append(cur.fetchone())
return actors
def db_get_movie_actors(movie):
actors = []
for row in db.table('Is_Actor_In').all_where('movie = ?', [movie['id']]):
actors.append(db.table('Person').get(row['person']))
return actors
Neben Regisseur/in und einer Liste von Schauspieler/innen zeigt diese Seite auch einen Link an, der auf die Liste aller Filme zurückverweist.
Das Formular versendet hier keine GET-Anfrage, wenn der Submit-Button gedrückt wird, sondern eine POST-Anfrage. Die HTTP-POST-Methode sollte immer dann verwendet werden, wenn eine Anfrage Daten serverseitig ändern kann, während GET nur bei rein lesenden Anfragen verwendet wird.
Wir legen nun fest, wie die POST-Anfrage zum Hinzufügen von Filmen verarbeitet wird:
@post('/movies')
def post_movies():
dir_id = db_get_or_insert_person(request.params.directed_by)
db.execute('INSERT INTO Movie (title, year, directed_by) VALUES (?, ?, ?);',
[request.params.title, int(request.params.year), dir_id])
db.commit()
redirect('/movies')
Hierzu definieren wir noch eine Hilfsfunktion, mit der wir die ID einer Person über ihren Namen abfragen und die Person dabei in die Datenbank eintragen, wenn noch kein Eintrag mit diesem Namen vorhanden ist:
def db_get_or_insert_person(name):
# ID der Person mit dem Namen abfragen
cur = db.execute('SELECT id FROM Person WHERE name = ?;', [name])
row = cur.fetchone()
# Neuen Eintrag für Person hinzufügen, falls Abfrage ohne Ergebnis ist
if row == None:
db.execute('INSERT INTO Person (name) VALUES (?);', [name])
db.commit()
# ID nach dem Eintragen erneut abfragen
cur = db.execute('SELECT id FROM Person WHERE name = ?;', [name])
row = cur.fetchone()
return row['id']
@post('/movies')
def post_movies():
dir_id = db.table('Person').insert({'name': request.params.directed_by})
movie = {
'title': request.params.title,
'year': int(request.params.year),
'directed_by': dir_id
}
db.table('Movie').insert(movie)
redirect('/movies')
Zur Erinnerung: Die insert
-Methode unserer Datenbank-Klasse fügt nur dann einen neuen Datensatz ein, falls noch kein identischer Datensatz vorhanden ist. Sie liefert die id
des vorhandenen oder eingefügten Datensatzes zurück.
Die entsprechende Funktion ist mit @post
gekennzeichnet statt mit @get
. Ihr Aufruf erzeugt zunächst einen neuen Datensatz aus den Formulareingaben, die als Anfrageparameter mit der POST-Anfrage übertragen wurden. Diese sind in Bottle-Anwendungen über das Objekt request.params
verfügbar, das für jeden Anfrageparameter ein gleichnamiges Attribut besitzt. Auf die Attribute kann mit der Punkt-Schreibweise zugegriffen werden, z. B. request.params.directed_by
für den Namen der regieführenden Person aus dem Formular. Wir sorgen anschließend dafür, dass ein entsprechender Datensatz in der Person
-Tabelle vorhanden ist. Dazu fragen wir die id
eine evtl. bereits vorhandenen Eintrag ab und fügen den Eintrag nachträglich hinzu, falls dieser Datensatz noch nicht existiert. Die id
der Person verwenden wir dann als Attributwert des Fremdschlüssels directed_by
im Datensatz für die Movie
-Tabelle.
Nachdem der so erzeugte Datensatz in die Movie
-Tabelle eingefügt wurde, senden wir mit dem Funktionsaufruf redirect
als Antwort an den HTTP-Client eine sogenannte Weiterleitungs-Antwort. Diese fordert den Client auf, eine neue GET-Anfrage an die mitgegebene URL zu stellen. Dadurch wird hier wieder die Seite mit der Liste aller Filme aufgerufen, die nun den neu hinzugefügten Film anzeigt.
Schließlich diskutieren wir noch wie der Knopf zum Löschen von Filmen erzeugt und die zugehörige Anfrage behandelt wird. Das Untertemplate delete_button.tpl beschreibt einen Knopf mit einer bestimmten Beschriftung, mit dem eine POST-Anfrage an eine bestimmte URL zum Löschen eines Films gesendet werden kann. Die ID des Films wird über ein verstecktes Eingabefeld als Anfrageparameter mitgeschickt.
<form action="/movies/delete" method="post" style="display: inline">
<input type="hidden" name="movie_id" value="{{movie_id}}">
<input type="submit" value="Löschen">
</form>
Wir reagieren darauf wie in der folgenden Funktion, die mit @post
gekennzeichnet ist, definiert ist.[^httpdelete]
[^httpdelete] Es gibt in HTTP neben GET und POST auch DELETE-Anfragen, deren Zweck eigentlich das Löschen von Daten auf dem Server ist. Da Webbrowser ohne Javascript aber nur GET- und POST-Anfragen senden können, werden oft (wie hier auch) POST-Anfragen statt DELETE-Anfragen verwendet. Wenn wir stattdessen tatsächlich auf eine DELETE-Anfrage reagiert möchten, muss die entsprechende Funktion mit @delete
gekennzeichnet werden statt mit @post
. Eine weitere gängige HTTP-Methode, die von Webbrowsern jedoch wie DELETE nicht direkt unterstützt wird, ist PUT. Diese Methode wird verwendet, um Daten hinter einer URL zu ersetzen (z. B. Stammdaten eines Films ändern). In der Server-Anwendung behandeln wir solche Anfragen durch Funktionen, die mit @put
gekennzeichnet sind.
@post('/movies/delete')
def delete_movies():
movie_id = request.params.movie_id
db.execute('DELETE FROM Is_Actor_In WHERE movie = ?;', [movie_id])
db.execute('DELETE FROM Movie WHERE id = ?;', [movie_id])
db.commit()
redirect('/movies')
@post('/movies/delete')
def delete_movies():
movie_id = request.params.movie_id
db.execute('DELETE FROM Is_Actor_In WHERE movie = ?;', [movie_id])
db.table('Movie').delete(movie_id)
redirect('/movies')
In der Funktion werden zunächst aus der Is_Actor_In
-Tabelle alle Datensätze gelöscht, die auf den zu löschenden Movie
-Datensatz verweisen. Anschließend wird der über movie_id
referenzierte Datensatz aus der Movie
-Tabelle gelöscht.
sqlite3
: https://docs.python.org/3/library/sqlite3.htmlEs reicht aber auch, einfach die Datei bottle.py
von der Projekt-Homepage herunterzuladen und im selben Verzeichnis zu speichern, in der das Python-Programm liegt, in dem wir unsere Webanwendung programmieren. ↩︎
Zur Erinnerung: GET-Anfragen sind die Standard-Anfragen in HTTP und werden u. a. gesendet, wenn ein Hyperlink im Browser angeklickt wird oder eine URL über die Adresszeile des Browsers aufgerufen wird. ↩︎
Soll der HTTP-Server dagegen für andere Rechner im Netzwerk erreichbar sein, muss bei host
die IP-Adresse des Hostrechners, auf dem der Server läuft, angegeben werden (bzw. konkreter: die IP-Adresse seiner Netzwerkschnittstelle). Alternativ kann auch 0.0.0.0 (= alle verfügbaren Netzwerkschnittstellen) angegeben werden. Für port
kann eine beliebige ungenutzte Portnummer verwendet werden. Die Webanwendung ist dann über die URL http://IP-Adresse:Port erreichbar. Wenn Port 80 (= Standard für HTTP) verwendet wird, kann die Portangabe in der URL auch weggelassen werden. ↩︎