流式处理xml-conduit解析结果

时间:2014-01-26 18:24:34

标签: xml haskell stream xml-conduit

我想使用xml-conduit,特别是Text.XML.Stream.Parse,以便从大型XML文件中懒惰地提取对象列表。

作为测试用例,我使用the recently re-released StackOverflow data dumps。为了简单起见,我打算从stackoverflow.com-Users.7z中提取所有用户名。即使该文件是.7zfile表示它只是bzip2压缩数据(文件末尾可能有一些7zip内容,但现在我不在乎)。< / p>

XML的简化版本是

<users>
    <row id="1" DisplayName="StackOverflow"/>
    ...
    <row id="2597135" DisplayName="Uli Köhler"/>
    ... 
</users>

基于this previous Q&A和示例on Hackage流 - 读取bz2-ed表单中的示例XML对我来说非常适合

但是,当使用runghc运行以下程序时,它会在不打印任何输出的情况下运行:

{-# LANGUAGE OverloadedStrings #-}
import Data.Conduit (runResourceT, ($$), ($=))
import qualified Data.Conduit.Binary as CB
import Data.Conduit.BZlib
import Data.Conduit
import Data.Text (Text)
import System.IO
import Text.XML.Stream.Parse
import Control.Applicative ((<*))

data User = User {name :: Text} deriving (Show)

parseUserRow = tagName "row" (requireAttr "DisplayName" <* ignoreAttrs) $ \displayName -> do
    return $ User displayName

parseUsers = tagNoAttr "users" $ many parseUserRow

main = do
    users <- runResourceT $ CB.sourceFile "stackoverflow.com-Users.7z" $= bunzip2 $= parseBytes def $$ force "users required" parseUsers
    putStrLn $ unlines $ map show users

我认为发生此问题是因为Haskell在开始打印之前尝试深入评估users列表。该理论得到了该程序的内存使用率的支持,该程序的内存使用率每秒持续增长约2%(来源:htop)。

如何将结果“直播”到标准输出?我假设这可以通过在结尾处添加另一个管道语句(如$$ CB.sinkFile "output.txt")来实现。但是,此特定版本需要Conduit输出ByteString。你能指出我从哪里出发的正确方向吗?

任何帮助将不胜感激!

3 个答案:

答案 0 :(得分:10)

首先让我说xml-conduit中的流式助手API多年来一直没有工作,并且可能会受益于重新设想在过渡期间发生的管道变化。我认为可能有更好的方法来完成任务。

那就是说,让我解释一下你所看到的问题。 many函数创建结果列表,在完成处理之前不会生成任何值。在您的情况下,有这么多的值,这似乎永远不会发生。最终,当读取整个文件时,将立即显示整个用户列表。但这显然不是你正在寻找的行为。

相反,您要做的是创建一个{em> stream 的User值,这些值会在它们准备好后立即生成。你想要做的基本上是用一个新函数替换many函数调用,每次解析时yield结果都是yieldWhileJust :: Monad m => ConduitM a b m (Maybe b) -> Conduit a m b yieldWhileJust consumer = loop where loop = do mx <- consumer case mx of Nothing -> return () Just x -> yield x >> loop 。一个简单的实现可能是:

putStrLn $ unlines $ map show

此外,您不希望使用User,而是将整个管道附加到使用者,该使用者将打印每个单独生成的Data.Conduit.List.mapM_值。这可以使用CL.mapM_ (liftIO . print)轻松实现,例如:{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} import Control.Applicative ((<*)) import Control.Concurrent (threadDelay) import Control.Monad (forever, void) import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.ByteString (ByteString) import Data.Conduit import qualified Data.Conduit.List as CL import Data.Text (Text) import Data.Text.Encoding (encodeUtf8) import Data.XML.Types (Event) import Text.XML.Stream.Parse -- instead of actually including a large input data file, just for testing purposes infiniteInput :: MonadIO m => Source m ByteString infiniteInput = do yield "<users>" forever $ do yield $ encodeUtf8 "<row id=\"1\" DisplayName=\"StackOverflow\"/><row id=\"2597135\" DisplayName=\"Uli Köhler\"/>" liftIO $ threadDelay 1000000 --yield "</users>" -- will never be reached data User = User {name :: Text} deriving (Show) parseUserRow :: MonadThrow m => Consumer Event m (Maybe User) parseUserRow = tagName "row" (requireAttr "DisplayName" <* ignoreAttrs) $ \displayName -> do return $ User displayName parseUsers :: MonadThrow m => Conduit Event m User parseUsers = void $ tagNoAttr "users" $ yieldWhileJust parseUserRow yieldWhileJust :: Monad m => ConduitM a b m (Maybe b) -> Conduit a m b yieldWhileJust consumer = loop where loop = do mx <- consumer case mx of Nothing -> return () Just x -> yield x >> loop main :: IO () main = infiniteInput $$ parseBytes def =$ parseUsers =$ CL.mapM_ print

我根据您的代码汇总了a full example。输入是一个人工生成的无限XML文件,只是为了证明它确实立即产生输出。

{{1}}

答案 1 :(得分:2)

基于Michael Snoyman's excellent answer这里是一个修改版本,它从stackoverflow.com-Users.7z读取数据,而不是从人工生成的IO流中获取数据。

有关如何直接使用xml-conduit的参考,请参阅Michael's answer。此答案仅作为如何在可选压缩文件上使用此处描述的方法的示例提供。

此处的主要更改是您需要使用runResourceT来阅读文件,最终print需要liftIO ()到{{1} }}

ResourceT IO ()

答案 2 :(得分:2)

进行了编辑,以使M. Snoyman中具有洞察力的示例保持最新,但是却被中等水平的脱机者抛弃了。因此,这。

原始版本将不再编译,并且会产生许多已弃用的警告(legacy syntax)。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes        #-}
import           Control.Applicative    ((<*))
import           Control.Concurrent     (threadDelay)
import           Control.Monad          (forever, void)
import           Control.Monad.Catch    (MonadThrow)
import           Control.Monad.IO.Class (MonadIO (liftIO))
import           Data.ByteString        (ByteString)
import           Data.Conduit
import qualified Data.Conduit.List      as CL
import           Data.Text              (Text)
import           Data.Text.Encoding     (encodeUtf8)
import           Data.XML.Types         (Event)
import           Text.XML.Stream.Parse

-- instead of actually including a large input data file, just for testing purposes
infiniteInput :: MonadIO m => ConduitT () ByteString m ()
infiniteInput = do
    yield "<users>"
    forever $ do
        yield $ encodeUtf8
            "<row id=\"1\" DisplayName=\"StackOverflow\"/><row id=\"2597135\" DisplayName=\"Uli Köhler\"/>"
        liftIO $ threadDelay 1000000
    --yield "</users>" -- will never be reached

data User = User {name :: Text} deriving (Show)

parseUserRow :: MonadThrow m => forall o. ConduitT Event o m (Maybe User)
parseUserRow = tag' "row" (requireAttr "DisplayName" <* ignoreAttrs) $ \displayName -> do
    return $ User displayName

parseUsers :: MonadThrow m => ConduitT Event User m ()
parseUsers = void $ tagNoAttr "users" $ manyYield parseUserRow

--or use manyYield, now provided by Text.XML.Stream.Parse
yieldWhileJust :: Monad m
               => ConduitT a b m (Maybe b)
               -> ConduitT a b m ()
yieldWhileJust consumer =
    loop
  where
    loop = do
        mx <- consumer
        case mx of
            Nothing -> return ()
            Just x -> yield x >> loop

main :: IO ()
main = runConduit $ infiniteInput
    .| parseBytes def
    .| parseUsers
    .| CL.mapM_ print

ghc 8.6.5,xml-conduit 1.9.0.0