Name für diese Art von Parser, ODER warum es nicht existiert


27

Herkömmliche Parser verbrauchen ihre gesamte Eingabe und erzeugen einen einzelnen Analysebaum. Ich suche einen, der einen kontinuierlichen Stream verbraucht und eine Analyse-Gesamtstruktur erzeugt. [ Bearbeiten: Siehe Diskussion in Kommentaren dazu, warum diese Verwendung dieses Begriffs unkonventionell sein kann. ] Mein Bauch sagt, dass ich nicht die erste Person sein kann, die einen solchen Parser benötigt (oder für nötig hält), aber ich habe monatelang erfolglos hin und her gesucht.

Ich erkenne, dass ich möglicherweise vom XY-Problem gefangen bin. Mein letztendlicher Zweck ist es, einen Textstrom zu analysieren, den größten Teil davon zu ignorieren und einen Strom von Analysebäumen aus den erkannten Abschnitten zu erzeugen.

Meine Frage ist also bedingt: Wenn eine Klasse von Parsern mit diesen Merkmalen existiert, wie heißt sie? Und wenn nicht, warum nicht? Was ist die Alternative? Vielleicht fehlt mir eine Möglichkeit, konventionelle Parser dazu zu bringen, das zu tun, was ich will.


1
Grundsätzlich analysiert Ihr Parser ein einzelnes Dokument und gibt einen Analysebaum aus, beginnt dann sofort mit dem Parsen eines anderen Dokuments usw. Ich nehme an, dass diese Verhaltensänderung im Vergleich zu der Vielfalt der auf ein einzelnes Dokument angewendeten Analysetechniken trivial ist. Daher das Fehlen einer speziellen Bezeichnung dafür.
9000,

3
Ich habe eine Google-Suche nach "Parse Forest" durchgeführt und festgestellt, dass der Earley Parser sie produziert.
Robert Harvey

7
Suchen Sie möglicherweise nach monadischen Parser-Kombinatoren , dh einem größeren Parser, der aus mehreren kleineren Parsern besteht? Sie sind praktisch für Situationen, in denen eine "Insel" einer Sprache in eine andere eingebettet ist. Mein ehemaliger Kollege im C # -Designteam
Eric Lippert

3
Es gibt einige Verwirrung. Meinen Sie damit, dass Sie einen Analysebaum für jedes Dokument in Ihrem Stream möchten und dass diese zusammen einen Analysewald bilden. Das ist nicht die übliche Bedeutung von Parsing Forest. Eine Analyse-Gesamtstruktur ist eine Reihe von Analysebäumen für ein einzelnes mehrdeutiges Dokument (vereinfacht ein wenig), die auf verschiedene Arten analysiert werden können. Und darum geht es in allen Antworten. Besteht Ihr Stream aus vielen vollständigen Dokumenten, die durch Müll getrennt sind, oder handelt es sich um ein einzelnes Dokument, das teilweise verstümmelt wurde. Soll Ihr Dokument syntaktisch korrekt sein oder nicht? Die richtige technische Antwort hängt davon ab.
Babou

1
Vergessen Sie dann alle Antworten zu Parse-Wäldern und Derivaten von Earley, GLR, Marpa. Sie sind anscheinend nicht das, was Sie wollen, es sei denn, es zeigt sich ein anderer Grund. Sind Ihre Dokumente syntaktisch korrekt? Einige Analysetechniken können den Kontext für teilweise verstümmelte Dokumente wiederherstellen. Haben Sie eine genaue Syntax für diese Dokumente. Ist es für alle dasselbe? Möchten Sie die Analysebäume wirklich, oder möchten Sie zufrieden sein, indem Sie die Dokumente isolieren und sie möglicherweise später separat analysieren. Ich glaube, ich weiß, was Ihre Verarbeitung verbessern könnte, aber ich bin nicht sicher, ob Sie das von der Stange bringen können.
Babou

Antworten:


48

Ein Parser, der ein (Teil-) Ergebnis zurückgibt, bevor die gesamte Eingabe verbraucht wurde, wird als inkrementeller Parser bezeichnet . Inkrementelles Parsen kann schwierig sein, wenn lokale Mehrdeutigkeiten in einer Grammatik vorliegen, die erst später in der Eingabe entschieden werden. Eine andere Schwierigkeit besteht darin, die Teile des Analysebaums vorzutäuschen, die noch nicht erreicht wurden.

Ein Parser, der eine Gesamtheit aller möglichen Analysebäume zurückgibt, dh für jede mögliche Ableitung einer mehrdeutigen Grammatik einen Analysebaum zurückgibt, heißt ... Ich bin mir nicht sicher, ob diese Dinge noch einen Namen haben. Ich weiß, dass der Marpa-Parser-Generator dazu in der Lage ist, aber jeder Parser auf Earley- oder GLR-Basis sollte dies tun können.


Sie scheinen jedoch nichts davon zu wollen. Sie haben einen Stream mit mehreren eingebetteten Dokumenten, zwischen denen sich Müll befindet:

 garbagegarbage{key:42}garbagegarbage[1,2,3]{id:0}garbage...

Sie möchten anscheinend einen Parser, der den Müll überspringt und (träge) eine Folge von ASTs für jedes Dokument ausgibt. Dies könnte betrachtet wird ein inkrementelles Parser in seinem allgemeinsten Sinne zu sein. Aber Sie würden tatsächlich eine Schleife wie folgt implementieren:

while stream is not empty:
  try:
    yield parse_document(stream at current position)
  except:
    advance position in stream by 1 character or token

Die parse_docmentFunktion wäre dann ein herkömmlicher, nicht inkrementeller Parser. Es gibt eine kleinere Schwierigkeit, sicherzustellen, dass Sie genug vom Eingabestream gelesen haben, um eine erfolgreiche Analyse durchzuführen. Wie dies gehandhabt werden kann, hängt von der Art des verwendeten Parsers ab. Zu den Möglichkeiten gehören das Erweitern eines Puffers bei bestimmten Analysefehlern oder die Verwendung der verzögerten Tokenisierung.

Lazy Tokenization ist aufgrund Ihres Eingabestreams wahrscheinlich die eleganteste Lösung. Anstatt in einer Lexer-Phase eine feste Liste von Token zu erstellen, fordert der Parser träge das nächste Token von einem Lexer-Rückruf an [1] . Der Lexer würde dann so viel von dem Strom verbrauchen, wie benötigt wird. Auf diese Weise kann der Parser nur fehlschlagen, wenn das reale Ende des Streams erreicht ist oder wenn ein realer Analysefehler aufgetreten ist (dh wir haben mit dem Parsen begonnen, während wir uns noch im Müll befanden).

[1] Ein Callback-gesteuerter Lexer ist auch in anderen Kontexten eine gute Idee, da dies einige Probleme bei der Suche nach dem längsten Token vermeiden kann .

Wenn Sie wissen, nach welcher Art von Dokumenten Sie suchen, können Sie das Überspringen so optimieren, dass es nur an vielversprechenden Stellen endet. Beispielsweise beginnt ein JSON-Dokument immer mit dem Zeichen {oder [. Daher ist Müll eine beliebige Zeichenfolge, die diese Zeichen nicht enthält.


5
Ihr Pseudocode ist eigentlich das, was ich getan habe, aber ich dachte, es wäre nur ein hässlicher Hack. Der Parser löst zwei Arten von Ausnahmen ( NO_MATCHund UNDERFLOW) aus, mit denen ich unterscheiden kann, ob ich die Stream-Position erhöhen oder auf weitere Eingaben warten soll.
Kevin Krumwiede

5
@ Kevin: Ich verwende dies auch mit einigen Sicherheitsfunktionen, um eingehende Daten aus einem Netzwerk in einem proprietären Format zu verarbeiten. Nichts verrücktes daran!
Leichtigkeit Rennen mit Monica

5

Es gibt keinen bestimmten Namen für einen Parser, der dies ausführt. Aber ich werde einen Algorithmus hervorheben, der dies tut: Parsen mit Derivaten .

Es verbraucht Eingabe, ein Token nach dem anderen. Am Ende der Eingabe wird ein Analysewald erstellt. Alternativ können Sie auch den gesamten Parsing-Wald abrufen, während Sie sich mitten im Parsing befinden ( partielles Parsing ).

Beim Parsen mit Derivaten werden kontextfreie Grammatiken verarbeitet, und es wird eine Analysegesamtstruktur für mehrdeutige Grammatiken erstellt.

Es ist eine elegante Theorie, aber sie steckt erst in den Kinderschuhen und ist nicht weit verbreitet. Matt Might hat eine Liste von Links zu verschiedenen Implementierungen in Scala / Racket / etc.

Die Theorie ist leichter zu erlernen, wenn Sie mit der Erkennung von Derivaten beginnen (dh mit der Ermittlung von Derivaten von Sprachen mit dem Ziel, Eingaben zu erkennen, um festzustellen, ob sie gültig sind oder nicht) und dann das Programm so ändern, dass sie mit Derivaten analysiert werden ( Das heißt, ändern Sie es, anstatt Ableitungen von Sprachen zu nehmen , Ableitungen von Parsern zu nehmen und eine Analyse- Gesamtstruktur zu berechnen.


4
Downvoter: Könnten Sie bitte erklären, was eine Downvote wert war? Wenn es etwas gibt, das ich reparieren oder verbessern muss, wäre es sicher nett zu wissen.
Cornstalks

Ich bin nicht der Downvoter, und ich würde nicht ohne Kommentar von Downvoting träumen. Ihr enthusiastischer Artikel enthält jedoch keinen Hinweis auf die vielen vorhandenen Parser, die in Bezug auf Komplexität und Analyse der Gesamtstruktur dasselbe Ergebnis erzielen. Funktionale Programmierung ist großartig, aber es ist auch schön, ein Ergebnis mit der vorhandenen Literatur zu diesem Thema zu vergleichen. Wie praktisch ist Ihr Analysewald für die weitere Verwendung?
Babou

@babou: Ich bin nicht der Autor dieses Blogs / dieser Zeitung. Aber ja, ich stimme zu, ich könnte mehr Details hinzufügen, indem ich diesen Algorithmus mit anderen vergleiche und es im Detail erkläre. Matt Might hat eine ganze Vorlesung darüber , aber es wäre schön, sie in dieser Antwort zusammenzufassen. Wenn ich Zeit habe, werde ich versuchen, diese Antwort zu erweitern.
Cornstalks

1
Verbringen Sie nicht zu viel Zeit damit, es zu erweitern. Soweit ich das beurteilen kann, ist das nicht das Ziel des OP. Seine Frage erfordert sorgfältiges Lesen. Seine Nutzung des Parsing-Waldes ist nicht deine. - - In Bezug auf Derivate ... es hört sich so an, als ob es interessant wäre, aber man muss es mit früheren Arbeiten in Beziehung setzen ... und es gibt einen bedeutenden Teil davon. Aber ich meine nicht in dieser Antwort, sondern in den Zeitungen von M Might oder seinem Blog.
Babou

2

Weit davon entfernt, ideal zu sein, aber ich habe es mehr als einmal gesehen: Versuche bei jeder Eingabezeile zu analysieren. Wenn dies fehlschlägt, behalten Sie die Zeile bei und fügen Sie die nächste hinzu. Im Pseudocode:

buffer = ''
for each line from input:
    buffer = buffer + line
    if can parse buffer:
        emit tree
        buffer = ''

Das große Problem ist, dass Sie in einigen Sprachen nicht wissen können, ob ein Ausdruck vollständig ist, bevor Sie die nächste Zeile lesen. In diesem Fall könnten Sie anscheinend den nächsten lesen und prüfen, ob es sich um einen gültigen Anfang oder eine gültige Fortsetzung handelt. Dafür benötigen Sie jedoch die genaue Sprachsyntax

Schlimmer noch, in diesen Sprachen ist es nicht schwer, einen pathologischen Fall zu erstellen, der erst am Ende der Datei analysiert werden kann, selbst wenn es sich nicht um eine einzelne lange Aussage handelt.


0

In einer Nussschale

Es scheint, dass die schnelle Lösung für Ihr Problem darin besteht, ein REGEX oder einen FSA (Finite-State-Automaten) zu definieren, der alle möglichen Anfänge von Dokumenten erkennt (Fehlalarme sind zulässig, die eigentlich keinem Dokument entsprechen würden). Sie können es dann bei Ihrer Eingabe sehr schnell ausführen, um die nächste Stelle zu identifizieren, an der ein Dokument mit wenigen Fehlern beginnen könnte. Es kann einige fehlerhafte Positionen für einen Dokumentstart verursachen, diese werden jedoch vom Parser erkannt und abgebrochen.

So Finite State Automaton kann der Parser Name sein , das Sie gesucht haben. :)

Das Problem

Es ist immer schwierig, ein praktisches Problem zu verstehen, besonders wenn das Vokabular viele Interpretationen hat. Das Wort Parsing Forest wurde (afaik) für das kontextfreie Parsen (CF) mehrdeutiger Sätze mit mehreren Parsingbäumen geprägt. Es kann etwas verallgemeinert werden, um ein Satzgitter oder andere Arten von Grammatik zu analysieren. Daher waren alle Antworten zu Earley, GLR, Marpa und abgeleiteten Parsern (es gibt viele andere) in diesem Fall nicht relevant.

Aber das haben Sie anscheinend nicht im Sinn. Sie möchten eine eindeutige Zeichenfolge analysieren, bei der es sich um eine Folge eindeutiger Dokumente handelt, und einen Analysebaum für jedes Dokument oder eine strukturierte Darstellung abrufen , da Sie nicht genau angeben, wie die Syntax Ihrer Dokumente definiert ist und woher sie stammt eine formale sprachliche Sichtweise. Was Sie haben, sind ein Algorithmus und Tabellen, die den Parsing-Job ausführen, wenn sie am Anfang eines Dokuments gestartet werden. So sei es.

Das eigentliche Problem ist, dass Ihr Dokumentenstrom erheblichen Müll enthält, der die Dokumente voneinander trennt. Und es scheint, dass es Ihre Schwierigkeit ist, diesen Müll schnell genug zu scannen. Ihre derzeitige Technik besteht darin, am Anfang zu beginnen und zu versuchen, ab dem ersten Zeichen zu scannen und beim nächsten Zeichen mit dem Neustart fortzufahren, wenn dies fehlschlägt, bis Sie ein gesamtes Dokument gescannt haben. Anschließend wiederholen Sie die Eingabe ab dem ersten Zeichen nach dem gerade gescannten Dokument.

Dies ist auch die von @amon im zweiten Teil seiner Antwort vorgeschlagene Lösung .

Dies ist möglicherweise keine sehr schnelle Lösung (ich kann sie nicht testen), da es unwahrscheinlich ist, dass der Code des Parsers so optimiert ist, dass er am Anfang eines Dokuments sehr effizient gestartet wird. Bei normaler Verwendung wird dies nur einmal ausgeführt, sodass es aus Optimierungssicht kein Hot Spot ist. Daher ist Ihr mäßiges Glück mit dieser Lösung nicht zu überraschend.

Was Sie also wirklich brauchen, ist ein Algorithmus, der schnell den Anfang eines Dokuments findet, das mit einer Menge Müll beginnt. Und Sie haben Glück: Es gibt solche Algorithmen. Und ich bin mir sicher, dass Sie es wissen: Es heißt Suche nach einer REGEX.

Die einfache Lösung

Sie müssen lediglich die Spezifikation Ihrer Dokumente analysieren, um herauszufinden, wie diese Dokumente beginnen. Ich kann Ihnen nicht genau sagen, wie, da ich nicht sicher bin, wie ihre Syntaxspezifikation formal organisiert ist. Möglicherweise beginnen sie alle mit einem Wort aus einer endlichen Liste, möglicherweise gemischt mit Satzzeichen oder Zahlen. Das müssen Sie überprüfen.

Sie müssen lediglich einen Finite-State-Automaten (FSA) oder für die meisten Programmierer einen regulären Ausdruck (REGEX) definieren, der die ersten Zeichen eines Dokuments erkennt: Je mehr, desto besser, aber nicht unbedingt sehr groß (da dies Zeit und Raum beanspruchen kann). Dies sollte ausgehend von der Spezifikation Ihrer Dokumente relativ einfach zu bewerkstelligen sein und kann wahrscheinlich automatisch mit einem Programm durchgeführt werden, das die Spezifikation Ihrer Dokumente liest.

Sobald Sie Ihren regulären Ausdruck erstellt haben, können Sie ihn in Ihrem Eingabestream ausführen, um wie folgt sehr schnell zum Anfang Ihres ersten (oder nächsten) Dokuments zu gelangen:

Ich nehme an:
- docstartist eine Regex, die dem Anfang aller Dokumente entspricht.
- search(regex, stream)ist eine Funktion, die streamnach einer passenden Teilzeichenfolge sucht regex. Wenn er zurückkehrt, wird der Stream ab dem Beginn des ersten übereinstimmenden Teilstrings auf sein Suffix reduziert, oder für den leeren Stream wird keine Übereinstimmung gefunden.
- parse(stream)Versucht, ein Dokument vom Anfang des Streams zu analysieren (was davon übrig ist), und gibt den Analysebaum in einem beliebigen Format zurück oder schlägt fehl. Bei der Rückkehr wird der Stream an der Position unmittelbar nach dem Ende des analysierten Dokuments auf sein Suffix reduziert. Es ruft eine Ausnahme auf, wenn das Parsen fehlschlägt.

forest = empty_forest
search(docstart, stream)
while stream is not empty:
  try:
    forest = forest + parse(stream)
  except
    remove first character from stream
  search(docstart, stream)

Beachten Sie, dass das Entfernen des ersten Zeichens erforderlich ist, damit bei der nächsten Suche nicht wieder dieselbe Übereinstimmung gefunden wird.

Natürlich ist die Verkürzung des Streams ein Bild. Es kann nur ein Index für den Stream sein.

Ein letzter Hinweis ist, dass Ihr Regex nicht zu genau sein muss, solange er alle Anfänge erkennt. Wenn gelegentlich eine Zeichenfolge erkannt wird, die nicht der Anfang eines Dokuments sein kann (falsch positiv), sind die Kosten für einen nutzlosen Anruf beim Parser die einzige Strafe.

Das kann also möglicherweise dazu beitragen, den regulären Ausdruck zu vereinfachen, falls dies nützlich ist.

Über die Möglichkeit einer schnelleren Lösung

Die obige Lösung sollte in den meisten Fällen ziemlich gut funktionieren. Wenn Sie jedoch wirklich viel Müll und Terabyte an Dateien zu verarbeiten haben, gibt es möglicherweise andere Algorithmen, die schneller ausgeführt werden.

Die Idee leitet sich aus dem Boyer-Moore-Algorithmus für die Suche nach Zeichenfolgen ab . Dieser Algorithmus kann einen Stream extrem schnell nach einer einzelnen Zeichenfolge durchsuchen, da er eine Strukturanalyse der Zeichenfolge verwendet, um das Lesen des größten Teils des Streams zu überspringen und Fragmente zu überspringen, ohne sie überhaupt anzusehen. Es ist der schnellste Suchalgorithmus für eine einzelne Zeichenfolge.

Die Schwierigkeit besteht darin, dass die Anpassung an reguläre Ausdrücke und nicht an einzelne Zeichenfolgen sehr heikel erscheint und je nach den Funktionen des zu untersuchenden regulären Ausdrucks möglicherweise nicht so gut funktioniert. Dies kann wiederum von der Syntax der zu analysierenden Dokumente abhängen. Aber vertraue mir nicht zu sehr, da ich keine Zeit hatte, die gefundenen Dokumente sorgfältig zu lesen.

Ich überlasse Ihnen ein oder zwei Hinweise, die ich im Internet gefunden habe, darunter einen, der anscheinend ein referiertes Forschungspapier ist , aber Sie sollten dies als spekulativer, möglicherweise als recherchierender Hinweis betrachten, der nur in Betracht gezogen werden sollte, wenn Sie starke Leistungsprobleme hatten. Und es gibt wahrscheinlich kein Regalprogramm, das das macht.


-2

Was Sie beschreiben, kann als SAX vs. SOM bezeichnet werden.

SAX - (Simple API for XML) ist eine Ereignissequenzzugriffs-Parser-API, die von der XML-DEV-Mailingliste für XML-Dokumente entwickelt wurde.

SOM - (XML Schema Object Model) Direktzugriff auf die Darstellung einer XML-Datei im Speicher

Es gibt Implementierungen beider Typen in C # und Java und wahrscheinlich viele weitere. Normalerweise ist eine XSD oder DTD optional.

Die Freude von SAX ist, dass es wenig Speicherplatz benötigt, was für große XML-Dateien großartig ist. Der Nachteil ist, dass der Direktzugriff mit SAX entweder nicht vorhanden oder langsam ist und die Entwicklungszeit in der Regel erheblich länger ist als mit SOM. Das offensichtliche Problem bei SOM sind möglicherweise große RAM-Anforderungen.

Diese Antwort gilt nicht für alle Plattformen und alle Sprachen.


1
Warum analysiert das OP Ihrer Meinung nach XML?
Dan Pichelman

1
Dies beantwortet die Frage nicht.

@Schneemann Bisher beantwortete fast nichts die Frage, einschließlich der ersten Hälfte der akzeptierten Antwort. Es macht keinen Sinn, jemanden zu wählen. Die Frage muss sorgfältig gelesen werden.
Babou

@babou Ich habe niemanden ausgewählt, ich habe meine Ablehnung erklärt.

@Schneemann erklärt meine Downvote . Das ist fair und ich wünschte, mehr Benutzer würden es tun. Ich bin kein Muttersprachler: Wenn ich ihn anpicke, kann das ein zu starker Ausdruck sein. Es ist nur so, dass jeder ungerechtfertigte Annahmen gemacht hat. Es lohnt sich also nicht einmal, es zu bemerken. Es ist wahr, dass dieser ein bisschen mehr abschneidet als die anderen.
Babou
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.