![](/img/trans.png)
[英]Haskell streaming - how to separate 1 stream into 2 after copy?
[英]Efficient streaming and manipulation of a byte stream in Haskell
在為大型(<bloblength><blob>)*
編碼的二進制文件編寫反序列化器時,我遇到了各種Haskell生成轉換消耗庫。 到目前為止,我知道有四個流媒體庫:
conduit
( Haskell Cast#6很好地揭示了conduit
和pipes
之間的差異) 這是一個精簡的示例,當我嘗試使用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
是錯誤的。 我看到兩個解決方案的路線:
await
不想進入ByteStream
的第二個塊,那么還有另一個函數可以提取下一個塊嗎? 在我看過的例子中(例如, 管道101 ),這不是它的完成方式 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-group
和pipes-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
是大數字,那么您不希望將相應的字節流累積為惰性字節串,但是如果將它們寫入不同的文件而不累積,我們可以很容易地更改程序。 這將需要復雜的導管使用,但是pipes
和streaming
的首選方法。
這是一個相對簡單的解決方案,我想投入到戒指中。 它是一個重復使用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.