Анализ XML в Haskell

Я пытаюсь получить данные с веб-страницы, периодически использующей XML файл, с котировками на фондовом рынке (примеры данных). Структура XML очень проста и выглядит примерно так:

<?xml version="1.0"?>
<Contents>
  <StockQuote Symbol="PETR3" Date="21-12-2010" Time="13:20" Price="23.02" />
</Contents>

(это больше, чем это, но этого достаточно в качестве примера).

Я хотел бы проанализировать его в структуре данных:

 data Quote = Quote { symbol :: String, 
                      date   :: Data.Time.Calendar.Day, 
                      time   :: Data.Time.LocalTime.TimeOfDay,
                      price  :: Float}

Я понимаю более или менее то, как работает Parsec (на уровне книги Real World Haskell), и я немного попробовал библиотеку Text.XML, но все, что я мог разработать, это код, который работал, но слишком большой для такого простая задача и выглядит наполовину испеченным взломом, а не лучшим, что можно было бы сделать.

Я мало знаю о синтаксических анализаторах и XML (я знаю, в основном, то, что я читал в книге RWH, я никогда раньше не использовал парсеров) (я просто делаю статистическое и численное программирование, я не компьютерный ученый). Есть ли библиотека синтаксического анализа XML, где я могу просто сказать, что такое модель, и сразу извлечь информацию, не анализируя каждый элемент вручную и не анализируя чистую строку?

Я думаю о чем-то вроде:

  myParser = do cont  <- openXMLElem "Contents"
                quote <- openXMLElem "StockQuote" 
                symb <- getXMLElemField "Symbol"
                date <- getXMLElemField "Date"
                (...) 
                closequote <- closeXMLElem "StockQuote"
                closecont  <- closeXMLElem "Contents"
                return (symb, date)


  results = parse myParser "" myXMLString

где мне не нужно было бы разбираться с чистой строкой и сами создавать комбинаторы (я сосать ее).

EDIT: Мне, вероятно, нужно немного почитать (достаточно, чтобы это сделать правильно) о парсерах вообще (не только Parsec) и минимальном формате XML. Вы, ребята, что-то рекомендуете?

Реальная строка, которую я должен проанализировать, такова:

 stringTest = "<?xml version=\"1.0\"?>\r\n<ComportamentoPapeis><Papel Codigo=\"PETR3\" 
 Nome=\"PETROBRAS ON\" Ibovespa=\"#\" Data=\"05/01/201100:00:00\" 
 Abertura=\"29,80\" Minimo=\"30,31\" Maximo=\"30,67\" Medio=\"30,36\" 
 Ultimo=\"30,45\" Oscilacao=\"1,89\" Minino=\"29,71\"/></ComportamentoPapeis>\r\n"

EDIT2:

Я попробовал следующее (readFloat, readQuoteTime и т.д.) - это просто функции для чтения вещей из строк).

bvspaParser :: (ArrowXml a) => a XmlTree Quote
bvspaParser = hasName "ComportamentoPapeis" /> hasName "Papel" >>> proc x -> do
   (hour,date) <- readQuoteTime ^<< getAttrValue "Data" -< x
   quoteCode   <- getAttrValue "Codigo" -< x
   openPrice   <- readFloat ^<< getAttrValue "Abertura" -< x
   minim       <- readFloat ^<< getAttrValue "Minimo" -< x
   maxim       <- readFloat ^<< getAttrValue "Maximo" -< x
   ultimo      <- readFloat ^<< getAttrValue "Ultimo" -< x
   returnA     -< Quote quoteCode (LocalTime date hour) openPrice minim maxim ultimo

docParser :: String -> IO [Quote]
docParser  str = runX $ readString [] str >>> (parseXmlDocument False) >>> bvspaParser

Когда я вызываю его в ghci:

*Main> docParser stringTest >>= print
[]

Что-то не так?

Ответы

Ответ 1

Я использовал Haskell XML Toolbox в прошлом. Что-то вдоль линий

{-# LANGUAGE Arrows #-}

quoteParser :: (ArrowXml a) => a XmlTree Quote
quoteParser =
    hasName "Contents" /> hasName "StockQuote" >>> proc x -> do
    symbol <- getAttrValue "Symbol" -< x
    date <- readTime defaultTimeLocale "%d-%m-%Y" ^<< getAttrValue "Date" -< x
    time <- readTime defaultTimeLocale "%H:%M" ^<< getAttrValue "Time" -< x
    price <- read ^<< getAttrValue "Price" -< x
    returnA -< Quote symbol date time price

parseQuoteDocument :: String -> IO (Maybe Quote)
parseQuoteDocument xml =
    liftM listToMaybe . runX . single $
    readString [] xml >>> getChildren >>> quoteParser

Ответ 2

Существует множество библиотек XML, написанных для Haskell, которые могут выполнять синтаксический анализ для вас. Я рекомендую библиотеку с именем xml (см. http://hackage.haskell.org/package/xml). С его помощью вы можете просто написать, например:

let contents = parseXML source
    quotes   = concatMap (findElements $ simpleName "StockQuote") (onlyElems contents)
    symbols  = map (findAttr $ simpleName "Symbol") quotes
    simpleName s = QName s Nothing Nothing
print symbols

Этот фрагмент печатает [Just "PETR3"] как результат для вашего примера XML, и его легко расширить для сбора всех необходимых данных. Чтобы написать программу в описываемом вами стиле, вы должны использовать монаду Maybe, поскольку функции поиска xml часто возвращают Maybe String, сигнализируя, можно ли найти тег, элемент или атрибут. Также см. Связанный с этим вопрос: Какую библиотеку XML Haskell использовать?

Ответ 4

Существуют и другие способы использования этой библиотеки, но для чего-то простого подобного я скомпоновал парсер сакса.

import Prelude as P
import Text.XML.Expat.SAX
import Data.ByteString.Lazy as L

parsexml txt = parse defaultParseOptions txt :: [SAXEvent String String]

main = do
  xml <- L.readFile "stockinfo.xml"
  return  $ P.filter stockquoteelement (parsexml xml)

  where
    stockquoteelement (StartElement "StockQuote" attrs) = True
    stockquoteelement _ = False

Оттуда вы можете выяснить, куда идти. Вы также можете использовать Text.XML.Expat.Annotated, чтобы проанализировать его в структуру, которая больше похожа на то, что вы ищете выше:

parsexml txt = parse defaultParseOptions txt :: (LNode String String, Maybe XMLParseError)

И затем используйте Text.XML.Expat.Proc для просмотра структуры.

Ответ 5

В следующем фрагменте используется xml-перечислитель. Он оставляет дату и время в виде текста (разбор данных остается в виде упражнения для читателя):

{-# LANGUAGE OverloadedStrings #-}
import Text.XML.Enumerator.Parse
import Data.Text.Lazy (Text, unpack)

data Quote = Quote { symbol :: Text
                   , date   :: Text
                   , time   :: Text
                   , price  :: Float}
  deriving Show

main = parseFile_ "test.xml" (const Nothing) $ parseContents

parseContents = force "Missing Contents" $ tag'' "Contents" parseStockQuote
parseStockQuote = force "Missing StockQuote" $ flip (tag' "StockQuote") return $ do
    s <- requireAttr "Symbol"
    d <- requireAttr "Date"
    t <- requireAttr "Time"
    p <- requireAttr "Price"
    return $ Quote s d t (read $ unpack p)