Textpareser in Funktionen aufteilen. Best practice?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

Guten Morgen!

Ich habe für ein Hobbyprojekt einen Textparser geschrieben, der darüber informiert wird, dass eine Textdatei sich geändert hat und dann die Änderungen darin analysiert.

Die grobe Struktur sieht bisher so aus:

Code: Alles auswählen

class Parser(object):

    _parse_new_line(self, line):
        # parsen der neuen daten und Erstellung des Message objects
        return Message

    file_modified(self, filepathpath):
        # [...]
        return _parse_new_data(line)
Jetzt wird _parse_new_data aber immer länger und ich würde das gerne Zerlegen und in Funktionen aufteilen um das ganze übersichtlich zu halten.

Die eigentliche Frage ist wie, wie ihr das vom Design her lösen würdet.

Message beinhaltet eh verschiedene Daten über die analysierte Textzeile. Daher würde ich grundsätzlich so vorgehen, dass ich Message am Anfang erstelle und dann in den Funktionen bearbeite.

Code: Alles auswählen

def _parse_new_data(self, lines):
    message = Message(init_data)
    message = do_something_with_message(message)
    message = do_something_other_with_message(message)
Ich bräuchte eigentlich auch noch den bisherigen Verlauf, also alle vorherigen Messages bei manchen Auswertungen. Die sind bisher in Parser.messages gespeichert. Die könnte dich der Funktion direkt mit übergeben (message = do_something_with_message(message, self.messages)), das hätten den Charme, dass man die Funktion sogar aus der Klasse lösen könnte. Oder ich hole sie mir in der Funktion aus der umgebenden Parser-Klasse.

Und als dritte Möglichkeit fällt mir noch ein, dass ich die do_something-Dinger in eine Klasse kapsel könnte, die von einer Basis-Klasse erbt und damit einen gewissen Aufbau "erzwingt". Das fühlt sich aber etwas unpythonisch an.

Wie würdet ihr denn vorgehen?
BlackJack

@sparrow: Ich habe ehrlich gesagt das Problem nicht verstanden welches Du zu lösen versuchst. Wenn eine Methode zu lang wird, dann könnte man sie auf mehrere Methoden aufteilen. Wenn darin irgendeine Art Muster enthalten ist, dann gibt es dafür in der Regel eine Möglichkeit das in Code zu giessen.
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

@BJ: Das mit dem Aufteilen ist ja richtig, die Frage ist nur, wie man das am schönsten darstellt.

Ich würde das jetzt so lösen wie oben dargestellt. Am Anfang vor der eigentlichen Textanalyse ein Objekt erstellen und das bei dem Aufruf der Funktionen in die der Parser aufgeteilt wurde, übergeben und von dort auch wieder zurück geben lassen. Das wird funktionieren, keine Frage. Die Frage ist, ist das "best practice" oder löst man das außerhalb meines Tellers (von dem ich nur schwer herunter schauen kann) anders. Zum Beispiel, weil es eher unschön ist, das message Objekt zurück zu geben, weil es ja in der Funktion bearbeitet wird und die Änderungen auch außerhalb zur Verfügung stehen. Das wäre gegen "explicit is better than implicit" verstoßen, aber vielleicht sagt man an der Stelle ja: "sinnvoll weil es die Wiederholung der ständigen Zuweisung spart", oder so.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@sparrow: Funktionen teilt man normalerweise nicht horizontal sonder vertikal. Du machst also nicht aus einer Funktion drei mit dem selben Grad an Abstraktion sondern , sondern mehr wie ein Baum, wo die Äste konkretere Probleme als der Stamm lösen.
BlackJack

@sparrow: Das lässt sich so pauschal IMHO nicht beantworten. Wenn Du Dir die wiederholten Zuweisungen im Quelltext sparen möchtest, könntest Du auch die Methoden/Funktionen die da nacheinander abgearbeitet werden sollen in eine Liste stecken und dann in einer Schleife abarbeiten. So von der Idee her:

Code: Alles auswählen

    m = M()
    m = f_a(m, ls)
    m = f_b(m, ls)
    m = f_c(m, ls)
    # ...

# ->
    
    m = M()
    for f in [f_a, f_b, f_c, ...]:
        m = f(m, ls)
Hätte auch den Vorteil das man die Liste mit den Funktionen/Methoden auch dynamisch zusammenstellen kann.

Wobei `m` verändern *und* zurückgeben in der Tat etwas unschön ist.

Und natürlich was Sirius3 gesagt hat. Wie viele Funktionen kann man denn auf diese Weise verketten ohne das so ein `Message`-Objekt zu einem Objekt mit zu vielen Attributen wird? Oder was machen die Funktionen/Methoden denn eigentlich mit diesem Objekt?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@sparrow:
(Keine Ahnung, ob ich Dein Problem richtig verstanden habe)
Wenn Du etlichen Zustand/Daten mitschleppen musst (z.B. die alten Messages), bietet sich doch eine Klasse an. Da kannst Du wunderbar Logik mit Datenhaltung verbinden. Die Aufteilung von `grosses_funktion()` in `status1 = häppchen1(daten); status2 = häppchen2(status1) ...` oder `status2 = häppchen2(häppchen1(daten))` wird ich davon abhängig machen, auf welcher Ebene die Funktionalität logisch trennbar ist und ob vllt. die Teilschritte noch anderweitig nutzbar sind. Hier kann es sinnvoll sein, sich seine eigene "innere" API zu bauen, z.B. kann sich ja ein `parse(data)` durchaus aus einzelnen Parseschritten zusammensetzen.
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

Message beinhaltet unter anderem:

Message.original_text
Message.formated_text

Unter anderem machen die Funktionen so etwas:
in original_text nach einem Muster suchen und anschließend den inhalt von formated_text so ändern, dass etwas markiert wird (Markup). Ob, wie und was markiert wird wird in der Funktion entschieden. formated_text enthält also am Ende eine hübsche, darstellbare Variante von Text.
Die Regeln zum Finden und Markieren sind halt recht komplex, weshalb ich die einzelnen Vorgänge gerne Kapseln würde.
Einzelne Schritte wären dann u.a.:
- wer hat die Nachricht ausgelöst
- wie wird der Status der Nachricht eingeschätzt?
- welche Systeme sind laut Log betroffen und wie ist das im Zusammenhang mit dem ermittelten Status zu bewerten?
- Gab es bereits eine solche Warnung und eskaliert die Situation gerade?
- ....

Die Daten in den zu verarbeitenden Dateien sind nicht homogen und die Regeln pro Schritt so komplex, dass eine einzelne Funktion, die das komplett abdecken soll, sehr schnell unübersichtlich wird.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@sparrow: kannst Du noch konkreter werden? Normalerweise will man ja nichts verändern, weil man dann immer alle Nebenbedingungen wer was wann geändert hat, mitberücksichtigen muß.
Ich würde die Nachricht in einen Baum parsen, den Baum mit Deinen verschiedenen Mustern bearbeiten und Annotations an die Äste hängen und zum Schluß den Baum mit Annotations wieder in eine Repräsentation bringen. Alle drei Schritte kann man schön logisch voneinander trennen. Die Informatiker haben dem sogar schon einen schönen Namen gegeben und in die Vorlesung "Compilerbau" gepackt.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@sparrow:
Falls das nicht eine Nummer zu groß wird, böten sich hier Mixins an.
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

Genau. Deshalb gibt es in Message die Unterteilung nach original_text und formated_text. Eins bleibt im Ursprungszustand, das andere wird verändert.

Und mit dem Baum bauen hast du recht. Ich habe nur das Problem, dass die Daten keinem festen Muster oder Syntax folgen, sondern die Erkennung recht Aufwändig ist. Die Eingaben kommen teilweise aus Quellen, die manuell gefüttert werden.
Da steht dann zum Beispiel:
"xxx@jabberid: temperature system3: 12°C"
oder aber,
"yyy@jabberid: 23°C sys4 temp"
"zzz@jabberid: sys5 temp ok"

Da würde ich jetzt herausfiltern wer das geschrieben hat (kein Problem) und anhand von regeln versuchen den Rest der Nachricht zu bewerten. Art der Nachricht (Temperaturangabe), betroffenes system, status der temperatur.

Das funktioniert auch alles, es gibt aber nicht nur Nachrichten zur Temperatur sondern ganz viele Fälle. Manche betreffen Systeme, manche Benutzer, manche Bauteile.

Die Nachricht zu parsen ist auch nicht das Problem, es ist nur relativ aufwändig die Eventualitäten abzubilden.

Mir geht es hauptsächlich darum, wie ich das im Code hübsch darstelle.
In dem obigen Beispielen würde der Ablauf so sein, dass ich den Absender ermittele und anschließend den Typ der Nachricht. Wenn ich der Meinung bin ich konnte den Typ bestimmen, übergebe ich das Message-Objekt einer Funktion, die entsprechend versucht betroffene Systeme und andere Angaben aus dem Text zu ermitteln und entsprechend Veränderungen an dem Message Objekt vornimmt (status setzen, formatierung in formated_text vornehmen, ...).

Klar lässt sich das gut kapseln. Ich bin mir nur unsicher wegen der Rückgabe aus der Funktion von einem Objekt, das der Funktion übergeben wurde und dort auch bearbeitet wird.

Das Finden von bestimmten Elementen im Text (Systemnamen, Benutzernamen, etc.) inkl. "jemand hat sich vertippt" oder etwas abgekürzt ist natürlich unabhängig davon bereits in Funktionen - die ich aber erst in den - ich nenne das mal Unterfunktionen - aufrufe, wenn ich bereits weiß was für eine Art von Nachricht das wahrscheinlich ist.
Nützt ja nichts einen Text auf 3 verschiedene Eingabemöglichkeiten von Temperaturen zu prüfen, wenn der Benutzer nur mitteilen wollte, dass ein System offline ist.

Aber quasi dieses:
message = handle_temperature_message(message)
fühlt sich irgendwie seltsam an.
Antworten