我正在尝试使用Haskell中的pipes-attoparsec解析二进制数据。管道(代理)涉及的原因是将读取与解析交错以避免大文件的高内存使用。许多二进制格式基于块(或块),它们的大小通常由文件中的字段描述。我不确定这个块的解析器是什么被调用,但这就是我所说的标题中的“sub-parser”。我遇到的问题是以简洁的方式实现它们而没有可能很大的内存占用。我想出了两个在某些方面都失败的替代方案。
备选方案1是将块读入单独的字节串并为其启动单独的解析器。简洁,大块将导致高内存使用。
备选方案2是在同一上下文中保持解析并跟踪消耗的字节数。这种跟踪容易出错,并且似乎会影响组成最终blockParser的所有解析器。对于格式错误的输入文件,在比较跟踪大小之前,通过进一步解析大小字段可能会浪费时间。
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
答案 0 :(得分:2)
我喜欢称之为“固定输入”解析器。
我可以告诉你pipes-parse
将如何做到这一点。您可以在库的parseN
和parseWhile
函数中查看我要在pipes-parse
中描述的内容的预览。这些实际上是针对通用输入的,但我也编写了类似的String
解析器以及here和here。
诀窍很简单,你在输入标记的一端插入一个你希望解析器停止的地方,运行解析器(如果遇到输入标记的假端则会失败),然后删除输入标记的结尾
显然,这并不像我说的那么容易,但这是一般原则。棘手的部分是:
以这样的方式进行操作,使其仍然流式传输。我链接的那个并没有这样做,但是,你以流方式执行此操作的方式是在上游插入一个管道,计算流经它的字节数,然后将输入结束标记插入正确的位置。 / p>
不干扰输入标记的现有结尾
这个技巧可以适用于pipes-attoparsec
,但我认为最好的解决方案是attoparsec
直接包含此功能。但是,如果该解决方案不可用,那么我们可以限制馈送到attoparsec
解析器的输入。
答案 1 :(得分:2)
好的,所以我终于想出了如何做到这一点,并且我已经在pipes-parse
库中编写了这种模式。 pipes-parse
tutorial解释了如何执行此操作,特别是在“嵌套”部分中。
本教程仅解释了这种与数据类型无关的解析(即通用的元素流),但您也可以将其扩展为与ByteString
一起使用。
使这项工作的两个关键技巧是:
将StateP
修改为全球(pipes-3.3.0
)
将子解析器嵌入到瞬态StateP
层中,以便它使用新的剩余上下文
pipes-attoparsec
即将发布基于pipes-parse
的更新,以便您可以在自己的代码中使用这些技巧。