NerdkramsProjekte
Mit Common Lisp gegen Webmüll

Im November 2015 schrieb ich, der kosten­lo­se Webdienst feed43 sei zum Entmüllen von Websites mit­tels RSS prin­zi­pi­ell geeig­net, ver­schlucke sich aber gele­gent­lich. Das ist auf Dauer ganz schön anstren­gend. Außerdem besteht wie bei allen kosten­lo­sen Webdiensten die Gefahr, dass ich eines Tages ohne ihn aus­kom­men muss. Ich habe mir also selbst eine Alternative ent­wickelt, die auf einem mei­ner Server läuft und deren kor­rek­te Funktionsweise ich im Zweifelsfall also selbst sicher­stel­len kann.

Die Wahl der Programmiersprache

Ein Binärprogramm, zum Beispiel in C++, wäre hier zu unfle­xi­bel, auf Servern bevor­zu­ge ich - schon aus Gründen der Wartbarkeit und damit Sicherheit - rei­ne Scriptsprachen. Wie üblich stand ich also vor der Entscheidung, ob ich mich für Perl, Python oder Common Lisp (absicht­lich ver­ein­fach­te Lisp-Dialekte wie Scheme und Racket kann ich nicht ernst neh­men) ent­schei­den sollte.

Alle drei Sprachen bie­ten her­vor­ra­gen­de HTML- und XML-Bibliotheken, sie sind für ein schnel­les pro­to­typ­ing auch glei­cher­ma­ßen effi­zi­ent zu nut­zen. An Python aller­dings stö­ren mich nach wie vor die feh­len­den Klammern, die ein refac­to­ring wäh­rend des Entwicklungsprozesses nen­nens­wert auf­hal­ten wür­den: Einrückung ist Syntax, Änderungen im Programmablauf las­sen sich also nicht ein­fach mit ein paar Klammern vor­neh­men. Python mag ein­fach sein, aber es nervt. - In einem ersten Entwurf für die­ses Projekt hat­te ich dank der guten sqlite3-Bibliothek recht schnell ein lauf­fä­hi­ges Python-Script, das zumin­dest mei­ne Datenbank ver­wal­ten konn­te (dazu unten mehr), fer­tig­ge­stellt, aber auch unge­zähl­te graue Haare mehr am Körper; auch, weil Python recht geschwät­zig ist: „Explicit is bet­ter than impli­cit“, was pri­ma ist für Leute, die nach Codezeilen bezahlt wer­den, aber ungut für Leute, die gern mög­lichst wenig Zeit ver­schwen­den wür­den. (Es wirkt iro­nisch, dass ich das als Teil einer wort­rei­chen Erklärung für eigent­lich Triviales anmer­ke, nicht?)

Das von mir anson­sten sehr geschätz­te Perl hat die­ses Problem nicht, aller­dings kommt es mir in einem ande­ren Punkt in die Quere: Die Entwicklergruppe hin­ter Mojolicious, der freund­lich­sten Bibliothek zum Verarbeiten von Websites, hält offen­bar nicht beson­ders viel von sta­bi­len APIs, erst Anfang März muss­te ein auf Mojolicious basie­ren­des Webprojekt, das ich aus Gründen im Auge behal­te, plötz­lich nach­bes­sern, was mich ver­un­si­chert hat: Ich möch­te das Werkzeug, wenn es fer­tig ist, wahr­schein­lich gern noch ein paar Jahre lang nut­zen, ohne wegen eines Systemupdates funk­tio­nie­ren­de abstra­hier­te Routinen umschrei­ben zu müssen.

Übrig bleibt also Common Lisp. Das wird lustig.

Die Wahl der Datenbank

Da die Feeds natür­lich nicht nur in Dateiform vor­lie­gen, son­dern zunächst ein­mal gene­riert und gele­gent­lich aktua­li­siert wer­den sol­len, muss eine Liste der zu gene­rie­ren­den Feeds und (um Duplikate zu ver­mei­den) der bis­he­ri­gen Einträge zumin­dest ein­fach edi­tier­bar vor­lie­gen. Eine auf­ge­bla­se­ne Datenbank wie MariaDB wäre zwar eine funk­tio­nie­ren­de, aber nicht unbe­dingt die offen­sicht­lich beste Lösung. Ich habe mich für SQLite ent­schie­den, das die gesam­te Datenbank platz­spa­rend in einer ein­zi­gen Datei ablegt und mit data­f­ly auch aus Common Lisp her­aus benutz­bar ist.

Dazu genügt mir fol­gen­des Datenbankschema:

CREATE TABLE entries (
  id INTEGER PRIMARY KEY autoincrement,
  feedid INTEGER,
  title text NOT NULL,
  contents BLOB,
  url text NOT NULL,
  TIMESTAMP INTEGER
);
CREATE TABLE feeds (
  id INTEGER PRIMARY KEY autoincrement,
  feedtitle text NOT NULL,
  url text NOT NULL,
  entryselector text NOT NULL,
  titleselector text NOT NULL,
  contentselector text NOT NULL,
  lastsuccess INTEGER
);

Den „letz­ten Erfolg“ eines Feeds möch­te ich zumin­dest spei­chern, um spä­ter leich­ter zu sehen, ob der Cronjob noch ord­nungs­ge­mäß funk­tio­niert. Zur Funktionsweise kom­me ich aber wei­ter unten noch.

Die Umgebung

Da die von mir gewähl­ten Komponenten recht por­ta­bel sind, ist die Wahl des Betriebssystems erst ein­mal nicht so wich­tig. Vorhanden sein soll­te neben SQLite auch eine Version von Common Lisp mit instal­lier­tem Quicklisp. Ich habe mich für SBCL ent­schie­den. Das Script soll­te im Prinzip auch unter Clozure CL, das ich am hei­mi­schen Laptop für SLIME nut­ze, lau­fen, aber dort ist es momen­tan nicht mög­lich, ein Script so kom­for­ta­bel direkt aus der Kommandozeile her­aus auf­zu­ru­fen, wie sbcl – script es mir ermöglicht.

Nun kann es losgehen.

Das Script

Um mir das Testen und Korrigieren so ein­fach wie mög­lich zu machen, habe ich den Code ent­ge­gen übli­chen Gepflogenheiten nicht in meh­re­re Dateien auf­ge­teilt. Beim Laden des Scripts wer­den per Quicklisp die nöti­gen exter­nen Bibliotheken gela­den und gege­be­nen­falls instal­liert, sofern noch nicht vor­han­den; die­se Installation macht sich beim Aufruf in einer kur­zen Wartezeit bemerk­bar, aber das ist noch erträg­lich. Die ver­schie­de­nen Betriebsmodi haben jeweils einen eige­nen Codebereich, der aus der Hauptfunktion her­aus auf­ge­ru­fen wird.

Damit ihr den Code nicht umständ­lich hier her­aus­ko­pie­ren müsst und um den Text hier noch eini­ger­ma­ßen über­sicht­lich zu hal­ten, habe ich ein Projekt auf Bitbucket ange­legt, wo künf­tig die aktu­ell­ste Version des Codes zu fin­den ist.

Die Bedienung des RSS-Parsers ist eigent­lich selbsterklärend:

% ./rssparser.lisp
Syntax:
* rssparser.lisp add <Title> <URL> <EntrySelector> <TitleSelector> [<ContentSelector>]
* rssparser.lisp delete <ID>
* rssparser.lisp list

If you're a bot:
* rssparser.lisp parse

Das nach­träg­li­che Ändern von Feeds ist nicht vor­ge­se­hen, um die Integrität der bereits erstell­ten Feeddateien nicht zu gefährden.

Funktionsweise

Da jeder Feed auf Grundlage von CSS-Selektoren erstellt wird, müs­sen die­se zunächst ermit­telt wer­den. Das geht mit den Entwicklerwerkzeugen, die in den mei­sten aktu­el­len Browsern zur Verfügung ste­hen, ziem­lich ein­fach. Ich mache mal ’n Beispiel: Nehmen wir an, ich wür­de gern den jeweils aktu­el­len Entwicklungsstand von KiTTY abon­nie­ren. Im Webbrowser mei­ner Wahl besu­che ich also die Seite mit den aktu­el­len Änderungen und drücke auf F12.

Ich benö­ti­ge hier jeweils den CSS-Selektor für einen Nachrichteneintrag, sei­nen Titel und sei­nen Inhalt. Nachrichteneinträge sind auf der KiTTY-Website, wie der Browser mir zeigt, in DIV-Elementen mit der Klasse news grup­piert und ihre Überschriften in H1-Tags. Gesonderte Bereiche für die Inhalte gibt es nicht, ich kann den letz­ten Parameter nun also ent­we­der weg­las­sen (dann wer­den nur die Überschriften im Feed gespei­chert) oder einen lee­ren String über­ge­ben (dann wird der gan­ze Nachrichteneintrag als Inhalt gespei­chert). Ich ent­schei­de mich für Letzteres, denn die Liste der Änderungen möch­te ich gern kom­plett - also nicht nur die Versionsnummern - abonnieren.

% ./rssparser.lisp add "KiTTY" "http://www.9bis.net/kitty/?action=news&zone=en" ".news" "h1" ""
Success!

Wird rssparser.lisp par­se aus­ge­führt oder gleich als Cronjob instal­liert, so wer­den regel­mä­ßig alle Feeds als RSS-XML-Dateien im Ordner feeds/ abge­legt bezie­hungs­wei­se aktua­li­siert; even­tu­ell nicht mehr erreich­ba­re Websites wer­den nach einer Statusmeldung auto­ma­tisch aus der Liste gelöscht. Die erzeug­ten Feeds las­sen sich dann im RSS-Leser eurer Wahl abon­nie­ren. Ein Aufruf des Feeds im Firefox zeigt: Es hat geklappt.

rssparser Screenshot

Ihr könnt den Ordner für die XML-Dateien über die Konstante +feed-fol­der+ auch ändern, beach­tet aber, dass Schreibrechte für den Zielordner vor­han­den sein müssen.

Gibt es kein GUI?

Grafische Oberflächen sind bei Programmen, die auf Webservern lau­fen und nicht stän­dig bemut­tert wer­den müs­sen, im Prinzip nur unnüt­zer Überhang, ein Webinterface für Leute, die Angst vor der Konsole haben, wäre mit­tels Caveman2 oder Lucerne aber ohne all­zu gro­ßen Aufwand nach­rüst­bar. Die SQLite-Datenbank lässt sich aller­dings auch mit Werkzeugen wie dem SQLite Database Browser pflegen.

Fazit

Das war jetzt ein biss­chen mehr Arbeit als ange­nom­men, aber das Ergebnis ist ein auf mei­nen Zweck zuge­schnit­te­nes Programm, das ohne über­flüs­si­ge „Extras“ und vor allem ohne Werbung aus­kommt und das nicht zu funk­tio­nie­ren auf­hört, so lan­ge ich es brau­che. Theoretisch lie­ße sich das Script auch in eine por­ta­ble Binärdatei kom­pi­lie­ren, das wür­de die Wartbarkeit aber wie­der­um verringern.

Ergänzungen, Lobpreisungen und Gewinnbeteiligungen sind willkommen.

Senfecke:

Comments are closed.

:) 
:D 
:( 
:o 
8O 
:? 
8) 
:lol: 
:x 
:aufsmaul: 
mehr...
 

Erlaubte Tags:
<strong> <em> <pre> <code> <a href="" title=""> <img src="" title="" alt=""> <blockquote> <q> <b> <i> <del> <span style=""> <strike>

Datenschutzhinweis: Ihre IP-Adresse wird nicht gespeichert. Details finden Sie hier.