2013-03-15 8 views
5

Ich versuche, Binärdaten mit Pipes-Attoparsec in Haskell zu analysieren. Der Grund dafür, dass Pipes (Proxies) beteiligt sind, besteht darin, das Lesen mit Parsing zu verschachteln, um eine hohe Speicherbenutzung für große Dateien zu vermeiden. Viele Binärformate basieren auf Blöcken (oder Chunks), und ihre Größen werden oft durch ein Feld in der Datei beschrieben. Ich bin mir nicht sicher, was ein Parser für einen solchen Block heißt, aber das meine ich mit "Sub-Parser" im Titel. Das Problem, das ich habe, ist, sie in einer prägnanten Weise ohne einen potenziell großen Speicherbedarf zu implementieren. Ich habe mir zwei Alternativen ausgedacht, die beide in gewisser Hinsicht versagen."Sub-Parser" in Pipes-Attoparsec

Alternative 1 besteht darin, den Block in eine separate Bytefolge zu lesen und einen separaten Parser dafür zu starten. Kurz gesagt, ein großer Block führt zu einem hohen Speicherverbrauch.

Alternative 2 besteht darin, im selben Kontext zu analysieren und die Anzahl der verbrauchten Bytes zu verfolgen. Dieses Tracking ist fehleranfällig und scheint alle Parser zu befallen, die in den finalen blockParser fallen. Bei einer fehlerhaften Eingabedatei könnte sie auch Zeit durch Parsing vergeuden, die weiter ist als durch das Größenfeld angegeben, bevor die verfolgte Größe verglichen werden kann.

import Control.Proxy.Attoparsec 
import Control.Proxy.Trans.Either 
import Data.Attoparsec as P 
import Data.Attoparsec.Binary 
import qualified Data.ByteString as BS 

parser = do 
    size <- fromIntegral <$> anyWord32le 

    -- alternative 1 (ignore the Either for simplicity): 
    Right result <- parseOnly blockParser <$> P.take size 
    return result 

    -- alternative 2 
    (result, trackedSize) <- blockparser 
    when (size /= trackedSize) $ fail "size mismatch" 
    return result 

blockParser = undefined 

main = withBinaryFile "bin" ReadMode go where 
    go h = fmap print . runProxy . runEitherK $ session h 
    session h = printD <-< parserD parser <-< throwParsingErrors <-< parserInputD <-< readChunk h 128 
    readChunk h n() = runIdentityP go where 
     go = do 
      c <- lift $ BS.hGet h n 
      unless (BS.null c) $ respond c *> go 

Antwort

2

Ich nenne dies gerne einen "festen Eingabe" Parser.

Ich kann Ihnen sagen, wie pipes-parse es tun wird. Sie können eine Vorschau dessen sehen, was ich in pipes-parse in den parseN und parseWhile Funktionen der Bibliothek beschreiben werde. Diese sind eigentlich für generische Eingaben, aber ich schrieb ähnliche, zum Beispiel String Parser sowie here und here.

Der Trick ist wirklich einfach, Sie fügen ein falsches Ende des Eingabemarkers ein, wo der Parser stoppen soll, führen Sie den Parser aus (der fehlschlägt, wenn er das falsche Ende des Eingabemarkers trifft), dann entfernen Sie das Ende der Eingabe Marker.

Offensichtlich ist das nicht so einfach wie ich es klingen lasse, aber es ist das allgemeine Prinzip. Die heiklen Teile sind:

  • Doing es so, dass es immer noch streamt. Die eine, die ich verlinkt habe, macht das noch nicht, aber die Art, wie du dies auf eine Streaming-Weise tust, ist, eine Pipe upstream einzufügen, die die durchfließenden Bytes zählt und dann den Marker am Ende der Eingabe an der richtigen Stelle einfügt.

  • nicht mit bestehenden Ende der Eingabe störender Marker

Dieser Trick für pipes-attoparsec angepasst werden kann, aber ich denke, die beste Lösung wäre für attoparsec direkt um diese Funktion zu schließen. Wenn diese Lösung jedoch nicht verfügbar ist, können wir die Eingabe beschränken, die an den Parser attoparsec übergeben wird.

+0

Das Einfügen einer Pipe, die Upstream zählt, klingt interessant, aber woher weiß es, wie viele Bytes gezählt werden sollen? Dieser Wert wird nur vom nachgeschalteten Parser erkannt, der die Anfrage nicht direkt mit dem Wert als Parameter aufrufen kann, da er von parserD ausgeführt wird. – absence

+0

@absence Nun, ignorieren Sie die Pipes-attoparsec-Schnittstelle für jetzt, weil Renzo und ich es bald beheben werden. Der feste Eingabe-Parser verwendet intern eine Pipe, die die Byteanzahl einschränkt. Stellen Sie sich folgendes vor: 'parser1 >> (restrict n> -> parser2) >> parser3'. Die Kombinatorik mit fester Breite fügt etwas wie "Beschränken" stromaufwärts von dem gegebenen Parser ein. Es ist komplizierter, aber im Geist ziemlich ähnlich. –

+0

Die Links sind tot – SwiftsNamesake

2

Ok, also habe ich endlich herausgefunden, wie man das macht, und ich habe dieses Muster in der pipes-parse Bibliothek kodifiziert. Die pipes-parse tutorial erklärt, wie man das macht, speziell im Abschnitt "Nesting".

Das Lernprogramm erklärt dies nur für das datenathnologische Parsen (d. H. Einen generischen Strom von Elementen), aber Sie können es auch so erweitern, dass es mit ByteString funktioniert.

Die beiden wichtigsten Tricks, die diese Arbeit zu machen sind:

  • Fixing StateP global zu sein (in pipes-3.3.0)

  • den Unter Parser in einer transienten StateP Schicht einbetten, so dass es eine verwendet Fresh Leftovers Kontext

Die pipes-attoparsec wird bald ein Update, dass buil veröffentlicht werden ds auf pipes-parse, damit Sie diese Tricks in Ihrem eigenen Code verwenden können.

+0

Kann ich passUpTo in einem Data.Atoparsec.Parser wie die Parser-Funktion in meinem Beispiel aufrufen? Oder ist es besser, mehrere kleine parseD-Proxies zu kombinieren, anstatt einen großen Parser zu verwenden, der, während er aus kleineren Parsern zusammengesetzt ist, eine schwarze Box für Pipes-attoparsec ist? – absence

+0

Sie möchten, dass parseD 'über kleine' Parser's läuft, da sie den Speicher nicht freigeben können, bis jeder 'Parser' abgeschlossen ist. 'attoparsec' gibt niemals die Eingabe frei, bis der' Parser' abgeschlossen ist, da es sich immer das Recht vorbehält, in einem 'Parser' zurückzugehen. Die einzige Möglichkeit, etwas in konstantem Speicher zu parsen, ist das Identifizieren von Grenzen in dem Stream, wo es sicher ist, vorherige Eingaben zu löschen. Wenn Sie beispielsweise eine riesige CSV-Datei analysieren, können Sie für jede Zeile der CSV-Datei einen 'Parser' definieren, der' parseLine' genannt wird, und dann einfach'parseD' ausführen, um einen Strom analysierter Zeilen zu generieren. –

+0

@ abwesenheit @ Außerdem wird 'pipes-bytestring' ein 'passBytesUpTo'-Primitiv bereitstellen, das es Ihnen erlaubt, einen ganzen' attoparsec'-Parser auf einen festen Eingang zu beschränken, während immer noch im konstanten Speicher gestreamt wird. Das ist wahrscheinlich näher an dem, was Sie wollen. Die Idee ist, dass Sie "passBytesUpTo" * upstream * eines Aufrufs an "Control.Proxy.Atoparsec.parse" setzen, und es wird diesen Parser in einer festen Anzahl von Bytes ausführen. –

Verwandte Themen