С Haskell, как мне обрабатывать большие объемы XML?
Я изучал дампы данных переполнения стека и, таким образом, использовал преимущества удобного XML и "разбора" с помощью регулярных выражений. Мои попытки с различными библиотеками XML на Haskell найти первый пост в порядке следования документов конкретным пользователем потерпели неудачу.
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
Где я неправ? Как правильно обрабатывать здоровенные XML-документы с помощью Haskell?
6 ответов
Я заметил, что вы делаете String IO во всех этих случаях. Вы обязательно должны использовать либо Data.Text, либо Data.Bytestring (.Lazy), если вы хотите эффективно обрабатывать большие объемы текста, например String == [Char], что является неподходящим представлением для очень больших плоских файлов.
Это означает, что вам нужно использовать XML-библиотеку Haskell, которая поддерживает строки байтов. Несколько десятков библиотек xml находятся здесь: http://hackage.haskell.org/packages/archive/pkg-list.html
Я не уверен, какие строки поддержки поддерживаются, но это условие, которое вы ищете.
Ниже приведен пример, который использует 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
Вы можете попробовать мою библиотеку fast-tagsoup. Это простая замена тэгсупу и анализу со скоростью 20-200 МБ / с.
Проблема с пакетом tagsoup заключается в том, что он работает со String внутри, даже если вы используете интерфейс Text или ByteString. fast-tagsoup работает со строгими строками ByteStrings, используя высокопроизводительный низкоуровневый синтаксический анализ, при этом возвращая список ленивых тегов в качестве вывода.
TagSoup поддерживает ByteString через свой класс Text.StringLike. Единственными изменениями, необходимыми для вашего примера, было вызвать ByteString.Lazy's 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 гигабайта оперативной памяти), заняв 6 минут; версия ByteString заняла 10 минут.
У меня была похожая проблема (с использованием HXT) - я избежал проблемы с памятью, используя парсер Expat с HXT. В 5-мегабайтном XML-файле достаточно просто прочитать документ и распечатать его: пиковое потребление памяти возросло с 2 ГБ до примерно 180 МБ, а время выполнения было намного короче (не измерялось).
Возможно, вам нужен ленивый анализатор XML: ваше использование выглядит как довольно простое сканирование входных данных. У HaXml есть ленивый парсер, хотя вы должны явно запросить его, импортировав правильный модуль.