2013-03-15 18 views
5

Sto provando ad analizzare i dati binari usando pipe-attoparsec in Haskell. Il motivo per cui i pipe (proxy) sono coinvolti consiste nell'interlacciare la lettura con l'analisi per evitare l'utilizzo di memoria elevata per file di grandi dimensioni. Molti formati binari sono basati su blocchi (o blocchi) e le loro dimensioni sono spesso descritte da un campo nel file. Non sono sicuro di ciò che viene chiamato un parser per tale blocco, ma questo è ciò che intendo per "sub-parser" nel titolo. Il problema che ho è di implementarli in modo conciso senza un ingombro di memoria potenzialmente grande. Ho inventato due alternative che falliscono in qualche modo."Sub-parser" in pipe-attoparsec

L'alternativa 1 è leggere il blocco in un test separato e avviare un parser separato per esso. Mentre conciso, un grande blocco causerà un uso di memoria elevato.

L'alternativa 2 è di mantenere l'analisi nello stesso contesto e tenere traccia del numero di byte consumati. Questo tracciamento è soggetto a errori e sembra infestare tutti i parser che compongono nell'ultimo blockParser. Per un file di input non valido potrebbe anche sprecare tempo analizzando ulteriormente di quanto indicato dal campo delle dimensioni prima che la dimensione tracciata possa essere confrontata.

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 

risposta

2

Mi piace chiamare questo un parser "input fisso".

Posso dirti come lo farà pipes-parse. È possibile visualizzare un'anteprima di ciò che sto per descrivere in pipes-parse nelle funzioni parseN e parseWhile della libreria. Quelli sono in realtà per input generici, ma ho scritto quelli simili per esempio parser String e here e here.

Il trucco è davvero semplice, si inserisce una finta fine del marcatore di input in cui si desidera arrestare il parser, si esegue il parser (che non riuscirà se raggiunge la finta fine del marker di input), quindi si rimuove la fine dell'input pennarello.

Ovviamente, non è così facile come lo faccio sembrare, ma è il principio generale. Le parti più complesse sono:

  • Eseguirlo in modo che sia ancora in streaming. Quello che ho collegato non lo fa, tuttavia, ma il modo in cui lo fai in streaming è quello di inserire un pipe upstream che conta i byte che lo attraversano e quindi inserisce il marker di fine ingresso nel punto corretto.

  • non interferire con l'estremità esistente di ingresso marcatori

Questo trucco può essere adattato per pipes-attoparsec, ma che la soluzione migliore sarebbe per attoparsec di includere direttamente questa funzione. Tuttavia, se tale soluzione non è disponibile, possiamo limitare l'input che viene inviato al parser attoparsec.

+0

Inserire una pipe che conti i suoni upstream interessanti, ma come farà a sapere quanti byte contare? Questo valore viene rilevato solo dal parser downstream, che non può chiamare direttamente la richiesta con il valore come parametro, poiché viene eseguito da parserD. – absence

+0

@absence Bene, per ora ignora l'interfaccia pipe-attoparsec perché Renzo e io lo sistemeremo presto. Il parser di input fisso utilizza internamente una pipe che limita il conteggio dei byte. Pensa a questo: 'parser1 >> (restrict n> -> parser2) >> parser3'. La larghezza fissa combinatoria inserisce qualcosa come "restrict" a monte di quel dato parser. È più complicato di così, ma abbastanza simile nello spirito. –

+0

I collegamenti sono morti – SwiftsNamesake

2

Ok, quindi ho finalmente capito come fare questo e ho codificato questo modello nella libreria pipes-parse. Il pipes-parse tutorial spiega come farlo, in particolare nella sezione "Annidamento".

Il tutorial spiega questo solo per l'analisi agnostica di tipo datatype (vale a dire un flusso generico di elementi), ma è possibile estenderlo per funzionare anche con ByteString s.

I due trucchi principali che fanno di questo lavoro sono:

  • fissaggio StateP essere globale (in pipes-3.3.0)

  • Incorporare il sub-parser in un StateP strato transitoria in modo che utilizzi un fresco rimanente contesto

Il pipes-attoparsec sta per rilasciare un aggiornamento presto che buil ds su pipes-parse in modo da poter utilizzare questi trucchi nel proprio codice.

+0

Posso chiamare passUpTo all'interno di un Data.Attoparsec.Parser come la funzione parser nel mio esempio? O è meglio combinare più piccoli proxy parseD invece di usare un enorme Parser che, mentre composto da parser più piccoli, è una scatola nera per pipe-attoparsec? – absence

+0

Si desidera che 'parseD' esegua il loop su' Parser' piccoli perché non può liberare memoria fino al completamento di ogni 'parser'. 'attoparsec' non libera mai input finché il' Parser' non termina perché si riserva sempre il diritto di tornare indietro all'interno di un 'parser'. L'unico modo per analizzare qualcosa nella memoria costante è identificare i limiti nel flusso in cui è sicuro svuotare l'input precedente. Ad esempio, se si sta analizzando un file CSV enorme, è possibile definire un 'parser' per ogni riga del file CSV chiamata' parseLine', quindi eseguire 'parseD' per generare un flusso di righe analizzate. –

+0

@ assenza @ Inoltre, 'pipe-bytestring' fornirà una primitiva' passBytesUpTo' che consentirà di delimitare un intero parser 'attoparsec' su un input fisso mentre continua a fluire nella memoria costante. Probabilmente è più vicino a quello che vuoi. L'idea è di mettere 'passBytesUpTo' * a monte * di una chiamata a' Control.Proxy.Attoparsec.parse' e ​​eseguirà quel parser in un numero fisso di byte. –