Samstag, 30. Juli 2011

Der Microblog

Mehr als Twitter, aber weniger als Blogspot – einen solchen Microblog wünschte ich mir. Nun kann er schon seit einigen Monaten unter http://ruediger-plantiko.net/microblog/ betrachtet werden. Hier will ich meine Ankündigung wahrmachen, ihn kurz zu dokumentieren.

An dem, was ich hier auf den "Blogspot" stelle, habe ich meist längere Zeit laboriert – habe es im Geiste vorbereitet, abgewogen, das Für und Wider bedacht, es dann aufgeschrieben, korrigiert, überarbeitet, ...

Auf der anderen Seite nutze ich meinen Twitter-Account, um schnell zwischendurch einmal etwas in die Welt zu posaunen: Eine Mini-Zustandsmeldung, einen bedenkenswerten Artikel, den ich gerade gelesen habe, einen Link auf ein Stück ausführbaren Code, an dem ich gerade herumexperimentiere, und so weiter. Die Beschränkung auf 140 Zeichen für eine Nachricht ist für diese Art von Zustandsmeldung gerade richtig.

Aber manchmal gibt es auch Gedanken, Themen, Fragen, die sich einfach nicht in 140 Zeichen fassen lassen, sondern ein oder zwei Abschnitte Prosa erfordern, um sie klar zu vermitteln. Ich hatte das Bedürfnis, sie in kurzer Frist niederzulegen - ohne sie erst lange mit mir herumzutragen und zu einem umfangreichen Artikel zu verarbeiten. Genau für diese Zwecke erbaute ich mir meinen persönlichen Microblog. Wenn ich dort - mit einer sprechenden URL - etwas hineinstelle, steht es mir frei, diese URL zusätzlich auch noch zu twittern, um den Inhalt zu verbreiten. Die - durchaus sinnvolle - Begrenzung von Twitter ist im Microblog aufgehoben.

Meine URLs sind rein semantisch aufgebaut und folgen dem Schema "Wer - macht - was". Ein Beispiel:
http://ruediger-plantiko.net/microblog/?eine-bresche-schlagen
Der URL kann man entnehmen, dass Rüdiger Plantiko einen Microblog geschrieben hat, der anscheinend das Thema Eine Bresche schlagen hat. Mehr nicht. Ob der angezeigte Inhalt eine HTML-Seite ist oder einen anderen Inhaltstyp hat, ist nicht erkennbar. Auch nicht, ob für die Anzeige des Blogs CGI oder irgendeine andere Technologie verwendet wird. Statt einer künstliche Artikel-ID (etwa in der Art ?article_id=D0F5C7) nutze ich den Queryteil der URL für eine lesbare Kennzeichnung des Artikels, die nach einer unmittelbar nachvollziehbaren Regel aus der Überschrift gewonnen ist.

Diese URLs werden gültig bleiben - auch wenn es mir irgendwann einmal in den Sinn kommen sollte, das dahinterliegende technische Konzept völlig zu ändern. Egal mit welcher Programmiersprache der Request bedient wird, egal ob eine rein serverseitige Generierung oder eine Mischung aus Client- und Servercode verwendet wird: Die URL kann immer für den Abruf des entsprechenden Inhalts verwendet werden.

Wie aber ist der Microblog aktuell implementiert? Ich habe eine Mischung aus JavaScript und Perl in Form eines "Client-Server-Handshaking" gewählt, die sich auch schon bei früheren Aufgaben bewährt hat. Statt einer Framework-Kanone wie Prototype oder jQuery verwende ich meine eigene minimalistische JavaScript-Bibliothek minlib, vor allem da sie unkomprimiert nur etwa 10 KB benötigt, was sich günstig auf die Ladezeiten auswirkt.

Egal welchen Eintrag meines Microblogs man abruft, wird zunächst eine immer gleiche HTML-Seite an den Browser übergeben, die als Rahmen dient. Nach dem Laden der Seite wird der gewünschte Text mittels eines Ajax-Requests vom Server abgeholt und in den dafür vorgesehenen Inhaltsbereich gestellt.

Wenn ich einen Eintrag erstelle, wird dieser auf dem Server in einer Datei microblog.dat eingefügt. Hier ein Beispieleintrag:
---
title:Eine Bresche schlagen
id:eine-bresche-schlagen
timestamp:20110524205854
content:Beim Entwickeln einer Applikation ist es eine brauchbare
Strategie, zuerst <i>eine Bresche bis zum Ziel zu schlagen</i>:
<ul>
<li>Die für jede Applikation im jeweiligen Konzept benötigten ...
Wie man sieht, beginnt jeder Eintrag mit dem speziellen Trennzeichen ---, das in einer Zeile für sich stehen muss. Ein Eintrag hat vier Attribute, von denen drei in eine Zeile passen und das vierte, der eigentliche Text des Eintrags, von beliebiger Länge ist.

Es hat sich bewährt, bei allen Anwendungen, die mit Ajax operieren, durchgängig mit dem UTF8-Format zu arbeiten. Die Datei microblog.dat ist daher ebenso im UTF8-Format codiert wie die zu sendende Webseite. Sowohl in Perl (mein Provider verwendet aktuell die Version 5.8.4) als auch in JavaScript ist bei Verwendung von UTF8 kein Extra-Code für die Umwandlung der Zeichencodierung nötig.[1]

Das Perl-Script kann nun anhand des Query-Strings zu verschiedenen Aktionen veranlasst werden. Der Query-String kann einerseits die ID eines Topics sein – dann werden die Attribute dieses Topics im JSON-Format angeliefert:
/cgi-bin/microblog.pl?cassian-ueber-die-fressgier
bringt die Antwort:
[
{
id:"cassian-ueber-die-fressgier",
title:"Cassian über die Fressgier",
timestamp:"20110724210423",
content:"Der Mönch Johannes Cassian (ca. 360 - ca. 435) ..."
}
]
Die URL kann statt der ID eines Topics aber auch ein Verb enthalten, um gezielt eine bestimmte Aktion auszuführen. Mit der folgenden URL beispielsweise werden die letzten 5 Topics im JSON-Format beschafft.
/cgi-bin/microblog.pl?last-5
Das obige Datenbeispiel zeigt, dass als Übergabeformat die Datenstruktur eines Arrays of Hashs (AoH) gewählt wurde. In dieser Form lassen sich die Daten auch auf der JavaScript-Ebene leicht verarbeiten und ins HTML-Dokument einmischen.

Da einem Querystring nicht auf Anhieb angesehen werden kann, ob es sich um eine ausführbare Aktion (wie last-5) oder um eine Topic-ID (wie cassian-ueber-die-fressgier) handelt, muss das aus Performancesicht Günstigere zuerst ausprobiert werden: Das Programm prüft zunächst, ob es sich beim Querystring eventuell um den Namen eines Unterprogramms handelt (das ist nämlich nur ein Lookup in der Symboltabelle, die nach dem Parsen des Programms durch den Perl-Interpreter sowieso im Speicher vorliegt). Nur wenn kein Unterprogramm dieses Namens gefunden wird, wird das Argument als Themen-ID interpretiert:
#!/usr/bin/perl
use strict;
use warnings;

print "Content-Type:text/plain\n\n";

_do( $ENV{"QUERY_STRING"} || "get-all" );

# --- Eine Aktion ausführen
sub _do {
my $action = shift;
# Ausführbare Aktion?
if (not _do_action( $action )) {
# Letzter Versuch: Ist es vielleicht eine ID?
_get($action);
}
}
Als Konvention, um Verwechslungen mit Topic-ID's zu vermeiden, lasse ich alle Aktionen = Unterprogramme in diesem Script mit einem Unterstrich beginnen. Das Unterprogramm _do_action führt ein Unterprogramm des im Querystring angegebenen Namens aus, falls es ein solches gibt, und gibt in diesem Fall eine 1 zurück. Wenn es kein solches Unterprogramm gibt, wird 0 zurückgegeben:
# --- Die übergebene Aktion ausführen
sub _do_action {
my $action = shift;
my ($subname,$arg);
# Ein numerisches Argument am Schluss finden, z.B. bei "last-25"
($subname,$arg) = ($action =~ /^([\w-]+?)(?:-(\d+))?$/);
# Hyphens sind netter lesbar als Unterstriche
$subname =~ s/-/_/g;
# Ggf. führenden Unterstrich setzen
$subname =~ s/^(?!_)/_/;
# Unterprogramm dynamisch aufrufen
return _call( $subname, $arg );
}

Für den dynamischen Aufruf eines Unterprogramms ist es nötig, die strenge Syntaxprüfung strict 'refs', die mit use strict; normalerweise eingeschaltet ist, zu deaktivieren. Dies sollte natürlich für einen möglichst kleinen Codeabschnitt geschehen. Das war einer der Gründe, warum ich den tatsächlichen Aufruf des Unterprogramms wieder in einer eigenen Subroutine gekapselt habe.
# --- Unterprogramm dynamisch aufrufen
sub _call {
no strict 'refs';
my ($subname,$arg) = @_;
if (defined &{$subname}) {
&{$subname}($arg);
return 1;
}
return 0;
}

Nun verbleiben wir mit dem Fall, dass der Querystring eine Topic-ID enthält, das heisst mit dem Top-Level-Aufruf des Unterprogramms _get():
# --- Einen einzelnen Post über seine ID zurückgeben
sub _get {
my $id = shift;
my $posts = _select_posts();
my $post = $posts->{$id};
print "[\n";
_print_post( $post ) if $post;
print "]\n";
}

Wenn ich einmal mehr als ca. 500 Einträge im Microblog habe, werde ich den Zugriff auf einen einzelnen Post eventuell optimieren. Bis dahin ist es völlig ausreichend, bei jedem Zugriff die volle Datei microblog.dat als Hash of Hashs einzulesen und den Eintrag mit der gewünschten ID auszugeben, falls ein solcher existiert.

Hier die Routine zum Einlesen:
# --- microblog.dat einlesen und in Hash wandeln
sub _select_posts {
my ($posts, $post, $fields, $content);

local $/;
$/ = "---";
open BLOG, "microblog.dat"
or die "Kann Datei microblog.dat nicht zum Lesen öffnen";
foreach $post (<BLOG>) {
chomp $post;
$fields = {};
# Progressives Matching für die Ausdrücke der Form "Feldname:Feldwert"
# Ausser für "content" enthalten die Feldwerte keine Zeilenumbrüche
while ( $post =~ /(\w+)\s*:\s*(.*?)\s*\n/g ) {
$fields->{$1}=$2;
# Wenn das Feld "content" als nächstes erreicht wird, ...
if ($post=~ /\Gcontent:(.*)/s) {
# dann den ganzen Rest dieses "Records" als Feldwert aufnehmen
$content = $1;
# Anführungszeichen und Umbrüche müssen maskiert werden
$content =~ s/"/\\"/mg;
$content =~ s/\n/\\n/mg;
$fields->{content} = $content;
last;
}
}
$posts->{$fields->{id}} = $fields;
}
close BLOG;
return $posts;
}
Hier finden ein paar nützliche Perl-Idiome Verwendung, aufgrund derer der Code ziemlich kompakt gestaltet werden kann. Indem ich den Record Separator $/, das Trennzeichen zwischen zwei Records, das standardmässig ein Zeilenumbruch ist, für dieses Unterprogramm begrenzt auf --- setze, liefert mir das foreach der Reihe nach die einzelnen Topics an. Pro Topic suche ich nun mittels progressivem Matching nach Paaren der Form Name:Wert, die dann in die innere Hashreferenz $fields aufgenommen werden. Der äussere Hash verwendet danach die ID als Schlüssel und $fields als Wert.

Das Feldwert zum Namen content wird im Gegensatz zu den vorigen nicht durch einen Zeilenumbruch beendet, sondern ist bis zum Ende des Records zu übernehmen. Ich verwende daher innerhalb des progressiven Matchings, nach Zuweisung des aktuellen Namens und Wertes, einen "vorausschauenden" zweiten regulären Ausdruck, der mit der Zusicherung \G für das Ende des aktuellen Treffers schaut, ob der nächste Feldname wohl content ist. In diesem Fall übernimmt der reguläre Ausdruck den ganzen Rest des Records in den Feldwert, nimmt das Feld in den $fields-Hash auf und verlässt die Schleife mit last (denn content ist das letzte Feld im Record, und das muss auch so sein).

Damit sollte die Wirkungsweise des serverseitigen Scripts ausreichend beschrieben sein. Wie ist dieses nun im Client eingebunden? Wie erwähnt, wird nach dem Laden der Seite ein Request an das Perl-Script abgesetzt und das Ergebnis in das HTML-Dokument eingemischt. Hier ist die zuständige doOnLoad()-Funktion:
// --- Post(s) einlesen und anzeigen
function doOnLoad() {
var microblog = byId("microblog");
var queryPart = document.location.href.match(/(\?.+)/) ?
RegExp.$1 : "";
doRequest( "../cgi-bin/microblog.pl"+queryPart, function() {
var p;
eval("p="+this.responseText);
p.each( function() {
microblog.appendChild(createPost(this));
});
if (window.sh_highlightDocument) sh_highlightDocument();
});
}
Die zur Ausführung des Requests verwendete Funktion doRequest() entstammt meiner JavaScript-Bibliothek minlib (Die Verwendung ist unter diesem Link dokumentiert). Der zurückgegebenen Array of Hashs wird in einem each()-Iterator abgearbeitet. Dabei wird aus jedem Topic mittels createPost() ein HTML-Fragment aufgebaut, das dann in den Seitenbereich microblog eingefügt wird. Schliesslich wird explizit noch einmal das Syntax-Highlighting aufgerufen, da der Zeitpunkt onload, zu dem dies normalerweise erfolgt, beim Empfang der Ajax-Antwort ja schon längst verstrichen ist.

Zum Aufbau des HTML-Fragments für einen Topic verwende ich die DOM-API. Es empfiehlt sich, für jedes Teil des Gesamtobjekts eine passend benannte Funktion zu definieren, das macht den Code übersichtlicher und steigert die Wiederverwendbarkeit:
// --- DOM-Fragment für einen Post erzeugen
function createPost( post ) {
var t = createDiv( "post", {id:post.id} );
createLabel( post.id, t );
var title = createDiv( "title", "", t );
createLink( "?"+post.id, post.title, title );
createDiv("content", {innerHTML:formatHTML(post.content) }, t );
createDiv("timestamp", {innerHTML:getPublishedText(post.timestamp) }, t);
return t;
}

// --- Einen <div> erzeugen und in das Element parent einhängen
function createDiv( className, more, parent ) {
var div = newElement( "div", more, parent );
div.className = className;
return div;
}

// --- Einen <a href> erzeugen und in das Element parent einhängen
function createLink( href, content, parent ) {
return newElement( "a", {href:href,innerHTML:content}, parent );
}

// --- Einen Label <a name> erzeugen und in das Element parent einhängen
function createLabel( name, parent ) {
return newElement( "a", {name:name}, parent );
}

// Element mit Attributen erzeugen und in das Element parent einhängen
function newElement( elementName, more, parent ) {
var el = document.createElement( elementName );
if (more) setAttributes( el, more );
if (parent) parent.appendChild( el );
return el;
}
Die hier verwendete Funktion setAttributes() (alle Paare des als zweites Argument übergebenen Hashs in das im ersten Argument übergebene Objekt einsetzen) entstammt meiner Bibliothek minlib – im übrigen wird auf dieser Ebene nur noch die DOM-API verwendet.

Bevor der Text in die Seite gesetzt wird, habe ich mir in der Funktion formatHTML noch die Gelegenheit gegeben, ihn nach bestimmten Regeln zu überarbeiten. So möchte ich beispielsweise, dass Wörter, die mit http://... anfangen, automatisch als Links gesetzt werden. Ausserdem sollen Absätze im Text als HTML-Absätze mit dem Element <p> gerendert werden:
// --- HTML-Code eines Post-Contents überarbeiten
function formatHTML( content ) {
return content.
replace(/\n\n/g,"<p>").
replace( /(\s+|^|>)(https?:\/\/[^\s<]+)/g, '$1<a href="$2">$2</a>' );
}
Weitere Formatierungen sind mir nicht eingefallen. Sollte mir noch etwas in den Sinn kommen, wäre dies die Stelle, um es einzufügen.

Ich sehe nur einen Nachteil dieser Lösung: Menschen, die mit einem Textbrowser wie lynx im Internet unterwegs sind, werden die Inhalte des Microblogs nicht zu sehen bekommen. Das liegt natürlich daran, dass clientseitige Logik zur Datenbeschaffung verwendet wird. Das Problem liesse sich lösen, indem die Logik komplett auf die Serverseite verlagert wird. Ich erlaube mir aber, diesen Punkt bis auf weiteres zu ignorieren – Menschen, die mit einem Textbrowser unterwegs sind, dürften nicht nur an meinem Microblog, sondern überhaupt am Internet wenig Freude haben.[2]


[1] Umgekehrt ist es aber nicht ohne grosse Klimmzüge möglich, Ajax-Requests beispielsweise in der Latin1-Codierung zu versenden und entgegenzunehmen - selbst wenn die umgebende HTML-Seite Latin1-codiert ist.
[2] Womit ich nicht sage, dass Textbrowser grundsätzlich unnütz sind. Sie können sinnvoll sein, um automatisierte Anfragen im Web zu erstellen, z.B. bei einem Wetterdienst, der seine Prognosen immer in einem wohlbestimten Format liefert. Aber auch dafür verwendet man wohl besser eingebaute Funktionen, in Perl zum Beispiel LWP::Simple.

Keine Kommentare :