我编写了一个小的Haskell程序来打印当前目录中所有文件的MD5校验和(递归搜索)。基本上是md5deep
的Haskell版本。一切都很好,花花公子,除非当前目录有大量的文件,在这种情况下,我得到一个错误:
<program>: <currentFile>: openBinaryFile: resource exhausted (Too many open files)
似乎Haskell的懒惰导致它不会关闭文件,即使在相应的输出行已经完成之后也是如此。
相关代码如下。感兴趣的功能是getList
。
import qualified Data.ByteString.Lazy as BS
main :: IO ()
main = putStr . unlines =<< getList "."
getList :: FilePath -> IO [String]
getList p =
let getFileLine path = liftM (\c -> (hex $ hash $ BS.unpack c) ++ " " ++ path) (BS.readFile path)
in mapM getFileLine =<< getRecursiveContents p
hex :: [Word8] -> String
hex = concatMap (\x -> printf "%0.2x" (toInteger x))
getRecursiveContents :: FilePath -> IO [FilePath]
-- ^ Just gets the paths to all the files in the given directory.
关于如何解决这个问题有什么想法吗?
整个计划可在此处获取:http://haskell.pastebin.com/PAZm0Dcb
编辑:我有大量不适合RAM的文件,所以我不是在寻找一种能够将整个文件一次性读入内存的解决方案。
答案 0 :(得分:27)
您无需使用任何特殊的IO方式,只需更改执行操作的顺序即可。因此,不是打开所有文件然后处理内容,而是打开一个文件并一次打印一行输出。
import Data.Digest.Pure.MD5 (md5)
import qualified Data.ByteString.Lazy as BS
main :: IO ()
main = mapM_ (\path -> putStrLn . fileLine path =<< BS.readFile path)
=<< getRecursiveContents "."
fileLine :: FilePath -> BS.ByteString -> String
fileLine path c = hash c ++ " " ++ path
hash :: BS.ByteString -> String
hash = show . md5
顺便说一下,我碰巧使用的是另一个md5哈希库,区别并不显着。
这里发生的主要事情是:
mapM_ (\path -> putStrLn . fileLine path =<< BS.readFile path)
它正在打开一个文件,它占用了文件的全部内容并打印了一行输出。它会关闭文件,因为它占用了文件的全部内容。以前,当文件被占用时,你会延迟文件被关闭时延迟。
如果您不确定是否正在使用所有输入但想要确保文件仍然关闭,那么您可以使用withFile
中的System.IO
函数:
mapM_ (\path -> withFile path ReadMode $ \hnd -> do
c <- BS.hGetContents hnd
putStrLn (fileLine path c))
withFile
函数打开文件并将文件句柄传递给body函数。它保证在正文返回时文件被关闭。在处理昂贵的资源时,这种“withBlah”模式非常普遍。 System.Exception.bracket
直接支持此资源模式。
答案 1 :(得分:11)
懒惰的IO很容易出错。
正如dons建议的那样,你应该使用严格的IO。
您可以使用Iteratee等工具来帮助您构建严格的IO代码。我最喜欢的工作是monadic列表。
import Control.Monad.ListT (ListT) -- List
import Control.Monad.IO.Class (liftIO) -- transformers
import Data.Binary (encode) -- binary
import Data.Digest.Pure.MD5 -- pureMD5
import Data.List.Class (repeat, takeWhile, foldlL) -- List
import System.IO (IOMode(ReadMode), openFile, hClose)
import qualified Data.ByteString.Lazy as BS
import Prelude hiding (repeat, takeWhile)
hashFile :: FilePath -> IO BS.ByteString
hashFile =
fmap (encode . md5Finalize) . foldlL md5Update md5InitialContext . strictReadFileChunks 1024
strictReadFileChunks :: Int -> FilePath -> ListT IO BS.ByteString
strictReadFileChunks chunkSize filename =
takeWhile (not . BS.null) $ do
handle <- liftIO $ openFile filename ReadMode
repeat () -- this makes the lines below loop
chunk <- liftIO $ BS.hGet handle chunkSize
when (BS.null chunk) . liftIO $ hClose handle
return chunk
我在这里使用了“pureMD5”软件包,因为“Crypto”似乎没有提供“流式”md5实现。
monadic列表/ ListT
来自hackage上的“List”包(变形金刚'和mtl的ListT
已被破坏,并且没有像takeWhile
这样的有用函数<) p>
答案 2 :(得分:6)
注意:我已略微编辑了我的代码以反映Duncan Coutts's answer中的建议。即使在这次编辑之后,他的回答显然比我的好得多,并且似乎没有以同样的方式耗尽内存。
这是我对基于Iteratee
的版本的快速尝试。当我在一个包含大约2,000个小(30-80K)文件的目录上运行它时,它比your version here快大约30倍,并且似乎使用了更少的内存。
出于某种原因,它似乎仍然在非常大的文件上耗尽内存 - 我还不太了解Iteratee
,但却无法轻易地告诉原因。
module Main where
import Control.Monad.State
import Data.Digest.Pure.MD5
import Data.List (sort)
import Data.Word (Word8)
import System.Directory
import System.FilePath ((</>))
import qualified Data.ByteString.Lazy as BS
import qualified Data.Iteratee as I
import qualified Data.Iteratee.WrappedByteString as IW
evalIteratee path = evalStateT (I.fileDriver iteratee path) md5InitialContext
iteratee :: I.IterateeG IW.WrappedByteString Word8 (StateT MD5Context IO) MD5Digest
iteratee = I.IterateeG chunk
where
chunk s@(I.EOF Nothing) =
get >>= \ctx -> return $ I.Done (md5Finalize ctx) s
chunk (I.Chunk c) = do
modify $ \ctx -> md5Update ctx $ BS.fromChunks $ (:[]) $ IW.unWrap c
return $ I.Cont (I.IterateeG chunk) Nothing
fileLine :: FilePath -> MD5Digest -> String
fileLine path c = show c ++ " " ++ path
main = mapM_ (\path -> putStrLn . fileLine path =<< evalIteratee path)
=<< getRecursiveContents "."
getRecursiveContents :: FilePath -> IO [FilePath]
getRecursiveContents topdir = do
names <- getDirectoryContents topdir
let properNames = filter (`notElem` [".", ".."]) names
paths <- concatForM properNames $ \name -> do
let path = topdir </> name
isDirectory <- doesDirectoryExist path
if isDirectory
then getRecursiveContents path
else do
isFile <- doesFileExist path
if isFile
then return [path]
else return []
return (sort paths)
concatForM :: (Monad m) => [a1] -> (a1 -> m [a]) -> m [a]
concatForM xs f = liftM concat (forM xs f)
请注意,您需要iteratee
包和TomMD的pureMD5
。 (如果我在这里做了一些可怕的事情,我很抱歉 - 我是这个人的初学者。)
答案 3 :(得分:3)
编辑:我的假设是用户打开了数千个非常小的文件,结果发现它们非常大。懒惰将是至关重要的。
好吧,你需要使用不同的IO机制。之一:
我也强烈建议不要使用'unpack',因为这会破坏使用字节串的好处。
例如,您可以使用System.IO.Strict替换惰性IO,并产生:
import qualified System.IO.Strict as S
getList :: FilePath -> IO [String]
getList p = mapM getFileLine =<< getRecursiveContents p
where
getFileLine path = liftM (\c -> (hex (hash c)) ++ " " ++ path)
(S.readFile path)
答案 4 :(得分:2)
问题是mapM并不像你想象的那样懒惰 - 它会产生一个完整的列表,每个文件路径有一个元素。您使用的文件IO 是懒惰的,因此您会得到一个列表,其中每个文件路径都有一个打开的文件。
在这种情况下,最简单的解决方案是强制评估每个文件路径的哈希值。一种方法是使用Control.Exception.evaluate
:
getFileLine path = do
theHash <- liftM (\c -> (hex $ hash $ BS.unpack c) ++ " " ++ path) (BS.readFile path)
evaluate theHash
正如其他人所指出的那样,我们正在努力替换当前的懒惰IO方法,这种方法更为通用但仍然很简单。
答案 5 :(得分:0)
没问题,只需显式打开文件(openFile),读取内容(Data.ByteString.Lazy.hGetContents),执行md5哈希(让!h = md5内容),并显式关闭文件(hClose)。
答案 6 :(得分:0)
我想到的另一个解决方案是使用unsafeInterleaveIO
中的System.IO.Unsafe
。请参阅Haskell Cafe的this thread中Tomasz Zielonka的回复。
它推迟输入输出操作(打开文件),直到实际需要它为止。因此,可以避免一次打开所有文件,而是按顺序读取和处理它们(懒惰地打开它们)。
现在,我相信,mapM getFileLine
会打开所有文件,但在putStr . unlines
之前不会开始阅读这些文件。因此很多带有打开文件处理程序的thunk都会浮动,这就是问题所在。 (如果我错了,请纠正我。)
modified example with unsafeInterleaveIO
现在在一个100 GB的目录中运行几分钟,在恒定的空间内。
getList :: FilePath -> IO [String]
getList p =
let getFileLine path =
liftM (\c -> (show . md5 $ c) ++ " " ++ path)
(unsafeInterleaveIO $ BS.readFile path)
in mapM getFileLine =<< getRecursiveContents p
(我为hash的pureMD5实现改了)
P.S。我不确定这是不是好风格。我相信具有迭代和严格IO的解决方案更好,但这个更快。我在小脚本中使用它,但我害怕在更大的程序中依赖它。