2014-11-03 123 views
2

我试图通过haskell读取一个大的csv文件,并生成每列的字数。如何阅读大的CSV文件?

这超过了4M行的文件。

所以我选择读取一个块并获得每次字数(5k行一块)。 而不是总结在一起。

当我用12000行和120000行测试函数时,时间增加几乎是线性的。 但是,当读取180000行时,运行时间超过四次以上。

我认为这是因为内存不够,与磁盘交换使功能慢得多。

我把我的代码写成map/reduce样式,但是如何让haskell不把所有的数据保存在内存中呢?

这次打击是我的代码和分析结果。

import Data.Ord 
import Text.CSV.Lazy.String 
import Data.List 
import System.IO 
import Data.Function (on) 
import System.Environment 

splitLength = 5000 


mySplit' [] = [] 
mySplit' xs = [x] ++ mySplit' t 
    where 
    x = take splitLength xs 
    t = drop splitLength xs     

getBlockCount::Ord a => [[a]] -> [[(a,Int)]] 
getBlockCount t = map 
    (map (\x -> ((head x),length x))) $ 
    map group $ map sort $ transpose t 

foldData::Ord a=> [(a,Int)]->[(a,Int)]->[(a,Int)] 
foldData lxs rxs = map combind wlist 
    where 
     wlist = groupBy ((==) `on` fst) $ sortBy (comparing fst) $ lxs ++ rxs 
     combind xs 
     | 1==(length xs) = head xs 
     | 2 ==(length xs) = (((fst . head) xs), ((snd . head) xs)+((snd . last) xs)) 


loadTestData datalen = do 
    testFile <- readFile "data/test_csv" 
    let cfile = fromCSVTable $ csvTable $ parseCSV testFile 
    let column = head cfile 
    let body = take datalen $ tail cfile 
    let countData = foldl1' (zipWith foldData) $ map getBlockCount $ mySplit' body 
    let output = zip column $ map (reverse . sortBy (comparing snd)) countData 
    appendFile "testdata" $ foldl1 (\x y -> x ++"\n"++y)$ map show $tail output 

main = do 
    s<-getArgs 
    loadTestData $ read $ last s 

剖析结果

loadData +RTS -p -RTS 12000 

total time =  1.02 secs (1025 ticks @ 1000 us, 1 processor) 
total alloc = 991,266,560 bytes (excludes profiling overheads) 

loadData +RTS -p -RTS 120000 

total time =  17.28 secs (17284 ticks @ 1000 us, 1 processor) 
total alloc = 9,202,259,064 bytes (excludes profiling overheads) 



    loadData +RTS -p -RTS 180000 

total time =  85.06 secs (85059 ticks @ 1000 us, 1 processor) 
total alloc = 13,760,818,848 bytes (excludes profiling overheads) 
+1

您需要使用流式库,例如'csv-conduit'或'pipes-csv' – ErikR 2014-11-03 02:43:54

回答

-2

我曾在另一种语言之前有这个问题。诀窍不是将数据读入内存,而是一次只读一行。当你阅读下一行时,只需要覆盖你的变量,因为你只是在寻找一个字数。 只需在您的io流中测试文件状态的EOF结束,然后退出。这样你就不必拆分文件。

希望有帮助

+1

“当您阅读下一行时只是覆盖您的变量,因为您只查找字数。”这是Haskell。我们没有“变数”。 – alternative 2014-11-03 03:22:22

10

所以首先,有几点建议。

  1. 列表并不快。好的,好吧,利弊是恒定的时间,但一般来说,名单并不快。你正在使用列表。 (Data.Sequence对于双端Cons'ing和消费来说会更快)

  2. 字符串很慢。字符串很慢,因为它们是[Char](Char列表)。您当前使用的库是按字符串列表编写的。通常,字符链接列表的链接列表不是您想要进行文本处理的。这不是bueno。在将来使用文本(用于,呃,文本)或ByteString(用于字节)而不是字符串,除非它是小而不是性能敏感的。

  3. 您正在使用的库只是懒惰,而不是流媒体。你必须处理覆盖到惰性语义上的流行为,以获得持续的内存使用。流式库解决了逐步处理数据和限制内存使用的问题。我建议学习Pipes或Conduit来解决这类一般问题。一些问题特定的库还将提供可用于流式传输的迭代器API。 Iteratee API可以直接使用或连接到Pipes/Conduit /等。

我不认为你使用的图书馆是一个好主意。

我建议你使用以下库之一:(基于管道)

http://hackage.haskell.org/package/pipes-csv

https://hackage.haskell.org/package/cassava-0.4.2.0/docs/Data-Csv-Streaming.html(通用CSV库,而不是基于特定的流媒体库)

https://hackage.haskell.org/package/csv-conduit(基于导管)

这些应该会给你良好的性能和持续的内存使用模无论你可能积累。

1

有几件事情需要注意的:

  1. 要传输的数据,以便您在内存中的任何时间仅保持输入文件的一小部分。您可以使用惰性IO和lazy-csv包完成此操作。但是,仍然很容易无意中保留将所有输入保存在内存中的引用。更好的选择是使用流式库,如csv-conduitpipes-csv

  2. 处理大量字符串数据时,使用ByteStringText

  3. 您希望确保在减少数据时使用严格的操作。否则,你只会在记忆中形成大量未评估的表达式,直到最后打印出结果。一个可以构建thunk的地方是你的foldData函数 - 字数表达式看起来并没有减少。

这里是一个程序,它将计算所有的字的总长度在CSV文件中的每一列和做它在恒定存储器的一个例子。的主要特点是:

  • 使用延迟IO
  • 使用lazy-csv包(懒惰)的ByteString代替String
  • 使用BangPatterns到strictify线的数目的计算
  • 使用未装箱阵列保持柱计数器

的代码:

{-# LANGUAGE BangPatterns #-} 

import qualified Data.ByteString.Lazy.Char8 as BS 
import Data.ByteString.Lazy (ByteString) 
import Text.CSV.Lazy.ByteString 
import System.Environment (getArgs) 
import Data.List (foldl') 
import Data.Int 
import Data.Array.IO 
import Data.Array.Unboxed 
import Control.Monad 

type Length = Int64 -- use Int on 32-bit systems 

main = do 
    (arg:_) <- getArgs 
    (line1:lns) <- fmap BS.lines $ BS.readFile arg 

    -- line1 contains the header 
    let (headers:_) = [ map csvFieldContent r | r <- csvTable (parseCSV line1) ] 
     ncols = length headers :: Int 

    arr <- newArray (1,ncols) 0 :: IO (IOUArray Int Length) 
    let inc i a = do v <- readArray arr i; writeArray arr i (v+a) 

    let loop !n [] = return n 
     loop !n (b:bs) = do 
     let lengths = map BS.length $ head [ map csvFieldContent r | r <- csvTable (parseCSV b) ] 
     forM_ (zip [1..] lengths) $ \(i,a) -> inc i a 
     loop (n+1) bs 
    print headers 
    n <- loop 0 lns 
    putStrLn $ "n = " ++ show (n :: Int) 
    arr' <- freeze arr :: IO (UArray Int Length) 
    putStrLn $ "totals = " ++ show arr'