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:

SimpleWikiParser

Beitragvon EnTeQuAk » Sonntag 11. Februar 2007, 19:38

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][/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()


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

Re: SimpleWikiParser

Beitragvon Y0Gi » Sonntag 11. Februar 2007, 20:51

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
Moderator
Beiträge: 8458
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Beitragvon jens » Montag 12. Februar 2007, 09:08

@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.

CMS in Python: http://www.pylucid.org
GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
BlackJack

Beitragvon BlackJack » Montag 12. Februar 2007, 09:29

``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:

Beitragvon EnTeQuAk » Montag 12. Februar 2007, 10:22

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

Beitragvon sape » Montag 12. Februar 2007, 11:03

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:

Beitragvon EnTeQuAk » Montag 12. Februar 2007, 11:05

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
Moderator
Beiträge: 8458
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Beitragvon jens » Montag 12. Februar 2007, 11:24

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)

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

Beitragvon EnTeQuAk » Montag 12. Februar 2007, 11:33

Sieht durchaus besser aus :D

Danke! -- Habs aktualisiert! ;)
BlackJack

Beitragvon BlackJack » Montag 12. Februar 2007, 11:42

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

Beitragvon sape » Montag 12. Februar 2007, 11:50

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

Beitragvon sape » Montag 12. Februar 2007, 11:53

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
Moderator
Beiträge: 8458
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Beitragvon jens » Montag 12. Februar 2007, 11:57

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.

CMS in Python: http://www.pylucid.org
GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
sape
User
Beiträge: 1157
Registriert: Sonntag 3. September 2006, 12:52

Beitragvon sape » Montag 12. Februar 2007, 12:05

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

Beitragvon BlackJack » Montag 12. Februar 2007, 12:09

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.

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder