SimpleWikiParser

Code-Stücke können hier veröffentlicht werden.
EnTeQuAk
User
Beiträge: 986
Registriert: Freitag 21. Juli 2006, 15:03
Wohnort: Berlin
Kontaktdaten:

So... durch die Arbeit an dauCMS kam ich nicht drum rum, einen Wiki-Syntax-Parser zu schreiben.

Aus den Ideen von BlackJack und sape ging ein sehr simpler Parser hervor. Laut meinem Teamkollegen Mr_Snede ein sog. SAX-Parser (was auch immer das heißt :D )

Nun gut. Wie sieht der aus?

So:
ACHTUNG: Dies ist eine veraltete Version. Die neueste Version wird auf den letzten Seiten beschrieben!

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding: utf-8 -*-

'''simple lexer/parser
'''

import re, string

class SyntaxParser(object):
    """
    Syntaxparser and Lexer
    All formatting commands can be parsed one line at a time, though
    some state is carried over between lines.
    """
    def __init__(self):
        
        # that's the var that's needed to set a status
        # so that nested tags can be better parsed
        self.is_ = 0
        self.in_noparse = 0


    def _bold_replacer(self, match):
        '''
        replacer for **bold text***
        '''
        self.is_ = not self.is_
        return ['</b>', '<b>'][self.is_]
    
    
    def _underline_replacer(self, match):
        '''
        replacer for __underlined__ Text
        '''
        self.is_ = not self.is_
        return ['</u>', '<u>'][self.is_]
    
    
    def _italic_replacer(self, match):
        '''
        replacer for // italic text //
        '''
        self.is_ = not self.is_
        return ['</i>', '<i>'][self.is_]
        
    
    def _headline_replacer(self, match_d):
        ''' replacer function for == headlines == '''
        match = re.compile('(?P<hlevel>={1,6})(.*)(?P=hlevel)').match(match_d)
        if match:
            level = len(match.group('hlevel'))
            return '<h%d>%s</h%d>' % (level, match.group(2).strip(), level)
        else:
            return match_d
    

    def _url_replacer(self, match):
        '''
        replacer for URL's in the text
        '''
        return '<a href="%s">%s</a>' % (match, match)


    def _email_replacer(self, match):
        '''
        replacer for emails like 'da@dada.xy'
        '''
        return '<a href="mailto:%s">%s</a>' % (match, match)


    def _br_replacer(self, match):
        '''
        replacer for breaks
        '''
        return '\n<br />'
    
    def _noparse_replacer(self, match):
        '''
        implement a 'noparse'-Syntax... 
        it's like the [code]
BBCode Tags
'''
if match == '{{{' and not self.in_noparse:
self.in_noparse = 1
return '<pre class="noparse>"'
elif self.in_noparse and match == '}}}':
self.in_noparse = 0
return '</pre>'
else:
return match

def lex(self, raw):
'''
this function is the main-function, that let
the parser work.
it scans the text('raw') against 'scan_re'
and calls the 'replacer'-functions
'''
scan_re = re.compile(
r"("
+ r"(?P<br>\[\[BR\]\])"
+ r"|(?P<noparse>(\{\{\{|\}\}\}))"
+ r"|(?P<bold>\*\*)"
+ r"|(?P<italic>\/\/)"
+ r"|(?P<underline>__)"
+ r"|(?P<headline>(?P<hlevel>={1,6})(.*)(?P=hlevel))"
+ r"|(?P<url>(http|ftp|nntp|news|mailtoaim|icq|telnet|sftp|sip)\:[^\s'\"]+\S)"
+ r"|(?P<email>[-\w._+]+\@[\w.-]+)"
+ r")")
blank_re = re.compile("^\s*$")
eol_re = re.compile(r'\r?\n')
raw = string.expandtabs(raw)
indent_re = re.compile("^\s*")
for line in eol_re.split(raw):
if blank_re.match(line):
yield '<br />'
continue
yield re.sub(scan_re, self.parse, line)
if self.in_noparse: yield '</div>'


def parse(self, match):
'''
this function calls the 'replacer'-function wich
returns the replaced content
'''
for type, hit in match.groupdict().items():
if hit:
if self.in_noparse:
return apply(getattr(self, '_noparse_replacer'), (hit,))
else:
return apply(getattr(self, '_' + type + '_replacer'), (hit,))
else:
raise "Can't handle match %s" % match


def main():
txt = '''**__underlined bold__bold //italic//**
ente@gfx-united.de
http://dadada.xy

{{{
nicht getroffen: __ dadad ** dadada** __ }}}

= Überschrift erster Stufe = (die größte)
== Überschrift zweiter Stufe ==


=== Überschrift dritter Stufe === [[BR]]
==== Überschrift vierter Stufe ====
===== Überschrift fünfter Stufe =====
====== Überschrift sechster Stufe ====== (die kleinste)'''
da = SyntaxParser()
new = da.lex(txt)
for line in new:
print line

if __name__ == '__main__':
main()
[/code]

Wie funktioniert der?

Wie folgt:

Es gibt einen Text

Code: Alles auswählen

def main():
    txt = '''**__underlined bold__bold //italic//**
ente@gfx-united.de
http://dadada.xy
'''
    #[...]
Dieser wird an den Parser übergeben:

Code: Alles auswählen

da = SyntaxParser()
new = da.lex(txt)
und nun ist die Variable 'new' ein Generator-Objekt, welches nur noch in seine bestandteile zerlegt werden muss:

Code: Alles auswählen

for line in new:
    print line
Und die Interna?

Jaa... das Innere ist ansich recht einfach gestrickt.
Es gibt eine Funktion 'lex()', die sozusagen das "Gehrin" bildet.
in Ihr wird die Variable '' scan_re '' definiert. Diese definiert die Regular Expressions, die später beim ersten scannen in Tokens zerlegt werden und direkt anhand der regex-gruppen auf die 'replacer'-Funktionen verteilt werden. D.H., das jede Regex in der Theorie eine Funktion besitzt, die sie bearbeitet.
Diese Funktion gibt dann den ersetzten String zurück.
Was sie dafür macht... bleibt ihr überlassen... Hier ist der Parser sehr flexibel.

Das ganze wird dann im Enddefekt als Generatorobjekt zurückgegeben.

Im großen und ganzen eine recht flexible Angelegenheit.

Ein Struktugramm, wie der Parser genau arbeitet ist in Arbeit und wird evtl. morgen fertiggestellt.

Ich freue mich über Feedback!

MfG EnTeQuAk
Zuletzt geändert von EnTeQuAk am Montag 26. Februar 2007, 11:22, insgesamt 1-mal geändert.
Y0Gi
User
Beiträge: 1454
Registriert: Freitag 22. September 2006, 23:05
Wohnort: ja

EnTeQuAk hat geschrieben:Laut meinem Teamkollegen Mr_Snede ein sog. SAX-Parser (was auch immer das heißt :D )
Guckst du http://de.wikipedia.org/wiki/Simple_API_for_XML

Aber warum erfindest du eine komplett neue Syntax? Du könntest z.B. die verbreiteten Textile oder [strike]Markup[/strike] Markdown verwenden. Letzteres habe ich für ein Wiki eingesetzt und mit diesem Code so erweitert, dass es auch Wiki-Links (also das Verlinken von Seiten im Wiki) unterstützt. (Ein anderer Ansatz ist übrigens, alle CamelCase-Worte zu verlinken, aber da halte ich weniger von.)
Zuletzt geändert von Y0Gi am Montag 12. Februar 2007, 18:53, insgesamt 1-mal geändert.
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

@EnTeQuAk: Also ich würde es nicht mit apply(getattr(... machen. Ich würde ein dict erstellen, in den die unbound methods drin stecken... Dürfte etwas schneller sein.

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
BlackJack

``apply`` würde ich nicht nehmen weil's überflüssig ist:

Code: Alles auswählen

apply(getattr(self, '_noparse_replacer'), (hit,))
# =>
getattr(self, '_noparse_replacer')(hit)
Ansonsten ist der dynamische "dispatch" etwas freundlicher für's erweitern, sowohl um neue Methoden in der Originalklasse als auch bei Unterklassen.

Was man machen kann, um es etwas zu beschleunigen ist einen Cache für die Methoden anlegen, dann braucht man das `gettattr()` nur beim ersten Aufruf jeder Methode ausführen. Also jens' Vorschlag, nur dass man das Dictionary nicht selbst per Hand mit Elementen füllt.
EnTeQuAk
User
Beiträge: 986
Registriert: Freitag 21. Juli 2006, 15:03
Wohnort: Berlin
Kontaktdaten:

So. Erstmal herzlichen Dank, für die Meinungen.
Guckst du http://de.wikipedia.org/wiki/Simple_API_for_XML

Aber warum erfindest du eine komplett neue Syntax? Du könntest z.B. die verbreiteten Textile oder Markup verwenden. Letzteres habe ich für ein Wiki eingesetzt und mit diesem Code so erweitert, dass es auch Wiki-Links (also das Verlinken von Seiten im Wiki) unterstützt. (Ein anderer Ansatz ist übrigens, alle CamelCase-Worte zu verlinken, aber da halte ich weniger von.)
Wir erfinden das ganze nicht neu... wir versuchen das beste aus allem zu integrieren, um dem 'DAU' ein wirklich leicht bediehnbares System zu ermöglichen. Immo ist es das nicht... aber es wird später eine WUI geben, die einem in allen Fragen unterstützt, was wichtig ist...
(danke für den link :D)

Das mit dem Methoden-Chachen habe ich nun eingefügt:
http://daucms.de/trac/browser/dauCMS/tr ... _parser.py

Funktionieren tut es... einen Geschwindigkeitsvorteil hab ich nicht bemerkt... kann es aber mal mit einem Benchmark versuchen.

Ansonsten die Frage:
Meintet ihr das so? -- hab ich das richtig implementiert? (siehe funktion 'parse()')


Ansonsten arbeite ich an einer Standalone-Lösung, die ähnlich blackbirds Tekisuto sich mit Unterklassen erweitern lässt. Jedoch wird das noch eine Weile dauern ;)

MfG EnTeQuAK
sape
User
Beiträge: 1157
Registriert: Sonntag 3. September 2006, 12:52

EnTe, wie ich schon im anderen Thread meinte, finde ich die Idee mit dem replaces gut. Scheint mir ziemlich performant.

lg

P.S.: [ot]
Berichtigt mich bitte wenn ich mich irre, aber ich denke Python wird bei Einsatz von ``getattr`` doch nicht viel mehr Zeit benötigen als bei dem Zugriff eines Dicts über keys? Für die Attribute wird doch intern auch schon ein dict geführt?
[/ot]
EnTeQuAk
User
Beiträge: 986
Registriert: Freitag 21. Juli 2006, 15:03
Wohnort: Berlin
Kontaktdaten:

Berichtigt mich bitte wenn ich mich irre, aber ich denke Python wird bei Einsatz von ``getattr`` doch nicht viel mehr Zeit benötigen als bei dem Zugriff eines Dicts über keys? Für die Attribute wird doch intern auch schon ein dict geführt?
Ich probiere es grad mit einem Benchmark... ma schaun, bin gleich fertig :D


MfG EnTeQuAk
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Ich würde die for Schleife so machen:

Code: Alles auswählen

        for type, hit in match.groupdict().items():
            if not hit:
                continue
                
            if self.in_noparse:
                method_name = '_noparse_replacer'
            else:
                method_name = '_%s_replacer' % type
                
            if method_name in self.method_dict:
                method = self.method_dict[method_name]
            else:
                method = getattr(self, method_name)
                self.method_dict[method_name] = method
                
            return method(hit)

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
EnTeQuAk
User
Beiträge: 986
Registriert: Freitag 21. Juli 2006, 15:03
Wohnort: Berlin
Kontaktdaten:

Sieht durchaus besser aus :D

Danke! -- Habs aktualisiert! ;)
BlackJack

Das mit dem Zugriff von `getattr()` und dem Dictionary stimmt natürlich, aber mit dem Dictionary kann man sich die Formatierung von `type` zu einem Schlüssel sparen und gleich `type` selbst als Schlüssel benutzen. Ungetestet:

Code: Alles auswählen

method = self.cache.get(type_)
if method is None:
    method = getattr(self, '_replace_' + type_)
    self.cache[type_] = method
method(hit)
sape
User
Beiträge: 1157
Registriert: Sonntag 3. September 2006, 12:52

jens hat geschrieben:Ich würde die for Schleife so machen:

Code: Alles auswählen

        for type, hit in match.groupdict().items():
            if not hit:
                continue
                
            if self.in_noparse:
                method_name = '_noparse_replacer'
            else:
                method_name = '_%s_replacer' % type
                
            if method_name in self.method_dict:
                method = self.method_dict[method_name]
            else:
                method = getattr(self, method_name)
                self.method_dict[method_name] = method
                
            return method(hit)
...Was viel mehr Overhead erzeugt, als wenn man sich gleich für eins der beiden entscheidet, die im Laufzeitverhalten echt nicht spürbar unterscheidbar sind. Oder sieht das jemand anders?
sape
User
Beiträge: 1157
Registriert: Sonntag 3. September 2006, 12:52

BlackJack hat geschrieben:Das mit dem Zugriff von `getattr()` und dem Dictionary stimmt natürlich, aber mit dem Dictionary kann man sich die Formatierung von `type` zu einem Schlüssel sparen und gleich `type` selbst als Schlüssel benutzen. Ungetestet:
[...]
Wenn dass das einzige Argument ist dann verstehe ich den Sinn ehrlich gesagt nicht. Wenn kein unterschied (oder marginal) zwischen `getattr()` und dem ``dict`` besteht, verschlechtert sich doch nun die Laufzeit (nicht spürbar) spätestens mit der if abfrage und den cachen?
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Wie wäre es, wenn man generell eine separate Klasse mit den replacer-Methoden nimmt.
Dann brauchen die auch nicht mehr alle "_%s_replacer" heißen. Man spart sich dadurch die String Formatierung.

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
sape
User
Beiträge: 1157
Registriert: Sonntag 3. September 2006, 12:52

Jens warum? Ich finde diese Methode sehr gut.

http://www.ubuntuusers.de/paste/7241/

Code: Alles auswählen

[...]write_function = getattr(self, 'write_' + token_id.lower())[...]
Was spricht dagegen? Ist doch sehr Flexible um nicht zu sagen dynamisch die Sache. Diese Art von Flexibilität würde ich mir doch nicht wegen 0.0004sec unterschied im Laufzeitverhalten nehmen wollen.
BlackJack

sape hat geschrieben:
BlackJack hat geschrieben:Das mit dem Zugriff von `getattr()` und dem Dictionary stimmt natürlich, aber mit dem Dictionary kann man sich die Formatierung von `type` zu einem Schlüssel sparen und gleich `type` selbst als Schlüssel benutzen. Ungetestet:
[...]
Wenn dass das einzige Argument ist dann verstehe ich den Sinn ehrlich gesagt nicht. Wenn kein unterschied (oder marginal) zwischen `getattr()` und dem ``dict`` besteht, verschlechtert sich doch nun die Laufzeit (nicht spürbar) spätestens mit der if abfrage und den cachen?
Nein sie verbessert sich, weil man nicht jedesmal `type` als Schlüssel formatieren muss. Das sollte mehr Laufzeit und Speicher verbrauchen, als ein einfacher Test auf `None`. Ob das die Laufzeit nun *spürbar* verbessert, keine Ahnung.
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

sape hat geschrieben:Jens warum?
Hm. Neben dem einsparen des String-Formatings, wäre es auch flexibler. Man könnte die Klasse dann dem Parser übergeben um eine andere Syntax zu nutzten.
OK, jetzt geht's über erben und "überschreiben" der Methoden. Aber was ist, wenn ich eine Methode überhaupt nicht haben will?

Ist es nicht durch eine seperaren Klasse sauberer getrennt: Programmlogik <-> Syntax ???

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
rayo
User
Beiträge: 773
Registriert: Mittwoch 5. November 2003, 18:06
Wohnort: Schweiz
Kontaktdaten:

Hi

Also ich würde das Dict bevorzugen, nur schon aus dem Grund, weil falls mal ein Token ein Sonderzeichen beinhaltet wird dies mit Funktionen nicht funktionieren. Mit einem String als Key würde dies Funktionieren.

Wegen dem Laufzeitverhalten:
Das interessiert mich überhaupt nicht oder ist der Lexer schon jetzt zu langsam? Optimierung immer erst ganz am Schluss wenn es wirklich zu langsam ist.

Immer zuerst das schönere und flexiblere Programmieren.

Gruss
sape
User
Beiträge: 1157
Registriert: Sonntag 3. September 2006, 12:52

BlackJack hat geschrieben: Nein sie verbessert sich, weil man nicht jedesmal `type` als Schlüssel formatieren muss. Das sollte mehr Laufzeit und Speicher verbrauchen, als ein einfacher Test auf `None`. Ob das die Laufzeit nun *spürbar* verbessert, keine Ahnung.
Du meinst das hier?: ``_replace_' + type_'``

Ok, wenn alle Iterationen in dem das hier...

Code: Alles auswählen

method = getattr(self, '_replace_' + type_)
...benutzt wird, immer mehr Zeit kostet als das hier...

Code: Alles auswählen

method = self.cache.get(type_)
if method is None:
..dann ok.

Wenn ich dich also richtig versteh stützt dich deine Argumentation darauf das ``_replace_' + type_'`` mehr Zeit kosten würde als obiges, weil eine Konkatenation mehr Zeit beansprucht als ein simples überprüfen auf None. -- Das klingt selbst für **mich** (Und du weißt wie ich drauf bin ^^) nach ziemlich extreme Optimierung an einer stelle wo das unangebracht ist, da keine Verbesserung im Laufzeitverhalten zu erwarten ist.

lg
BlackJack

jens hat geschrieben:Wie wäre es, wenn man generell eine separate Klasse mit den replacer-Methoden nimmt.
Dann brauchen die auch nicht mehr alle "_%s_replacer" heißen. Man spart sich dadurch die String Formatierung.
Das praktische an dem Entwurfsmuster ist, dass man einfach eine Unterklasse um weitere "replacer" erweitern kann, ohne den Rest anfassen zu müssen. Man könnte auch eine Basisklasse schreiben, die ausschliesslich den Dispatch-Mechanismus enthält und verschiedene Unterklassen für verschiedene Auszeichnungssprachen schreiben. Oder für verschiedene Ausgabesprachen, zum Beispiel RTF oder so.

Ausserdem kann man wirklich alles als Typnamen verwenden, inklusive Schlüsselworte von Python.

Eine weitere Methode es zu beschleunigen ohne eine weitere Klasse einzuführen, wäre vielleicht den Cache in der `__init__()` zu füllen. Einfach alle Attribute von `self` nach dem Muster `_replace_*` absuchen und den Cache entsprechend füllen.
sape
User
Beiträge: 1157
Registriert: Sonntag 3. September 2006, 12:52

BlackJack hat geschrieben:[...]
Eine weitere Methode es zu beschleunigen ohne eine weitere Klasse einzuführen, wäre vielleicht den Cache in der `__init__()` zu füllen. Einfach alle Attribute von `self` nach dem Muster `_replace_*` absuchen und den Cache entsprechend füllen.
Joa, sauber! :) So einfach das wir eigentlich auch drauf hätten kommen müssen!
Antworten