Haskell 的“readFile”是否将整个文件内容读入内存?

Does Haskell's `readFile` read the whole file content into memory?

提问人:halloleo 提问时间:7/2/2022 最后编辑:Sridhar Ratnakumarhalloleo 更新时间:7/4/2022 访问量:499

问:

我想从 Haskell 程序中的大文本文件(~10GB)中选择倒数第一行。n

我找到了一种如何从内部字符串中获取 -th last 的方法:n

myLen = 7
n = 3 -- one-based from the end

myLines = lines myText
idx = myLen - n

theLine = head (drop idx myLines)

main :: IO ()
main = do
  putStrLn theLine

关于该函数的文档说它“懒惰地读取内容”,所以一旦到达最后一行,它会将之前的所有行都存储在内存中吗(然后爆炸,因为我没有那么多内存)?readFilereadFilen

那么,这里是正确的方法吗?另外,如何将“以懒惰的方式”的输出放入行列表中,以便我可以选择最后一行?readFileIO StringreadFilen

字符串 haskell io lazy-evaluation

评论

0赞 Dogbert 7/2/2022
我相信最有效的方法是从末尾读取 64kb 块中的文件,尝试从末尾找到第 n 个换行符,然后从该换行符到下一个换行符进行最终读取。您可以使用 Data.ByteString 中的句柄 I/O 函数来执行此操作。即使使用惰性字符串,在 10gb 的文件中从前面读取也会非常低效。hSeek
3赞 chi 7/2/2022
如果编写的代码仅对 返回的列表执行“单次传递”,则该代码应该运行,而不必将整个列表存储在内存中。(请注意,haskell 原生字符串在某种程度上效率低下,延迟字节串可能是更好的选择)。或者,更准确地说,这些行将存储在内存中,但将被垃圾回收,因此一切都应该在恒定空间中运行。但是,虽然是单通道,但双通道,这肯定会将完整列表存储在内存中。小心。readFilehead (drop 10 xs)head (drop (length xs - 4) xs
2赞 chi 7/2/2022
我希望有一个用于此类任务的库。它允许像在文本编辑器中一样打开一个大的文本文件,而无需在RAM中加载整个文件,然后使用类似编辑器的命令(“上行4行”,搜索“foo”等)。手动执行此操作很麻烦,尤其是在文本文件是 UTF8 编码的情况下。
1赞 danidiaz 7/2/2022
有点相关:stackoverflow.com/questions/41654849/......
0赞 halloleo 7/3/2022
@chi 谢谢你的鸣叫!Re ,我提前知道行数,如我内部字符串演示中的单独变量所示。我不介意对文件进行一次完整的遍历,只是不将所有行都存储在内存中。drop (length xs - 4) xs

答:

3赞 yairchu 7/3/2022 #1

该问题分为几个部分:

关于 readFile 函数的文档说它“懒惰地读取内容”,所以一旦 readFile 到达倒数第 n 行,它是否会将之前的所有行存储在内存中(然后爆炸,因为我没有那么多内存)?

不一定。如果仅循环访问内容并生成结果,则垃圾回收器应解除分配内容。

那么,readFile是正确的方法吗?

我固执己见的回答是,如果它是一个严肃的工具,这不是正确的方法,因为“懒惰 IO”是一罐蠕虫readFile

如果是快速而肮脏的脚本,那就继续吧,但如果不是,如果性能很重要,那么最好使用较低级别的调用来读取严格的 s,对于你的问题,直接从文件末尾读取并处理它。ByteString

评论

0赞 Daniel Wagner 7/4/2022
蠕虫罐上的相关写作:Haskell Lazy ByteString + 读/写进度函数
2赞 Daniel Wagner 7/4/2022 #2

以下程序所需的内存仅与正在读取的文件中最长行的内存一样多:n

-- like drop, but takes its number encoded as a lazy
-- unary number via the length of the first list
dropUnary :: [a] -> [b] -> [b]
dropUnary [] bs = bs
dropUnary (_:as) (_:bs) = dropUnary as bs

takeLast :: Int -> [a] -> [a]
takeLast n as = dropUnary (drop n as) as

main :: IO ()
main = putStrLn . head . takeLast 3 . lines =<< readFile

前奏曲的功能已经很懒惰了,但在这里写了一些小心翼翼的东西。您可以将其视为在文件的“一次传递”中操作,查看连续 n 行的后续块,直到找到最后一个块。由于它不维护对当前正在查看的区块之前的文件内容的任何引用,因此可以对当前区块之前的所有文件内容进行垃圾回收(通常很快就会进行垃圾回收)。linestakeLast

评论

0赞 halloleo 7/4/2022
哇,这太酷了!(我需要更详细地研究才能理解它......dropUnary