有效地将大文件读入Map

时间:2015-07-29 10:28:18

标签: performance haskell io hashmap attoparsec

我试图编写代码来在Haskell中执行以下简单任务:使用此字典查找单词的词源,存储为大型tsv文件(http://www1.icsi.berkeley.edu/~demelo/etymwn/)。我想我会将tsv文件解析(使用attoparsec)到Map中,然后我可以根据需要使用它来高效地查找词源(并做其他一些事情)。

这是我的代码:

    public static void main(String[] args) {

            Function f = new Function<Tuple2<String,Integer>,Integer>(){

                @Override
                public Integer call(Tuple2<String,Integer> paramT1) throws Exception {
                    return paramT1._2;
                }

            };

            JavaStreamingContext ssc = JavaStreamingFactory.getInstance();

            JavaReceiverInputDStream<String> lines = ssc.socketTextStream("localhost", 9999);
            JavaDStream<String> words =  lines.flatMap(s->{return Arrays.asList(s.split(" "));});
            JavaPairDStream<String,Integer> pairRDD =  words.mapToPair(x->new Tuple2<String,Integer>(x,1));
            JavaPairDStream<String,Integer> aggregate = pairRDD.reduceByKey((x,y)->x+y);
            JavaDStream<Integer> con = aggregate.transform(
                    (Function<JavaPairRDD<String,Integer>, JavaRDD<Integer>>)pRDD-> pRDD.map( 
                            (Function<Tuple2<String,Integer>,Integer>)t->t._2));
          //JavaDStream<Integer> con = aggregate.transform(pRDD-> pRDD.map(f)); It works
            con.print();

            ssc.start();
            ssc.awaitTermination();


        }

它适用于少量输入,但很快就会变得太低效。我不太清楚问题出在哪里,并且很快意识到即使是在我尝试时查看文件的最后一个字符这样的琐碎任务花了太长时间,例如与

{-# LANGUAGE OverloadedStrings #-}

import Control.Arrow
import qualified Data.Map as M
import Control.Applicative
import qualified Data.Text as DT
import qualified Data.Text.Lazy.IO as DTLIO
import qualified Data.Text.Lazy as DTL
import qualified Data.Attoparsec.Text.Lazy as ATL
import Data.Monoid

text = do
    x <- DTLIO.readFile "../../../../etymwn.tsv"
    return $ DTL.take 10000 x

--parsers
wordpair = do
    x <- ATL.takeTill (== ':')
    ATL.char ':' *> (ATL.many' $ ATL.char ' ')
    y <- ATL.takeTill (\x -> x `elem` ['\t','\n'])
    ATL.char '\n' <|>   ATL.char '\t'
    return (x,y)

--line of file
line = do
    a <- (ATL.count 3 wordpair)
    case (rel (a !! 2)) of 
        True -> return . (\[a,b,c] -> [(a,c)]) $ a
        False -> return . (\[a,b,c] -> [(c,a)]) $ a
    where rel x = if x == ("rel","etymological_origin_of") then False else True

tsv = do 
    x <- ATL.many1 line
    return $ fmap M.fromList x

main = (putStrLn . show . ATL.parse tsv) =<< text

所以我的问题是:在方法和执行方面,我做错的主要事情是什么?有关更多Haskelly /更好代码的任何提示吗?

谢谢,

流便

2 个答案:

答案 0 :(得分:5)

请注意,您要加载的文件有600万行和 您感兴趣的文本包含约。 120 MB。

下限

为了建立一些下界,我首先创建了另一个包含的.tsv文件 etymwn.tsv文件的预处理内容。然后我计时了 花了这个perl程序读取该文件:

my %H;
while (<>) {
  chomp;
  my ($a,$b) = split("\t", $_, 2);
  $H{$a} = $b;
}

这花了大约。 17秒,所以我希望任何Haskell程序 把握那个时间。

如果此启动时间不可接受,请考虑以下选项:

  1. 在ghci中工作并使用“实时重新加载”技术来保存地图  使用Foreign.Store package  所以它通过ghci代码重新加载持续存在。  这样,您只需在迭代代码时加载一次地图数据。
  2. 使用持久性键值存储(例如sqlite,gdbm,BerkeleyDB)
  3. 通过客户端 - 服务器商店访问数据
  4. 减少您存储的键值对的数量(您是否需要全部600万?)
  5. 选项1在Chris Done的博客文章中讨论:

    选项2和3将要求您使用IO monad。

    解析

    首先,检查tsv功能的类型:

    tsv :: Data.Attoparsec.Internal.Types.Parser
              DT.Text [M.Map (DT.Text, DT.Text) (DT.Text, DT.Text)]
    

    您正在返回地图列表,而不仅仅是一张地图。这看起来不像 右。

    其次,正如@chi建议的那样,我怀疑使用attoparsec是懒惰的。 在某种程度上,它必须验证整个解析是否成功, 所以我看不出它是如何不能避免创建所有解析的行 在回来之前。

    要真实地解析输入,请采用以下方法:

    toPair :: DT.Text -> (Key, Value)
    toPair input = ...
    
    main = do
      all_lines <- fmap DTL.lines $ DTLIO.getContent
      let m = M.fromList $ map toPair all_lines
      print $ M.lookup "foobar" m
    

    您仍然可以使用attoparsec来实施toPair,但您将使用它 逐行而不是整个输入。

    ByteString与文本

    根据我的经验,使用ByteStrings比使用Text更快。

    ByteStrings的这个toPair版本比对应版本快4倍 文字版本:

    {-# LANGUAGE OverloadedStrings #-}
    import qualified Data.ByteString.Lazy.Char8 as L
    import qualified Data.Attoparsec.ByteString.Char8 as A
    import qualified Data.Attoparsec.ByteString.Lazy as AL
    
    toPair :: L.ByteString -> (L.ByteString, L.ByteString)
    toPair bs =
      case AL.maybeResult (AL.parse parseLine bs) of
        Nothing    -> error "bad line"
        Just (a,b) -> (a,b)
      where parseLine = do
              A.skipWhile (/= ' ')
              A.skipWhile (== ' ')
              a <- A.takeWhile (/= '\t')
              A.skipWhile (== '\t')
              rel <- A.takeWhile (/= '\t')
              A.skipWhile (== '\t')
              A.skipWhile (/= ' ')
              A.skipWhile (== ' ')
              c <- A.takeWhile (const True)
              if rel == "rel:etymological_origin_of"
                then return (c,a)
                else return (a,c)
    

    或者,只使用普通的ByteString函数:

    fields :: L.ByteString -> [L.ByteString]
    fields = L.splitWith (== '\t')
    
    snipSpace = L.ByteString -> L.ByteString
    snipSpace = L.dropWhile (== ' ') . L.dropWhile (/=' ')
    
    toPair'' bs = 
      let fs = fields bs
      case fields line of
        (x:y:z:_) -> let a = snipSpace x
                         c = snipSpace z
                     in
                     if y == "rel:etymological_origin_of"
                       then (c,a)
                       else (a,c)
        _         -> error "bad line"
    

    加载地图所花费的大部分时间都在解析这些行。 对于ByteStrings,这大约是14秒。加载所有600万行 与50秒。对于文本。

答案 1 :(得分:0)

要添加到this answer,我想要注意的是,attoparsec实际上非常支持“基于拉式”的增量解析。您可以使用方便的parseWith函数直接使用它。为了获得更好的控制,您可以使用parsefeed手动提供解析器。如果您不想担心这些问题,您应该可以使用类似pipes-attoparsec的内容,但我个人觉得管道有点难以理解。