使用 Haskell,我如何处理大量的 XML?

Posted

技术标签:

【中文标题】使用 Haskell,我如何处理大量的 XML?【英文标题】:With Haskell, how do I process large volumes of XML? 【发布时间】:2011-01-18 13:28:46 【问题描述】:

我一直在探索Stack Overflow data dumps,到目前为止,我一直在利用友好的 XML 和正则表达式“解析”。我尝试使用各种 Haskell XML 库来查找特定用户按文档顺序发布的第一篇文章都遇到了令人讨厌的颠簸。

TagSoup

import Control.Monad
import Text.html.TagSoup

userid = "83805"

main = do
  posts <- liftM parseTags (readFile "posts.xml")
  print $ head $ map (fromAttrib "Id") $
                 filter (~== ("<row OwnerUserId=" ++ userid ++ ">"))
                 posts

hxt

import Text.XML.HXT.Arrow
import Text.XML.HXT.XPath

userid = "83805"

main = do
  runX $ readDoc "posts.xml" >>> posts >>> arr head
  where
    readDoc = readDocument [ (a_tagsoup, v_1)
                           , (a_parse_xml, v_1)
                           , (a_remove_whitespace, v_1)
                           , (a_issue_warnings, v_0)
                           , (a_trace, v_1)
                           ]

posts :: ArrowXml a => a XmlTree String
posts = getXPathTrees byUserId >>>
        getAttrValue "Id"
  where byUserId = "/posts/row/@OwnerUserId='" ++ userid ++ "'"

xml

import Control.Monad
import Control.Monad.Error
import Control.Monad.Trans.Maybe
import Data.Either
import Data.Maybe
import Text.XML.Light

userid = "83805"

main = do
  [posts,votes] <- forM ["posts", "votes"] $
    liftM parseXML . readFile . (++ ".xml")
  let ps = elemNamed "posts" posts
  putStrLn $ maybe "<not present>" show
           $ filterElement (byUser userid) ps

elemNamed :: String -> [Content] -> Element
elemNamed name = head . filter ((==name).qName.elName) . onlyElems

byUser :: String -> Element -> Bool
byUser id e = maybe False (==id) (findAttr creator e)
  where creator = QName "OwnerUserId" Nothing Nothing

我哪里做错了?使用 Haskell 处理大量 XML 文档的正确方法是什么?

【问题讨论】:

我想支持这个问题。我有一个必须扫描的类似问题(大型 xml 数据集)。我目前使用 HXT+Expat,因为这是唯一一个在呈现 14MB XML 文件时不会内存爆炸的 HXT 后端。但是解析整个文件还需要一分半钟的时间。 【参考方案1】:

我注意到您在所有这些情况下都在执行 String IO。如果您希望有效地处理大量文本,则绝对必须使用 Data.Text 或 Data.Bytestring(.Lazy),因为 String == [Char],这对于非常大的平面文件来说是不合适的表示。

这意味着您需要使用支持字节串的 Haskell XML 库。几个 xml 库在这里:http://hackage.haskell.org/packages/archive/pkg-list.html#cat:xml

我不确定哪个支持字节串,但这是您正在寻找的条件。

【讨论】:

+1 for ByteStrings,那些(仍然)没有得到应有的爱。忘记 long Strings 的糟糕表现是一个非常容易的错误——不要因为简单就将它们留在列表中,伙计们!【参考方案2】:

下面是一个使用hexpat的例子:

-# LANGUAGE PatternGuards #-

module Main where

import Text.XML.Expat.SAX

import qualified Data.ByteString.Lazy as B

userid = "83805"

main :: IO ()
main = B.readFile "posts.xml" >>= print . earliest
  where earliest :: B.ByteString -> SAXEvent String String
        earliest = head . filter (ownedBy userid) . parse opts
        opts = ParserOptions Nothing Nothing

ownedBy :: String -> SAXEvent String String -> Bool
ownedBy uid (StartElement "row" as)
  | Just ouid <- lookup "OwnerUserId" as = ouid == uid
  | otherwise = False
ownedBy _ _ = False

ownedBy 的定义有点笨拙。也许是一个视图模式:

-# LANGUAGE ViewPatterns #-

module Main where

import Text.XML.Expat.SAX

import qualified Data.ByteString.Lazy as B

userid = "83805"

main :: IO ()
main = B.readFile "posts.xml" >>= print . earliest
  where earliest :: B.ByteString -> SAXEvent String String
        earliest = head . filter (ownedBy userid) . parse opts
        opts = ParserOptions Nothing Nothing

ownedBy :: String -> SAXEvent String String -> Bool
ownedBy uid (ownerUserId -> Just ouid) = uid == ouid
ownedBy _ _ = False

ownerUserId :: SAXEvent String String -> Maybe String
ownerUserId (StartElement "row" as) = lookup "OwnerUserId" as
ownerUserId _ = Nothing

【讨论】:

【参考方案3】:

你可以试试我的fast-tagsoup 库。它是 tagsoup 的简单替代品,解析速度为 20-200MB/秒。

tagsoup 包的问题在于,即使您使用 Text 或 ByteString 接口,它也可以在内部使用 String。 fast-tagsoup 使用高性能低级解析与严格的 ByteStrings 一起工作,同时仍然返回惰性标签列表作为输出。

【讨论】:

【参考方案4】:

我遇到了类似的问题(使用 HXT) - 我通过使用 Expat parser with HXT 避免了内存问题。在一个 5MB 的 XML 文件上,只需读取文档并打印它:峰值内存消耗从 2Gigs 下降到大约 180MB,并且执行时间要短得多(未测量)。

【讨论】:

【参考方案5】:

TagSoup 通过其 Text.StringLike 类支持 ByteString。您的示例所需的唯一更改是调用 ByteString.Lazy 的 readFile,并将 fromString 添加到 fromAttrib

import Text.StringLike
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Char8 as BSC

userid = "83805"
file = "blah//posts.xml"
main = do
posts <- liftM parseTags (BSL.readFile file)
print $ head $ map (fromAttrib (fromString "Id")) $
               filter (~== ("<row OwnerUserId=" ++ userid ++ ">"))
               posts  

您的示例为我运行(4 gig RAM),耗时 6 分钟; ByteString 版本花了 10 分钟。

【讨论】:

【参考方案6】:

也许您需要一个惰性 XML 解析器:您的使用看起来像是对输入的非常简单的扫描。 HaXml 有一个惰性解析器,尽管您必须通过导入正确的模块来明确要求它。

【讨论】:

以上是关于使用 Haskell,我如何处理大量的 XML?的主要内容,如果未能解决你的问题,请参考以下文章

我如何处理 .FirstOrDefault 方法?

我如何处理 PayPal 公钥?

使用继承和扫描仪简单计算我如何处理这些异常?

我如何处理 java 命令行参数? [复制]

我如何处理改造错误或获取 URL 请求以检测问题

在 Perl 中,我如何处理整个哈希?