2014-11-03 125 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'