Haskell如何将文件分解为多个文件

  • 本文关键字:文件 分解 Haskell haskell
  • 更新时间 :
  • 英文 :


我只是在学习haskell,我想做一个非常简单的程序,当有空行时,它会获取一个大文件并将其拆分为多个文件。

Line 1: skdjsakjadsldas
Line 2: sadjkndasjkdsajk
Line 3: sadojdadjisod
Line 4:
Line 5: asdjdashjkda
Line 6: asdiadsjidji

上面的文件将变成两个文件,一个包含前3行,另一个包含最后2行。为了做到这一点,我做了以下功能:

consumeLines :: Handle -> Handle -> IO ()
consumeLines handleRead handleWrite = do
result <- hIsEOF handleRead
unless result $ do
contents <- hGetLine handleRead
when (contents /= "") $ do 
hPutStrLn handleWrite contents 
consumeLines handleRead handleWrite

这个想法是给一个Handler提供我在文件中的位置,给另一个Handler一个我要写当前信息的文件。我的问题是,是否有更聪明的方法可以做到这一点?我有点害怕使用hGetContents和其他方法,因为我可能会把整个文件加载到我的机器中。这有点令人费解,因为在每一步我都必须创建一个新文件来写入行。有什么想法吗?

在Haskell中解决此问题的传统方法是:

  1. 加载输入文件

  2. 将输入拆分为行

  3. 按空行将行分组为段落

  4. 为每个段落生成一个文件名

  5. 将每个段落的行写入相应的文件

这是一个适合懒惰I/O的问题。对于#1,您可以简单地使用readFile,它生成一个字符串,当您遍历文件时,该字符串将按需延迟读取文件:

getParagraphs :: FilePath -> IO [String]
getParagraphs path = do
input <- readFile inputHandle
-- …

我有点害怕使用hGetContents和其他方法,因为我可能会把整个文件加载到我的机器中。

这是一个合理的担忧!如果您的代码保留了对readFilehGetContents的整个输入字符串的引用,那么它将把整个文件保存在内存中,而不仅仅是保留必要的部分。这是在较大的程序中避免懒惰I/O的一个原因,在那里更容易犯错误,但这里的程序足够小,我们可以很容易地验证它

对于#2和#3,将文件拆分为行和段,然后可以使用纯linesgroupBy函数!

import Data.Function (on)
import Data.List (groupBy)
getParagraphs :: FilePath -> IO [String]
getParagraphs path = do
input <- readFile path
pure $ splitParagraphs input
-- Or: getParagraphs path = splitParagraphs <$> readFile path
-- Or: getParagraphs path = fmap splitParagraphs (readFile path)
-- Or: getParagraphs = fmap splitParagraphs . readFile
splitParagraphs :: String -> [String]
splitParagraphs
= map unlines               -- Concatenate back into paragraphs
. filter (not . any null)   -- Remove runs containing empty lines
. groupBy ((==) `on` null)  -- Group empty and non-empty lines
. lines                     -- Split into lines

一般来说,将I/O限制在程序中做…好吧,输入和输出的部分被认为是一种好的风格!大多数实际计算都可以用纯代码完成。

然后,您可以单独生成要向其中写入段落的文件路径列表(#4(,并使用for_遍历为每个路径调用writeFile(#5(:

import Data.Foldable (for_)
writeFiles :: FilePath -> Int -> String -> [String] -> IO ()
writeFiles prefix startIndex extension contents = do
for_ (zip paths contents) $  (path, content) -> do
writeFile path content
where
paths = map makePath [startIndex ..]
makePath i = concat [prefix, show i, ".", extension]

或者更好的是,您可以使用一元zipWith,特别是zipWithM_来避免累积结果列表,因为writeFile操作都返回伪单位()值:

import Control.Monad (zipWithM_)
-- …
writeFiles prefix startIndex extension contents
= zipWithM_ writeFile paths contents
where
-- …

然后,您可以在主程序中简单地将这些连接起来。

main :: IO ()
main = do
paragraphs <- readParagraphs "input.txt"
writeFiles "output" 1 "txt" paragraphs    -- output1.txt, output2.txt, &c.
-- Or: readParagraphs "input.txt" >>= writeFiles "output" 1 "txt"

(我刚刚使用了FilePath和常量文件进行说明;将其调整为使用Handles和动态输入非常简单。(

在这种情况下,您可以确信readFile的结果不会被不必要地保留,因为它的每个使用者splitParagraphswriteFiles只包含对输入的单个线性遍历。

然而,在更复杂的情况下,惰性I/O可能会使您很容易犯资源错误,即您保留文件内容的时间超过了必要的时间,或者保持文件Handle的打开时间超过了需要的时间,更糟糕的是,在实际使用完Handle之前,不经意地关闭了它!

这些问题的一般解决方案可在具有资源意识的流式传输包中找到,如管道、导管和流式传输。这些库可以帮助您保证程序的资源使用,同时仍然以方便的方式交织I/O效果和纯数据处理。然而,探索每一个图书馆本身就是一个答案。

我将从以下内容开始:

main = readFile path >>= return . zip [1..] . splitFile >>= writeNthFile path

然后只需实现splitFile和writeNthFile。

最新更新