2013-03-15 13 views
5

Próbuję parsować dane binarne za pomocą pipe-attoparsec w Haskell. Przyczyną są potoki przewodów (serwery proxy) polegające na przeplataniu odczytu z analizą, aby uniknąć dużego wykorzystania pamięci w przypadku dużych plików. Wiele formatów binarnych opiera się na blokach (lub porcjach), a ich rozmiary są często opisywane przez pole w pliku. Nie jestem pewien, co nazywa się analizatorem składni takiego bloku, ale właśnie to mam na myśli przez "podseparator" w tytule. Problem polega na tym, że wdrażam je w zwięzły sposób, bez potencjalnie dużego śladu pamięci. Wymyśliłem dwie alternatywy, z których każda zawiodła pod pewnym względem."Podseparatory" w pipe-attoparsec

Alternatywa 1 polega na przeczytaniu bloku w oddzielnym teście bytestring i uruchomieniu oddzielnego analizatora składni. Choć zwięzły, duży blok spowoduje wysokie zużycie pamięci.

Alternatywa 2 to parsowanie w tym samym kontekście i śledzenie liczby zużytych bajtów. To śledzenie jest podatne na błędy i zdaje się atakować wszystkie analizatory składające się na końcowy obiekt blockParser. W przypadku zniekształconego pliku wejściowego może on również tracić czas, parsując dalej niż wskazuje pole rozmiaru, zanim można porównać śledzony rozmiar.

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 

Odpowiedz

2

Chciałbym nazwać to parserem "o stałym wpisie".

Mogę Ci powiedzieć, jak zrobi to pipes-parse. Możesz zobaczyć podgląd tego, co mam zamiar opisać w pipes-parse w funkcjach biblioteki biblioteki parseN i parseWhile. Są to w rzeczywistości ogólne dane wejściowe, ale napisałem podobne, na przykład String parserów, a także here i here.

Sztuczka jest naprawdę prosta, wstawiasz fałszywy koniec znacznika wejściowego, w którym chcesz zatrzymać analizator, uruchom analizator składni (który zakończy się niepowodzeniem, jeśli trafi na fałszywy koniec znacznika wejściowego), a następnie usuń koniec wejścia znacznik.

Oczywiście nie jest to tak łatwe, jak ja to robię, ale to ogólna zasada. Trudne części to:

  • Robi się to w taki sposób, że wciąż się strumieniuje. Ten, który łączyłem, jeszcze tego nie robi, ale sposób, w jaki robisz to w streamingu, polega na wstawieniu do niego potoku, który zlicza bajty przepływające przez niego, a następnie wstawia znacznik końca wejścia w odpowiednim miejscu.

  • nie zakłócając istniejącego koniec wejścia markers

Ta sztuczka może być dostosowany do pipes-attoparsec, ale myślę, że najlepszym rozwiązaniem byłoby attoparsec bezpośrednie włączenie tej funkcji. Jeśli jednak to rozwiązanie nie jest dostępne, możemy ograniczyć dane wejściowe podawane do analizatora składni attoparsec.

+0

Wstawianie fajki, która zlicza dźwięki w górę strumienia, jest interesujące, ale skąd będzie wiedzieć, ile bajtów należy policzyć? Ta wartość jest odkrywana tylko przez analizator składni, który nie może wywołać żądania bezpośrednio z wartością jako parametrem, ponieważ jest uruchamiany przez parserD. – absence

+0

@absence Cóż, zignoruj ​​interfejs pipe-attoparsec na razie, ponieważ Renzo i ja to naprawimy wkrótce. Stały parser wejściowy używa wewnętrznie potoku, który ogranicza liczbę bajtów. Pomyśl o tym w następujący sposób: 'parser1 >> (restrict n> -> parser2) >> parser3'. Kombinatoryczna wstawka o stałej szerokości coś w rodzaju 'ograniczenia' przed tym danym parserem. Jest to bardziej skomplikowane, ale dość podobne w duchu. –

+0

Łącza są martwe – SwiftsNamesake

2

OK, więc w końcu zorientowałem się, jak to zrobić i skodyfikowałem ten wzorzec w bibliotece pipes-parse. W dokumencie pipes-parse tutorial wyjaśniono, jak to zrobić, szczególnie w sekcji "Zagnieżdżanie".

Samouczek wyjaśnia to tylko dla analizy agnostycznej typu danych (tj. Ogólny strumień elementów), ale można ją rozszerzyć do pracy z ByteString.

dwóch podstawowych sztuczek, które sprawiają, że te prace są:

  • Mocowanie StateP być globalne (w pipes-3.3.0)

  • Osadzanie sub-parser w nieustalonych StateP warstwy tak, że używa świeże resztki kontekstowe

pipes-attoparsec wkrótce wyda aktualizację ds na pipes-parse, dzięki czemu możesz użyć tych sztuczek we własnym kodzie.

+0

Czy można wywołać passUpTo wewnątrz Data.Attoparsec.Parser podobnie jak funkcja analizatora składni w moim przykładzie? Czy lepiej jest połączyć wiele małych proxy parseD zamiast używać jednego ogromnego analizatora składni, który, choć złożony z mniejszych parserów, jest czarną skrzynką dla rur-attoparsec? – absence

+0

Chcesz, aby 'parseD' zapętlał się nad małym' Parserem ', ponieważ nie może zwolnić pamięci, dopóki nie skończy się każdy' Parser'. 'attoparsec' nigdy nie zwalnia danych wejściowych, dopóki' Parser' nie zakończy działania, ponieważ zawsze rezerwuje sobie prawo do cofnięcia się w 'Parserze'. Jedynym sposobem na sparsowanie czegoś w stałej pamięci jest identyfikacja granic w strumieniu, w którym można bezpiecznie opróżnić poprzednie dane wejściowe. Na przykład, jeśli parsujesz olbrzymi plik CSV, możesz zdefiniować 'Parser' dla każdej linii pliku CSV o nazwie' parseLine', a następnie po prostu uruchom 'parseD', aby wygenerować strumień analizowanych linii. –

+0

@ nieobecność @ Ponadto, 'pipes-bytestring' będzie dostarczać prymitywu' passBytesUpTo', który pozwoli ci ograniczyć cały parser 'attoparsec' do stałego wejścia, podczas gdy ciągle będzie streamował w stałej pamięci. To prawdopodobnie jest bliższe temu, co chcesz. Chodzi o to, że wstawisz 'passBytesUpTo' * upstream * połączenia do' Control.Proxy.Attoparsec.parse' i uruchomi on ten parser w ustalonej liczbie bajtów. –