2015-04-04 13 views
5

Voglio estrarre informazioni da un grande file XML (circa 20G) in Haskell. Poiché si tratta di un file di grandi dimensioni, ho utilizzato le funzioni di analisi SAX da Hexpath.Come analizzare un file XML di grandi dimensioni in Haskell con una quantità limitata di risorse?

Ecco un semplice codice che ho provato:

import qualified Data.ByteString.Lazy as L 
import Text.XML.Expat.SAX as Sax 

parse :: FilePath -> IO() 
parse path = do 
    inputText <- L.readFile path 
    let saxEvents = Sax.parse defaultParseOptions inputText :: [SAXEvent Text Text] 
    let txt = foldl' processEvent "" saxEvents 
    putStrLn txt 

Dopo l'attivazione di profili in Cabal, si dice che parse.saxEvents ha preso l'85% di memoria allocata. Ho anche usato foldr e il risultato è lo stesso.

Se processEvent diventa abbastanza complesso, il programma si arresta in modo anomalo con un errore stack space overflow.

Cosa sto sbagliando?

+0

Si consiglia di guardare in una soluzione di streaming: https://hackage.haskell.org/package/xml-conduit – Sibi

+0

Cosa 'processEvent' fare? Usa '++' sulla stringa dell'accumulatore? Inoltre, si esaurisce solo lo spazio di stack o anche la RAM? Si prega di postare alcune statistiche sull'utilizzo della RAM (per esempio l'output del flag '+ RTS -s'). Inoltre, qual è la tua versione di GHC? Per impostazione predefinita, dopo lo stack GHC 7.8, lo stack può raggiungere l'80% della RAM. –

+0

@Sibi hexpat ha già un'interfaccia di streaming, proprio come xml-conduit. – Michael

risposta

2

Non si dice cosa sia lo processEvent. In linea di principio, non dovrebbe essere problematico utilizzare il pigro ByteString per una rigida piega a sinistra sull'input generato in modo pigramente, quindi non sono sicuro di cosa stia andando male nel tuo caso. Ma si dovrebbe usare tipi appropriati per lo streaming quando si tratta di file giganteschi!

Infatti, hexpat ha un'interfaccia 'streaming' (proprio come xml-conduit). Utilizza la libreria non troppo conosciuta List e the rather ugly List class it defines. In linea di principio il ListT type dal pacchetto List dovrebbe funzionare bene. Mi sono arreso rapidamente a causa di una mancanza di combinatori e ho scritto un'istanza appropriata della brutta classe List per una versione compressa di Pipes.ListT che ho poi utilizzato per esportare le normali funzioni Pipes.Producer come parseProduce. Le manipolazioni banali necessari per questo vengono aggiunti sotto come PipesSax.hs

Una volta che abbiamo parseProducer possiamo convertire un ByteString o Producer testo in un produttore di SaxEvents con componenti di testo o ByteString. Ecco alcune semplici operazioni. Stavo usando un 238M "input.xml"; i programmi non richiedono mai più di 6 mb di memoria, a giudicare dall'aspetto di top.

-Sax.hs La maggior parte delle azioni IO utilizzare un tubo registerIds definita in fondo che è su misura per un po 'gigante di XML, di cui questa è una valida 1000 frammento http://sprunge.us/WaQK

{-#LANGUAGE OverloadedStrings #-} 
import PipesSax (parseProducer) 
import Data.ByteString (ByteString) 
import Text.XML.Expat.SAX 
import Pipes -- cabal install pipes pipes-bytestring 
import Pipes.ByteString (toHandle, fromHandle, stdin, stdout) 
import qualified Pipes.Prelude as P 
import qualified System.IO as IO 
import qualified Data.ByteString.Char8 as Char8 

sax :: MonadIO m => Producer ByteString m() 
       -> Producer (SAXEvent ByteString ByteString) m() 
sax = parseProducer defaultParseOptions 

-- stream xml from stdin, yielding hexpat tagstream to stdout; 
main0 :: IO() 
main0 = runEffect $ sax stdin >-> P.print 

-- stream the extracted 'IDs' from stdin to stdout 
main1 :: IO() 
main1 = runEffect $ sax stdin >-> registryIds >-> stdout 

-- write all IDs to a file 
main2 = 
IO.withFile "input.xml" IO.ReadMode $ \inp -> 
IO.withFile "output.txt" IO.WriteMode $ \out -> 
    runEffect $ sax (fromHandle inp) >-> registryIds >-> toHandle out 

-- folds: 
-- print number of IDs 
main3 = IO.withFile "input.xml" IO.ReadMode $ \inp -> 
      do n <- P.length $ sax (fromHandle inp) >-> registryIds 
       print n 

-- sum the meaningful part of the IDs - a dumb fold for illustration 
main4 = IO.withFile "input.xml" IO.ReadMode $ \inp -> 
     do let pipeline = sax (fromHandle inp) >-> registryIds >-> P.map readIntId 
      n <- P.fold (+) 0 id pipeline 
      print n 
    where 
    readIntId :: ByteString -> Integer 
    readIntId = maybe 0 (fromIntegral.fst) . Char8.readInt . Char8.drop 2 

-- my xml has tags with attributes that appear via hexpat thus: 
-- StartElement "FacilitySite" [("registryId","110007915364")] 
-- and the like. This is just an arbitrary demo stream manipulation. 
registryIds :: Monad m => Pipe (SAXEvent ByteString ByteString) ByteString m() 
registryIds = do 
    e <- await -- we look for a 'SAXEvent' 
    case e of -- if it matches, we yield, else we go to the next event 
    StartElement "FacilitySite" [("registryId",a)] -> do yield a 
                 yield "\n" 
                 registryIds 
    _ -> registryIds 

- 'biblioteca' : PipesSax.hs

Questo appena newtypes Pipes.ListT per ottenere le istanze appropriate. Non esportiamo nulla a che fare con List o ListT ma usiamo solo il concetto di Pipes.Producer standard.

{-#LANGUAGE TypeFamilies, GeneralizedNewtypeDeriving #-} 
module PipesSax (parseProducerLocations, parseProducer) where 
import Data.ByteString (ByteString) 
import Text.XML.Expat.SAX 
import Data.List.Class 
import Control.Monad 
import Control.Applicative 
import Pipes 
import qualified Pipes.Internal as I 

parseProducer 
    :: (Monad m, GenericXMLString tag, GenericXMLString text) 
    => ParseOptions tag text 
    -> Producer ByteString m() 
    -> Producer (SAXEvent tag text) m() 
parseProducer opt = enumerate . enumerate_ 
        . parseG opt 
        . Select_ . Select 

parseProducerLocations 
    :: (Monad m, GenericXMLString tag, GenericXMLString text) 
    => ParseOptions tag text 
    -> Producer ByteString m() 
    -> Producer (SAXEvent tag text, XMLParseLocation) m() 
parseProducerLocations opt = 
    enumerate . enumerate_ . parseLocationsG opt . Select_ . Select 

newtype ListT_ m a = Select_ { enumerate_ :: ListT m a } 
    deriving (Functor, Monad, MonadPlus, MonadIO 
      , Applicative, Alternative, Monoid, MonadTrans) 

instance Monad m => List (ListT_ m) where 
type ItemM (ListT_ m) = m 
joinL = Select_ . Select . I.M . liftM (enumerate . enumerate_) 
runList = liftM emend . next . enumerate . enumerate_ 
    where 
    emend (Right (a,q)) = Cons a (Select_ (Select q)) 
    emend _ = Nil 
Problemi correlati