- Luca Lindhorst - http://lucalindhorst.de -

Einstieg in PegJS

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 [1]) entschieden ein entsprechendes Tool zu nutzen, nämlich PegJS [2], 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 [3] 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:

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 [4] 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.ä. .