Von kleinen Experimenten oder kleinen Tools, die man sich mal kurz „nebenbei“ schreibt um eine Aufgabe einfacher lösen zu können, weiß ich, dass einen Parser für etwas komplexere Fälle gar nicht so einfach ist. Deshalb habe ich mich für mein neuestes Projekt (Proto Parser) entschieden ein entsprechendes Tool zu nutzen, nämlich PegJS, zwar gibt es noch weitere Alternativen, mir schien dieses Tool jedoch das eingängste zu sein. An dieser Stelle möchte ich euch einen kurzen Einblick geben, wie man einen einfachen Parser mit PegJS erstellen kann:
Allgemein
PegJS nimmt ein in einer eigenen Sprache erstellten Beschreibungs an und compiliert daraus Javascript Code, welcher dann jederzeit wiederverwendet werden kann um gewünschtes Format zu parsen. Diese Sprache weist dabei Ähnlichkeiten zu Regulären Ausdrücken auf, diese zu kennen ist also durchaus von Vorteil und erleichtert den Einstieg ungemein, ist aber nicht zwingend erforderlich.
Um einen solchen Parser zu schreiben bietet es sich an das PegJS Online-Tool zu nutzen, hier werden Fehler in der Beschreibung oder beim Parsen direkt ausgegeben. Solange die Parser nicht zu groß werden ist alles gut, sonst fängt dieser an zu haken, denn bei jeder Änderung des Beschreibungstextes (von jetzt an Deskriptor genannt) führt zu einer neuen Generierung, leider lässt sich dies nicht manuell auslösen.
Nun genug der Einleitung…
Deskriptor Elemente
Ein Deskriptor besteht aus Regeln, jede Regel kann wiederrum Regeln enthalten. Zum erstellen dieser Regeln gibt es folgende Sprachelemente:
- Häufigkeitsmodifikatoren. Diese Modifikatoren können hinter jede Art von Expression geschrieben werden.
- *: Elemente können beliebig oft vorkommen. (0- bis ∞-mal)
- ?: Elemente können einmal vorkommen, müssen aber nicht. (0- bis 1-mal)
- +: Elemente müssen mindestens einmal bis unendlich oft vorkommen. (1- bis ∞-mal)
- {n}: Elemente müssen n-mal vorkommen. (n-mal)
- {k, n}: Elemente müssen mindestens k-mal bis zu n-mal vorkommen. (k- bis n-mal)
- {,n} Elemente können beliebig oft, aber maximal n-mal vorkommen. (0- bis n-mal)
- Charaktergruppen. Diese Gruppen bezeichnen einen der angegebenen Zeichen.
- [abc]: Einmal der Kleinbuchstabe a, b oder c.
- [a-c5-9]: Bereiche. Alle (Klein-)Buchstaben von a-c und alle Ziffern von 5-9. („abc56789“ sind erlaubt)
- [a-zA-Z0-9]: Alle Buchstaben (exkl. Umlaute) und alle Ziffern.
- [\t\r\n ]: Besondere Zeichen können ebenfalls enthalten sein, hier \r, \n, \t, “ “ (Leerzeichen), diese werden dazu mit Backslash („\“) markiert. Backslash selbst wird ebenso maskiert „\\“.
- [^abc]: Negierung. Alle Zeichen außer den Kleinbuchstaben a, b und c.
- Zeichenketten. Auch feste Zeichenketten können Beschrieben werden, dazu werden einfach die Zeichen in Anführungszeichen gesetzt „Dies ist ein String“.
- Gruppen. Um bestimmte Gruppen zu selektieren oder daran Modifikatoren anzuhängen werden zeichen mithilfe von runden Klammern gruppiert:
(„Text“ („W“[abc]){,2}) : In diesem Beispiel darf die Äußere Gruppe mehrmals vorkommen, die innere maximal zweimal.
„TextWa“ ist also gültig, ebenso ist gültig „TextWaWb“, nicht aber „TextWaWbWc“, denn hier käme die innere Gruppe dreimal vor. - Kommentare. Kommentare lassen sich wie in C++ oder JS mit // (zeilenweise) oder /* */ ausmaskieren.
- Regeln. Regeln werden genau wie JS-Variablen benannt, also vorne ein Buchstabe danach sind ebenso Zahlen und Unterstriche erlaubt.
Regel1 = „ab“
Regel2 = „wb“ Regel1{,2}
In diesem Beispiel nutzt Regel2 Regel1, die Definitionsreihenfolge spielt hierbei übrigens keine Rolle. Gültig für Regel zwei: „wbab“, nicht gültig „wbababab“. - Oder. Mit einem Slash („/“) lassen sich Ausdrücke „oder-Verknüpfen“, hierbei wird immer der erste passende Ausdruck gewählt, sollten auf ein Textmuster also mehrere passen, so sollte der welcher mehr Zeichen vereinbart nach vorne gesetzt werden, sonst kann es passieren, dass eine Regel greift welche gar nicht gewünscht ist.
Weiterhin zu beachten ist, dass in den Regeln wirklich jedes einzelne Zeichen angegeben werden muss, sonst wird beim Parsen ein Fehler festgestellt.
Es gibt noch ein paar Sprachelemente mehr, diese können der Doku entnommen werden. Ein wirklich nützliches Element sind z.B. die Codeblöcke, hiermit kann die Rückgabe einer Regel noch einmal angepasst werden, dies schafft enorme Vereinfachung und eine wesentlich besser verarbeitbare Ausgabe der Parser. Diese Codeblöcke werden einfach in geschwungenen Klammern an die Regel angehängt („{}“). Zusätzlich existieren noch sog. Bezeichner (Label), diese werden getrennt durch einen Doppelpunkt vor einen bel. Ausdruck geschrieben, über diese Bezeichner kann im Code auf die geparsten Elemente zugegeriffen werden. ( label:[a-z] )
Beispiel
Hier jetzt mal ein einfaches Beispiel:
url = protocol "://" (username "@")? domain "/" path
protocol = "http" "s"?
username = [a-zA-Z0-9]+
domainPart = [a-zA-z] [a-zA-z0-9-]*
domain = domainPart ("." domainPart)*
path = [a-zA-z0-9-_.]+ ("/" [a-zA-z0-9-_.]+)* "/"?
Mit diesem Code lässt sich wunderbar eine einfache HTTP(s) URL parsen, allerdings ist die Ausgabe noch recht kryptisch. Daher verändern wir den Code zu folgendem:
url = proto:protocol "://" u:(username "@")? d:domain path:("/" path)?
{
var ret = {
proto: proto,
domain: d
};
if(u)
ret.user = u[0];
if(path)
ret.path = path.join('');
return ret;
}
protocol = a:"http" b:"s"?
{ return a + (b == null ? "" : b)}
username = a:[a-zA-Z0-9]+
{ return a.join('') }
domainPart = a:[a-zA-z] b:[a-zA-z0-9-]*
{ return a+b.join('') }
domain = a:domainPart b:("." domainPart)*
{
return a+b.map(function(a){
return a.join('')
}).join('')
}
pathPart = a:[a-zA-z0-9-_.]+
{ return a.join('') }
path = a:pathPart b:("/" pathPart)* c:"/"?
{
return a+b.map(function(a){
return a.join('')
}).join('') + (c == null ? "" : c)
}
Hier werden durch besagte Codeblöcke die Rückgaben angepasst, um die Verarbeitbarkeit zu verbessern. Als Empfehlung gilt hier immer alle mehrfachauftretenden Elemente als eigene Regel zu erstellen, hiermit vermeidet man erstens Inkonsistenzen bei komplexeren Parsern, außerdem ist es wesentlich einfacher hiermit im Code zu arbeiten, sonst hat man teils sehr tief verschachtelte Schleifen o.ä. .