7

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

这是我的代码:

{-# 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

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

foo = fmap DTL.last $ DTLIO.readFile "../../../../etymwn.tsv

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

谢谢,

鲁本

4

2 回答 2

5

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

下限

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

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

这花了大约。17 秒,所以我希望任何 Haskell 程序都需要大约 17 秒的时间。

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

  1. 在 ghci 中工作并使用“实时重新加载”技术使用Foreign.Store 包保存地图, 以便它在 ghci 代码重新加载时保持不变。这样,您只需在迭代代码时加载一次地图数据。
  2. 使用持久键值存储(例如 sqlite、gdbm、BerkeleyDB)
  3. 通过客户端-服务器存储访问数据
  4. 减少你存储的键值对的数量(你需要全部 600 万个吗?)

选项 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 建议的那样,我怀疑 usingattoparsec是懒惰的。特别是,它必须验证整个解析是否成功,所以我看不出它是如何避免在返回之前创建所有已解析的行的。

要真正延迟解析输入,请采用以下方法:

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

您仍然可以使用attoparsecto 实现toPair,但您将逐行使用它,而不是在整个输入上使用它。

字节串与文本

根据我的经验,使用 ByteStrings 比使用 Text 要快得多。

这个版本的toPairByteStrings 比对应的 Text 版本快大约 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 秒。为文本。

于 2015-07-29T20:48:44.033 回答
0

为了补充这个答案,我想指出 attoparsec 实际上对“基于拉的”增量解析有很好的支持。parseWith您可以通过便捷的功能直接使用它。parse为了进行更精细的控制,您可以使用和手动输入解析器feed。如果你不想担心这些,你应该可以使用类似的东西pipes-attoparsec,但我个人觉得管道有点难以理解。

于 2015-07-29T22:27:37.417 回答