Haskell的`readFile`函数会将整个文件内容读入内存吗?

3

我希望在Haskell程序中选择一个大文本文件(约10GB)中的第n个最后一行。

我找到了一种从“内部”字符串获取倒数第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

readFile函数的文档说明称它是“惰性读取内容”,那么一旦readFile到达倒数第n行时,它是否会将前面的所有行存储在内存中(然后由于我没有那么多内存而发生错误)?

所以,readFile在这里是否是正确的方法?另外,如何以“惰性方式”从readFile中获取IO String输出,并将其转换为行列表,以便我可以选择倒数第n行?


我认为最有效的方法是从文件末尾以64kb块的方式读取文件,尝试找到倒数第n个换行符,然后从该换行符读取到下一个换行符。您可以使用hSeek和Data.ByteString中的句柄I/O函数来实现此操作。即使使用惰性字符串,在10GB文件中从前面读取也会非常低效。 - Dogbert
3
如果您编写的代码仅对readFile返回的列表执行“单个遍历”,则应该无需将整个列表存储在内存中即可运行。 (请注意,Haskell本地字符串在某种程度上效率低下,惰性字节字符串可能是更好的选择)。或者更准确地说,行将被存储在内存中,但会进行垃圾回收,因此所有内容应以类似恒定的空间运行。 但是,虽然head(drop 10 xs)是单次遍历,但head(drop(length xs-4)xs是双次遍历,这肯定会将整个列表存储在内存中。注意。 - chi
2
我希望有一个适用于这种任务的库。它可以像文本编辑器一样打开大型文本文件,而不必将整个文件加载到RAM中,然后使用类似编辑器的命令进行移动(“向上移动4行”,搜索“foo”等)。手动完成这项工作很麻烦,特别是如果文本文件是UTF8编码的。 - chi
1
有点相关:https://dev59.com/757ha4cB1Zd3GeqPfzVr - danidiaz
@chi 感谢您的回应!关于 drop (length xs - 4) xs,我已经提前知道了行数,这在我的内部字符串演示中用一个单独的变量表示。我不介意对文件进行 一次 完整的遍历,但不想将所有行存储在内存中。 - halloleo
不需要将整个文件加载到内存中:extra包有一个函数takeEnd,可以帮助你。 - beerboy
2个回答

3
这个问题有几个部分:
readFile函数的文档称其“惰性读取内容”,所以一旦readFile到达倒数第n行,它是否已将之前所有行存储在内存中(然后由于我没有那么多内存而爆炸)?
不一定。如果只迭代内容并生成结果,则垃圾回收器应该会释放内容。
那么,在这里使用readFile是正确的方法吗?
我的主观答案是,如果是用于严肃的工具,则readFile不是正确的方法,因为惰性IO是一个麻烦的问题。
如果是用于快速而简单的脚本,则可以使用readFile,但如果不是,并且性能很重要,则最好使用更低级别的调用来读取严格的ByteString,并针对您的问题直接从文件末尾读取并处理。

有关编程的内容:Haskell Lazy ByteString + read/write progress function - Daniel Wagner

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

lines函数在Prelude库中已经足够懒惰,但是在这里编写takeLast时需要一些小心。你可以将其视为在文件的“一个遍历”中操作,查看n个连续行的后续块,直到找到最后一个块。因为它不保留对当前块之前文件内容的任何引用,所以所有文件内容都可以被垃圾回收(通常很快)。


哇,这太酷了!(我需要更详细地研究dropUnary才能理解它...) - halloleo

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