2012-03-21 4 views
9

Come leggere un file XML utilizzando sax e convertirlo in un elemento lxml etree.iterparse?Python sax a lxml per 80 + GB XML

Per fornire una panoramica del problema, ho creato uno strumento di ingestione XML utilizzando lxml per un feed XML che avrà una dimensione di 25 - 500 MB che richiede l'ingestione su base bi-giornaliera, ma deve eseguire un una volta l'ingestione di un file da 60 a 100 GB.

Avevo scelto di utilizzare lxml in base alle specifiche che dettagliavano un nodo non avrebbe superato la dimensione di 4 -8 GB che pensavo avrebbe consentito al nodo di essere letto in memoria e cancellato al termine.

Una panoramica se il codice è inferiore

elements = etree.iterparse(
    self._source, events = ('end',) 
) 
for event, element in elements: 
    finished = True 
    if element.tag == 'Artist-Types': 
     self.artist_types(element) 

def artist_types(self, element): 
    """ 
    Imports artist types 

    :param list element: etree.Element 
    :returns boolean: 
    """ 
    self._log.info("Importing Artist types") 
    count = 0 
    for child in element: 
     failed = False 
     fields = self._getElementFields(child, (
      ('id', 'Id'), 
      ('type_code', 'Type-Code'), 
      ('created_date', 'Created-Date') 
     )) 
     if self._type is IMPORT_INC and has_artist_type(fields['id']): 
      if update_artist_type(fields['id'], fields['type_code']): 
       count = count + 1 
      else: 
       failed = True 
     else: 
      if create_artist_type(fields['type_code'], 
       fields['created_date'], fields['id']): 
       count = count + 1 
      else: 
       failed = True 
     if failed: 
      self._log.error("Failed to import artist type %s %s" % 
       (fields['id'], fields['type_code']) 
      ) 
    self._log.info("Imported %d Artist Types Records" % count) 
    self._artist_type_count = count 
    self._cleanup(element) 
    del element 

Fatemi sapere se posso aggiungere qualsiasi tipo di chiarimento.

+0

Quindi qual è la domanda? Hai ricevuto un messaggio di errore? –

+2

La domanda è nella prima frase ... perché il downvote? – Nick

+0

La tua domanda è un po 'strana. Perché stai usando SAX? iterparse è * un'alternativa a * SAX. Potresti generare eventi iterparse da eventi SAX, ma perché qualcuno dovrebbe farlo? –

risposta

19

iterparse è un parser iterativo. Emetterà gli oggetti e gli eventi Element e incrementerà progressivamente l'intero albero Element mentre analizza, quindi alla fine avrà l'intero albero in memoria.

Tuttavia, è facile avere un comportamento di memoria limitato: eliminare elementi non più necessari mentre li si analizza.

Il tipico carico di lavoro "xml gigante" è un singolo elemento radice con un numero elevato di elementi figlio che rappresentano i record. Presumo che questo sia il tipo di struttura XML con cui stai lavorando?

Di solito è sufficiente utilizzare clear() per svuotare l'elemento che si sta elaborando. Il tuo utilizzo della memoria crescerà un po 'ma non è molto. Se hai un file veramente enorme, anche gli oggetti vuoti Element consumano troppo e in questo caso devi anche eliminare gli oggetti Element visti in precedenza. Si noti che non è possibile eliminare in sicurezza l'elemento corrente. Il lxml.etree.iterparse documentation describes this technique.

In questo caso, verrà elaborato un record ogni volta che viene trovato uno </record>, quindi verranno eliminati tutti gli elementi del record precedenti.

Di seguito è riportato un esempio che utilizza un documento XML infinitamente lungo. Stampa l'utilizzo della memoria del processo durante l'analisi. Si noti che l'utilizzo della memoria è stabile e non continua a crescere.

from lxml import etree 
import resource 

class InfiniteXML (object): 
    def __init__(self): 
     self._root = True 
    def read(self, len=None): 
     if self._root: 
      self._root=False 
      return "<?xml version='1.0' encoding='US-ASCII'?><records>\n" 
     else: 
      return """<record>\n\t<ancestor attribute="value">text value</ancestor>\n</record>\n""" 

def parse(fp): 
    context = etree.iterparse(fp, events=('end',)) 
    for action, elem in context: 
     if elem.tag=='record': 
      # processing goes here 
      pass 

     #memory usage 
     print resource.getrusage(resource.RUSAGE_SELF).ru_maxrss 

     # cleanup 
     # first empty children from current element 
      # This is not absolutely necessary if you are also deleting siblings, 
      # but it will allow you to free memory earlier. 
     elem.clear() 
     # second, delete previous siblings (records) 
     while elem.getprevious() is not None: 
      del elem.getparent()[0] 
     # make sure you have no references to Element objects outside the loop 

parse(InfiniteXML()) 
+0

Non c'è un singolo nodo "root", piuttosto i dati sono suddivisi in 20 o più "root" nodi ciascuno contenente i propri sottoinsiemi. Lo strumento attuale funziona in un modo simile al codice per quanto riguarda la rimozione di eventuali nodi non necessari una volta elaborati e questo consente di elaborare una porzione piuttosto ampia dei dati ma una volta che cerco di elaborare uno dei nodi più grandi "I'm assumendo dimensioni maggiori di 8 GB "il processo segnerà (al ciclo for)" per azione, elem in context: "" che mi sta portando a credere che sia stato letto nella memoria. – Nick

+0

Puoi mostrare qualche esempio di XML? Il codice che hai postato sembra solo mostrare un tipo di elemento principale. Iterparse non sta leggendo l'intero file in memoria, quindi si tratta di suddividere il flusso di lavoro in sottostrutture più piccole che * fanno * si adattano alla memoria e che eliminano ogni cosa dopo ogni iterazione. –

+1

Il codice pubblicato sopra è il massimo che posso dare sfortunatamente, ma con ciò detto dopo aver riscritto una buona parte dell'ingestione, l'importazione ora funziona utilizzando l'approccio sopra riportato. Vedere lo snippet seguente per il codice https://gist.github.com/2161849. – Nick

3

Ho trovato questo utile esempio su http://effbot.org/zone/element-iterparse.htm. L'enfasi in grassetto è mia.

incrementale di analisi #

noti che iterparse costruisce ancora un albero, proprio come parse, ma si può tranquillamente riorganizzare o rimuovere parti dell'albero durante l'analisi. Ad esempio, per analizzare i file di grandi dimensioni, si può sbarazzarsi di elementi non appena li avete trasformati:

for event, elem in iterparse(source): 
    if elem.tag == "record": 
     ... process record elements ... 
     elem.clear() 

Il modello di cui sopra ha uno svantaggio; non cancella l'elemento radice, quindi finirai con un singolo elemento con molti elementi figlio vuoti. Se i file sono enormi, piuttosto che grandi, questo potrebbe essere un problema. Per ovviare a questo, è necessario mettere le mani sull'elemento principale. Il modo più semplice per farlo è quello di consentire eventi di avvio, e salvare un riferimento al primo elemento in una variabile:

# get an iterable 
context = iterparse(source, events=("start", "end")) 

# turn it into an iterator 
context = iter(context) 

# get the root element 
event, root = context.next() 

for event, elem in context: 
    if event == "end" and elem.tag == "record": 
     ... process record elements ... 
     root.clear() 

(versioni future renderanno più facile l'accesso l'elemento principale da dentro il ciclo)

+0

Grazie per la risposta ma l'ho già esplorato e almeno dal mio test il nodo è ancora letto interamente in memoria e non è in streaming – Nick

0

Si tratta di un paio di anni e non hanno abbastanza reputazione di commentare direttamente sulla risposta accettata, ma ho cercato di utilizzare questo per analizzare un OSM dove mi sto trovando tutte le intersezioni in un paese. Il mio problema originale era che stavo esaurendo la RAM, quindi ho pensato che avrei dovuto usare il parser SAX ma ho trovato invece questa risposta. Stranamente non stava analizzando correttamente, e l'uso della pulizia suggerita in qualche modo stava cancellando il nodo elem prima di leggerlo (ancora non sono sicuro di come questo stava accadendo). Rimosso elem.clear() dal codice e ora funziona perfettamente!