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ó).