簡體   English   中英

在Haskell中高效流式傳輸和操作字節流

[英]Efficient streaming and manipulation of a byte stream in Haskell

在為大型(<bloblength><blob>)*編碼的二進制文件編寫反序列化器時,我遇到了各種Haskell生成轉換消耗庫。 到目前為止,我知道有四個流媒體庫:

這是一個精簡的示例,當我嘗試使用conduit進行Word32流式處理時出現問題。 稍微更現實的Word32將首先讀取確定blob長度的Word32然后產生該長度的懶惰ByteString (然后進一步反序列化)。 但在這里,我只是嘗試從二進制文件中以流方式提取Word32:

module Main where

-- build-depends: bytestring, conduit, conduit-extra, resourcet, binary

import           Control.Monad.Trans.Resource (MonadResource, runResourceT)
import qualified Data.Binary.Get              as G
import qualified Data.ByteString              as BS
import qualified Data.ByteString.Char8        as C
import qualified Data.ByteString.Lazy         as BL
import           Data.Conduit
import qualified Data.Conduit.Binary          as CB
import qualified Data.Conduit.List            as CL
import           Data.Word                    (Word32)
import           System.Environment           (getArgs)

-- gets a Word32 from a ByteString.
getWord32 :: C.ByteString -> Word32
getWord32 bs = do
    G.runGet G.getWord32be $ BL.fromStrict bs

-- should read BytesString and return Word32
transform :: (Monad m, MonadResource m) => Conduit BS.ByteString m Word32
transform = do
    mbs <- await
    case mbs of
        Just bs -> do
            case C.null bs of
                False -> do
                    yield $ getWord32 bs
                    leftover $ BS.drop 4 bs
                    transform
                True -> return ()
        Nothing -> return ()

main :: IO ()
main = do
    filename <- fmap (!!0) getArgs  -- should check length getArgs
    result <- runResourceT $ (CB.sourceFile filename) $$ transform =$ CL.consume
    print $ length result   -- is always 8188 for files larger than 32752 bytes

程序的輸出只是讀取的Word32的數量。 事實證明,在讀取第一個塊(大約32KiB)后,流終止。 由於某種原因, mbs永遠不會是Nothing ,所以我必須檢查null bs ,這會在塊被消耗時停止流。 顯然,我的導管transform是錯誤的。 我看到兩個解決方案的路線:

  1. await不想進入ByteStream的第二個塊,那么還有另一個函數可以提取下一個塊嗎? 在我看過的例子中(例如, 管道101 ),這不是它的完成方式
  2. 這只是設置transform的錯誤方法。

這怎么做得好? 這是正確的方法嗎? (表現很重要。)

更新:這是一個糟糕的方式用做Systems.IO.Streams

module Main where

import           Data.Word                (Word32)
import           System.Environment       (getArgs)
import           System.IO                (IOMode (ReadMode), openFile)
import qualified System.IO.Streams        as S
import           System.IO.Streams.Binary (binaryInputStream)
import           System.IO.Streams.List   (outputToList)

main :: IO ()
main = do
    filename : _ <- getArgs
    h <- openFile filename ReadMode
    s <- S.handleToInputStream h
    i <- binaryInputStream s :: IO (S.InputStream Word32)
    r <- outputToList $ S.connect i
    print $ last r

'壞'意味着:時間和空間要求很高,不處理Decode異常。

您的直接問題是由您如何使用leftover引起的。 該函數用於“提供當前monadic綁定中下一個組件消耗的單個剩余輸入”,因此當您在使用transform進行循環之前給它bs時,您實際上會丟棄其余的bytestring(即什么是bs之后)。

根據你的代碼正確的解決方案將使用增量輸入接口Data.Binary.Get更換你yield / leftover的東西,完全消耗每個塊的組合。 但是,更實用的方法是使用binary-conduit軟件包,它提供了conduitGet (它的源代碼可以很好地了解“手動”實現的樣子):

import           Data.Conduit.Serialization.Binary

-- etc.

transform :: (Monad m, MonadResource m) => Conduit BS.ByteString m Word32
transform = conduitGet G.getWord32be

需要注意的是,如果總字節數不是4的倍數(即最后一個Word32不完整),則會拋出一個解析錯誤。 在不太可能的情況下,不是你想要的,一個懶惰的出路只是在輸入字節\\bs -> C.take (4 * truncate (C.length bs / 4)) bs使用\\bs -> C.take (4 * truncate (C.length bs / 4)) bs

使用pipes (以及pipes-grouppipes-bytestring ),演示問題會減少到組合器。 首先,我們將傳入的未分化字節流解析為小的4字節塊:

chunksOfStrict :: (Monad m) => Int -> Producer ByteString m r -> Producer ByteString m r
chunksOfStrict n = folds mappend mempty id . view (Bytes.chunksOf n) 

然后我們將這些映射到Word32並且(這里)計算它們。

main :: IO ()
main = do
   filename:_ <- getArgs
   IO.withFile filename IO.ReadMode $ \h -> do
     n <- P.length $ chunksOfStrict 4 (Bytes.fromHandle h) >-> P.map getWord32
     print n

如果我們有少於4個字節或者無法解析,這將失敗,但我們也可以映射

getMaybeWord32 :: ByteString -> Maybe Word32
getMaybeWord32 bs = case  G.runGetOrFail G.getWord32be $ BL.fromStrict bs of
  Left r -> Nothing
  Right (_, off, w32) -> Just w32

然后,以下程序將打印有效4字節序列的解析

main :: IO ()
main = do
   filename:_ <- getArgs
   IO.withFile filename IO.ReadMode $ \h -> do
     runEffect $ chunksOfStrict 4 (Bytes.fromHandle h) 
                 >-> P.map getMaybeWord32
                 >-> P.concat  -- here `concat` eliminates maybes
                 >-> P.print 

當然,還有其他方法可以處理失敗的解析。

但是,這里更接近你要求的程序。 從字節流( Producer ByteString mr )獲取一個四字節段,如果它足夠長,則將其讀取為Word32 ; 然后它接受許多傳入的字節並將它們累積到一個惰性字節串中,從而產生它。 它只是重復這個,直到它用完了字節。 在下面的main中,我打印每個產生的延遲字節串:

module Main (main) where 
import Pipes 
import qualified Pipes.Prelude as P
import Pipes.Group (folds) 
import qualified Pipes.ByteString as Bytes ( splitAt, fromHandle, chunksOf )
import Control.Lens ( view ) -- or Lens.Simple (view) -- or Lens.Micro ((.^))
import qualified System.IO as IO ( IOMode(ReadMode), withFile )
import qualified Data.Binary.Get as G ( runGet, getWord32be )
import Data.ByteString ( ByteString )
import qualified Data.ByteString.Lazy.Char8 as BL 
import System.Environment ( getArgs )

splitLazy :: (Monad m, Integral n) =>
   n -> Producer ByteString m r -> m (BL.ByteString, Producer ByteString m r)
splitLazy n bs = do
  (bss, rest) <- P.toListM' $ view (Bytes.splitAt n) bs
  return (BL.fromChunks bss, rest)

measureChunks :: Monad m => Producer ByteString m r -> Producer BL.ByteString m r
measureChunks bs = do
 (lbs, rest) <- lift $ splitLazy 4 bs
 if BL.length lbs /= 4
   then rest >-> P.drain -- in fact it will be empty
   else do
     let w32 = G.runGet G.getWord32be lbs
     (lbs', rest') <- lift $ splitLazy w32 bs
     yield lbs
     measureChunks rest

main :: IO ()
main = do
  filename:_ <- getArgs
  IO.withFile filename IO.ReadMode $ \h -> do
     runEffect $ measureChunks (Bytes.fromHandle h) >-> P.print

這又是粗糙的,它使用runGet而不是runGetOrFail ,但這很容易修復。 管道標准過程是在失敗的解析上停止流轉換並返回未解析的字節流。

如果您預期Word32s是大數字,那么您不希望將相應的字節流累積為惰性字節串,但是如果將它們寫入不同的文件而不累積,我們可以很容易地更改程序。 這將需要復雜的導管使用,但是pipesstreaming的首選方法。

這是一個相對簡單的解決方案,我想投入到戒指中。 它是一個重復使用splitAt包裝到State monad中,它提供了與Data.Binary.Get (的一個子集)相同的接口。 得到的[ByteString]是在main獲得的, whileJust getBlob

module Main (main) where

import           Control.Monad.Loops
import           Control.Monad.State
import qualified Data.Binary.Get      as G (getWord32be, runGet)
import qualified Data.ByteString.Lazy as BL
import           Data.Int             (Int64)
import           Data.Word            (Word32)
import           System.Environment   (getArgs)

-- this is going to mimic the Data.Binary.Get.Get Monad
type Get = State BL.ByteString

getWord32be :: Get (Maybe Word32)
getWord32be = state $ \bs -> do
    let (w, rest) = BL.splitAt 4 bs
    case BL.length w of
        4 -> (Just w', rest) where
            w' = G.runGet G.getWord32be w
        _ -> (Nothing, BL.empty)

getLazyByteString :: Int64 -> Get BL.ByteString
getLazyByteString n = state $ \bs -> BL.splitAt n bs

getBlob :: Get (Maybe BL.ByteString)
getBlob = do
    ml <- getWord32be
    case ml of
        Nothing -> return Nothing
        Just l -> do
            blob <- getLazyByteString (fromIntegral l :: Int64)
            return $ Just blob

runGet :: Get a -> BL.ByteString -> a
runGet g bs = fst $ runState g bs

main :: IO ()
main = do
    fname <- head <$> getArgs
    bs <- BL.readFile fname
    let ls = runGet loop bs where
        loop = whileJust getBlob return
    print $ length ls

getBlob沒有錯誤處理,但它很容易擴展。 只要仔細使用結果列表,時間和空間的復雜性就非常好。 (創建一些供上述消費的隨機數據的python腳本在這里 )。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM