xml haskell tag-soup large-scale large-data

Con Haskell, ¿cómo puedo procesar grandes volúmenes de XML?



tag-soup large-scale (6)

He estado explorando los volcados de datos de Stack Overflow y hasta ahora aprovechando el XML amigable y el "análisis" con expresiones regulares. Mis intentos con varias bibliotecas XML de Haskell para encontrar la primera publicación en orden de documentos por parte de un usuario en particular se encontraron con desagradables golpes.

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

¿Qué hice mal? ¿Cuál es la forma correcta de procesar grandes documentos XML con Haskell?


A continuación se muestra un ejemplo que utiliza 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

La definición de ownedBy es un poco torpe. Tal vez un patrón de vista en su lugar:

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


Me doy cuenta de que estás haciendo String IO en todos estos casos. Absolutamente debe usar Data.Text o Data.Bytestring (.Lazy) si espera procesar grandes volúmenes de texto de manera eficiente, como String == [Char], que es una representación inadecuada para archivos planos muy grandes.

Eso implica que necesitará utilizar una biblioteca XML de Haskell que admita bytestrings. Las bibliotecas de XML de un par de docenas están aquí: http://hackage.haskell.org/packages/archive/pkg-list.html#cat:xml

No estoy seguro de cuáles son las secuencias de soporte, pero esa es la condición que está buscando.


Podrías probar mi biblioteca de fast-tagsoup . Es un reemplazo simple a tagsoup y analiza a velocidades de 20-200MB / seg.

El problema con el paquete tagsoup es que funciona con String internamente, incluso si usa la interfaz Text o ByteString. fast-tagsoup funciona con ByteStrings estrictos que utilizan un análisis de bajo nivel de alto rendimiento y al mismo tiempo devuelve la lista de etiquetas perezosas como salida.


Quizás necesite un analizador XML perezoso: su uso parece un escaneo bastante sencillo a través de la entrada. HaXml tiene un analizador perezoso, aunque debe solicitarlo explícitamente al importar el módulo correcto.


TagSoup admite ByteString a través de su clase Text.StringLike. Los únicos cambios necesarios para su ejemplo fueron llamar a ByteString.Lazy readFile y agregar una fromString a la 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

Su ejemplo corrió para mí (4 gigas de RAM), tomando 6 minutos; La versión ByteString tomó 10 minutos.


Tuve un problema similar (usando HXT): evité el problema de la memoria usando el analizador Expat con HXT . En un archivo XML de 5MB, solo leyendo el documento e imprimiéndolo: el consumo máximo de memoria pasó de 2Gigs a aproximadamente 180MB, y el tiempo de ejecución fue mucho más corto (no se midió).