【问题标题】:"Sub-parsers" in pipes-attoparsec管道 attoparsec 中的“子解析器”
【发布时间】:2013-03-15 18:15:06
【问题描述】:

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

备选方案 1 是将块读入单独的字节串并为其启动单独的解析器。虽然简洁,但大块会导致高内存使用。

备选方案 2 是在相同的上下文中保持解析并跟踪消耗的字节数。这种跟踪很容易出错,并且似乎感染了构成最终 blockParser 的所有解析器。对于格式错误的输入文件,在比较跟踪的大小之前,它还可能会在比 size 字段指示的范围内进一步解析,从而浪费时间。

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

【问题讨论】:

    标签: parsing haskell attoparsec haskell-pipes


    【解决方案1】:

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

    我可以告诉你pipes-parse 将如何做到这一点。您可以在库的parseNparseWhile 函数中看到我将在pipes-parse 中描述的内容的预览。这些实际上是用于通用输入,但我写了类似的输入,例如 String 解析器以及 herehere

    技巧很简单,在你希望解析器停止的地方插入一个假的输入标记结束,运行解析器(如果它碰到输入标记的假结束,它将失败),然后删除输入标记的结尾.

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

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

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

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

    【讨论】:

    • 插入一个计算上游的管道听起来很有趣,但是它怎么知道要计算多少字节呢?这个值只能被下游的解析器发现,不能直接以该值作为参数调用request,因为它是由parserD运行的。
    • @absence 好吧,暂时忽略管道-attoparsec 接口,因为 Renzo 和我会尽快修复它。固定输入解析器在内部使用限制字节数的管道。可以这样想:parser1 &gt;&gt; (restrict n &gt;-&gt; parser2) &gt;&gt; parser3。固定宽度组合在给定解析器的上游插入类似restrict 的东西。它比这更复杂,但在精神上非常相似。
    • 链接失效
    【解决方案2】:

    好的,所以我终于想出了如何做到这一点,我已经在pipes-parse 库中编写了这个模式。 pipes-parse tutorial 解释了如何执行此操作,特别是在“嵌套”部分。

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

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

    • StateP 修复为全局(在pipes-3.3.0 中)

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

    pipes-attoparsec 即将发布基于pipes-parse 的更新,以便您可以在自己的代码中使用这些技巧。

    【讨论】:

    • 我可以在 Data.Attoparsec.Parser 中调用 passUpTo 吗,就像我的示例中的解析器函数一样?还是组合多个小型 parseD 代理而不是使用一个巨大的 Parser 更好,虽然它由较小的 Parsers 组成,但对于 pipe-attoparsec 来说是一个黑盒子?
    • 您希望parseD 循环小Parsers,因为在每个Parser 完成之前它无法释放内存。 attoparsecParser 完成之前永远不会释放输入,因为它始终保留在 Parser 内回溯的权利。在常量内存中解析某些内容的唯一方法是识别流中可以安全刷新先前输入的边界。例如,如果您正在解析一个巨大的 CSV 文件,那么您可以为名为 parseLine 的 CSV 文件的每一行定义一个 Parser,然后只需运行 parseD 即可生成解析后的行流。
    • @absence@ 此外,pipes-bytestring 将提供一个passBytesUpTo 原语,允许您将整个attoparsec 解析器分隔到一个固定输入,同时仍然在恒定内存中流式传输。这可能更接近你想要的。这个想法是你将passBytesUpTo upstream 放在Control.Proxy.Attoparsec.parse 的调用中,它将以固定的字节数运行该解析器。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-13
    • 2017-11-23
    • 2020-05-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多