5

我正在尝试使用 Haskell 中的管道 attoparsec 解析二进制数据。涉及管道(代理)的原因是将读取与解析交错以避免大文件的高内存使用。许多二进制格式基于块(或块),它们的大小通常由文件中的字段描述。我不确定这样一个块的解析器被称为什么,但这就是我在标题中所说的“子解析器”的意思。我遇到的问题是以简洁的方式实现它们,而不会占用大量内存。我想出了两个在某些方面都失败的替代方案。

备选方案 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
4

2 回答 2

2

我喜欢称其为“固定输入”解析器。

我可以告诉你怎么pipes-parse做。pipes-parse您可以在库的parseNparseWhile函数中看到我将要描述的内容的预览。这些实际上是用于通用输入的,但我在此处此处String也为示例解析器编写了类似的输入。

诀窍非常简单,您在希望解析器停止的地方插入一个假的输入标记结尾,运行解析器(如果它碰到输入标记的假结尾,它将失败),然后删除输入标记的结尾。

显然,这并不像我说的那么容易,但这是一般原则。棘手的部分是:

  • 以仍然流式传输的方式进行操作。我链接的那个还没有这样做,但是您以流方式执行此操作的方式是在上游插入一个管道,该管道计算流过它的字节数,然后在正确的位置插入输入结束标记。

  • 不干扰现有的输入结束标记

这个技巧可以适应pipes-attoparsec,但我认为最好的解决方案是attoparsec直接包含这个特性。但是,如果该解决方案不可用,那么我们可以限制提供给attoparsec解析器的输入。

于 2013-03-16T00:37:21.813 回答
2

好的,所以我终于想出了如何做到这一点,并且我已经在pipes-parse库中编写了这个模式。本pipes-parse教程解释了如何做到这一点,特别是在“嵌套”部分。

本教程仅对与数据类型无关的解析(即通用元素流)进行了解释,但您也可以将其扩展为与ByteStrings 一起使用。

实现这项工作的两个关键技巧是:

  • 修复StateP为全局(in pipes-3.3.0

  • 将子解析器嵌入瞬态StateP层,以便它使用新的剩余上下文

pipes-attoparsec很快发布一个更新,pipes-parse以便您可以在自己的代码中使用这些技巧。

于 2013-06-03T19:56:46.823 回答