Haskell 读/写二进制文件完整工作示例

3
我希望如果有人能提供完整的工作代码,使得在Haskell中可以执行以下操作:
从二进制文件中读取一个非常大的序列(超过10亿个元素)的32位int值到适当的容器中(例如肯定不是列表,因为存在性能问题),并且如果每个数小于1000(十进制),则将其加倍,然后将结果的32位int值写入另一个二进制文件。我可能不想一次在内存中读取整个二进制文件的所有内容。我想在上一个块之后读取一个块。
我感到困惑,因为我几乎找不到关于这个的文档。Data.Binary,ByteString,Word8等等,这只会增加混淆。在C / C ++中,这样的问题有相当简单的解决方案。取一个所需大小的数组(例如unsigned int),使用read / write库调用即可完成。在Haskell中,对我来说似乎不那么容易。
如果您的解决方案使用最佳的标准包,并且可与主流Haskell(> GHC 7.10)一起使用,而不是某些模糊/过时的包,则我将不胜感激。
我从这些页面中阅读

https://wiki.haskell.org/Binary_IO

https://wiki.haskell.org/Dealing_with_binary_data

3个回答

4
如果您正在进行二进制I/O操作,您几乎肯定需要使用ByteString来处理实际的输入/输出部分。请看一下它提供的hGethPut函数。(或者,如果您只需要严格的线性访问,可以尝试使用惰性I/O,但很容易出错。)
当然,字节字符串只是字节的数组;您接下来的问题是将这些字节解释为字符/整数/双精度浮点数/其他所需类型。有一些包可用于此,但Data.Binary似乎是最主流的一个。 binary的文档似乎希望引导您使用Binary类,在其中编写序列化和反序列化整个对象的代码。但是您可以使用Data.Binary.GetData.Binary.Put中的函数来处理单个项。在那里,您会发现诸如getWord32be(获取大端模式下的Word32)等函数。
现在有了工作代码:
module Main where

import Data.Word
import qualified Data.ByteString.Lazy as BIN
import Data.Binary.Get
import Data.Binary.Put
import Control.Monad
import System.IO

main = do
  h_in  <- openFile "Foo.bin" ReadMode
  h_out <- openFile "Bar.bin" WriteMode
  replicateM 1000 (process_chunk h_in h_out)
  hClose h_in
  hClose h_out

chunk_size = 1000
int_size = 4

process_chunk h_in h_out = do
  bin1 <- BIN.hGet h_in chunk_size
  let ints1 = runGet (replicateM (chunk_size `div` int_size) getWord32le) bin1
  let ints2 = map (\ x -> if x < 1000 then 2*x else x) ints1
  let bin2 = runPut (mapM_ putWord32le ints2)
  BIN.hPut h_out bin2

我相信这就是您要求的内容。它读取1000个大小为chunk_size字节的块,将每一个块转换为Word32列表(所以它只有chunk_size / 4个整数在内存中),执行您指定的计算,并将结果写回。当然,如果您真的需要这样做,您需要检查EOF等情况。

1
已导入6个库!“..你会想要EOF检查..”-我想知道:EOF检查是否需要其他包,如Data.Binary.EOF或某些异常Monad?而且仅仅为了添加一个简单的EOF检查,就需要再添加另外100行代码吗?我希望情况不是那么糟糕。我正在尝试添加EOF检查,一旦能够完成这个“壮举”,我将标记您的解决方案为已接受。遗憾的是,官方文档没有提供一个可用的示例。 - mntk123
事实证明,在这个特定的例子中,实际上并不需要导入 Data.Word。如果停止使用 replicateM,也可以删除 Control.Monad。你可以使用已经导入的 System.IO 中的 hIsEOF 来检查 EOF,只需一个函数调用即可。哦,而且我毫不知情,你是从哪里得到 System.IO 不可靠的印象的?也许说这话的人是在谈论该模块中的特定功能? - MathematicalOrchid
与惰性 I/O 相比,后者提供了恒定的内存使用,但以确定资源使用为代价。强烈建议在实际用例中使用 Data.Conduit.Binary。(来源:fpcomplete)像往常一样,没有提供任何简单易上手的示例 :(. 也许惰性 I/O 的问题不适用于我的示例(或可能适用),我不知道。 - mntk123
好的,我已经解决了EOF部分。所以接受你的答案。不过我还遇到了另一个关于EOF的问题。我会为此提出另一个问题。 - mntk123
1
@mntk123 replicateM 1000 的意思是“读取1000个块”。而 chunk_size 是一个块中有多少字节。这些数字并不特别,我只是选择了一些示例值。实际上,你可能不会真的读取1000个块,你会编写一个循环来检查EOF或其他情况。 - MathematicalOrchid
显示剩余5条评论

2

在Haskell中处理二进制I/O的最佳方法是使用bytestrings。惰性bytestrings提供了缓冲I/O,因此您甚至不需要关心它。

下面的代码假定块大小是32位的倍数(实际上确实如此)。

module Main where

import Data.Word
import Control.Monad
import Data.Binary.Get
import Data.Binary.Put
import qualified Data.ByteString.Lazy as BS
import qualified Data.ByteString as BStrict

-- Convert one bytestring chunk to the list of integers
-- and append the result of conversion of the later chunks.
-- It actually appends only closure which will evaluate next
-- block of numbers on demand.
toNumbers :: BStrict.ByteString -> [Word32] -> [Word32]
toNumbers chunk rest = chunkNumbers ++ rest
    where
    getNumberList = replicateM (BStrict.length chunk `div` 4) getWord32le
    chunkNumbers = runGet getNumberList (BS.fromStrict chunk)

main :: IO()
main = do
    -- every operation below is done lazily, consuming input as necessary
    input <- BS.readFile "in.dat"
    let inNumbers = BS.foldrChunks toNumbers [] input
    let outNumbers = map (\x -> if x < 1000 then 2*x else x) inNumbers
    let output = runPut (mapM_ putWord32le outNumbers)
    -- There lazy bytestring output is evaluated and saved chunk
    -- by chunk, pulling data from input file, decoding, processing
    -- and encoding it back one chunk at a time
    BS.writeFile "out.dat" output

0

hGetLine 似乎是读取一行的函数。我想要读取的文件可能只有一行,其中包含十亿个整数并排放置。换句话说,如果出现 newline,那么它实际上是数据值(整数)的一部分。 - mntk123
2
我说过:_将hGetLine替换为读取4个字节的函数_,你应该能够在我提供的链接中找到这样的函数。 - ErikR
1
抱歉,但这正是我困惑的地方。 :( 你能修改你的代码以实现某些魔法吗? - mntk123
那么尝试一下该页面上列出的hGet函数怎么样? - ErikR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接