webentwicklung-frage-antwort-db.com.de

Verwenden von Python-Iterparse für große XML-Dateien

Ich muss in Python einen Parser schreiben, der einige extrem große Dateien (> 2 GB) auf einem Computer ohne viel Speicher (nur 2 GB) verarbeiten kann. Ich wollte iterparse in lxml verwenden, um es zu tun.

Meine Datei hat das Format:

<item>
  <title>Item 1</title>
  <desc>Description 1</desc>
</item>
<item>
  <title>Item 2</title>
  <desc>Description 2</desc>
</item>

und bis jetzt ist meine Lösung:

from lxml import etree

context = etree.iterparse( MYFILE, tag='item' )

for event, elem in context :
      print elem.xpath( 'description/text( )' )

del context

Leider verbraucht diese Lösung jedoch immer noch viel Speicher. Ich denke, das Problem ist, dass ich nach dem Umgang mit jedem "ITEM" etwas tun muss, um leere Kinder zu säubern. Kann mir jemand Vorschläge geben, was ich tun kann, nachdem ich meine Daten ordnungsgemäß bereinigt habe?

36
Dave Johnshon

Versuchen Sie Liza Dalys fast_iter . Nach der Verarbeitung eines Elements, elem, ruft es elem.clear() auf, um Nachkommen zu entfernen, und entfernt auch die vorherigen Geschwister. 

def fast_iter(context, func, *args, **kwargs):
    """
    http://lxml.de/parsing.html#modifying-the-tree
    Based on Liza Daly's fast_iter
    http://www.ibm.com/developerworks/xml/library/x-hiperfparse/
    See also http://effbot.org/zone/element-iterparse.htm
    """
    for event, elem in context:
        func(elem, *args, **kwargs)
        # It's safe to call clear() here because no descendants will be
        # accessed
        elem.clear()
        # Also eliminate now-empty references from the root node to elem
        for ancestor in elem.xpath('ancestor-or-self::*'):
            while ancestor.getprevious() is not None:
                del ancestor.getparent()[0]
    del context


def process_element(elem):
    print elem.xpath( 'description/text( )' )

context = etree.iterparse( MYFILE, tag='item' )
fast_iter(context,process_element)

Daly's Artikel ist eine hervorragende Lektüre, besonders wenn Sie große XML-Dateien verarbeiten.


Edit: Der oben angegebene fast_iter ist eine modifizierte Version von Daly's fast_iter. Nach der Bearbeitung eines Elements werden andere Elemente, die nicht mehr benötigt werden, aggressiver entfernt.

Das folgende Skript zeigt den Unterschied im Verhalten. Beachten Sie insbesondere, dass orig_fast_iter das A1-Element nicht löscht, während es von mod_fast_iter gelöscht wird, wodurch mehr Speicher eingespart wird.

import lxml.etree as ET
import textwrap
import io

def setup_ABC():
    content = textwrap.dedent('''\
      <root>
        <A1>
          <B1></B1>
          <C>1<D1></D1></C>
          <E1></E1>
        </A1>
        <A2>
          <B2></B2>
          <C>2<D></D></C>
          <E2></E2>
        </A2>
      </root>
        ''')
    return content


def study_fast_iter():
    def orig_fast_iter(context, func, *args, **kwargs):
        for event, elem in context:
            print('Processing {e}'.format(e=ET.tostring(elem)))
            func(elem, *args, **kwargs)
            print('Clearing {e}'.format(e=ET.tostring(elem)))
            elem.clear()
            while elem.getprevious() is not None:
                print('Deleting {p}'.format(
                    p=(elem.getparent()[0]).tag))
                del elem.getparent()[0]
        del context

    def mod_fast_iter(context, func, *args, **kwargs):
        """
        http://www.ibm.com/developerworks/xml/library/x-hiperfparse/
        Author: Liza Daly
        See also http://effbot.org/zone/element-iterparse.htm
        """
        for event, elem in context:
            print('Processing {e}'.format(e=ET.tostring(elem)))
            func(elem, *args, **kwargs)
            # It's safe to call clear() here because no descendants will be
            # accessed
            print('Clearing {e}'.format(e=ET.tostring(elem)))
            elem.clear()
            # Also eliminate now-empty references from the root node to elem
            for ancestor in elem.xpath('ancestor-or-self::*'):
                print('Checking ancestor: {a}'.format(a=ancestor.tag))
                while ancestor.getprevious() is not None:
                    print(
                        'Deleting {p}'.format(p=(ancestor.getparent()[0]).tag))
                    del ancestor.getparent()[0]
        del context

    content = setup_ABC()
    context = ET.iterparse(io.BytesIO(content), events=('end', ), tag='C')
    orig_fast_iter(context, lambda elem: None)
    # Processing <C>1<D1/></C>
    # Clearing <C>1<D1/></C>
    # Deleting B1
    # Processing <C>2<D/></C>
    # Clearing <C>2<D/></C>
    # Deleting B2

    print('-' * 80)
    """
    The improved fast_iter deletes A1. The original fast_iter does not.
    """
    content = setup_ABC()
    context = ET.iterparse(io.BytesIO(content), events=('end', ), tag='C')
    mod_fast_iter(context, lambda elem: None)
    # Processing <C>1<D1/></C>
    # Clearing <C>1<D1/></C>
    # Checking ancestor: root
    # Checking ancestor: A1
    # Checking ancestor: C
    # Deleting B1
    # Processing <C>2<D/></C>
    # Clearing <C>2<D/></C>
    # Checking ancestor: root
    # Checking ancestor: A2
    # Deleting A1
    # Checking ancestor: C
    # Deleting B2

study_fast_iter()
52
unutbu

iterparse() ermöglicht es Ihnen, Dinge zu erledigen , während Sie den Baum bauen . Wenn Sie also nicht entfernen, was Sie nicht mehr benötigen, werden Sie es trotzdem tun Am Ende steht der ganze Baum.

Weitere Informationen: lies this vom Autor der ursprünglichen ElementTree-Implementierung (aber es gilt auch für lxml)

4
Steven

Nach meiner Erfahrung iterparse mit oder ohne element.clear (siehe F. Lundh und L. Daly) kommen nicht immer mit sehr großen XML-Dateien zurecht: Es geht eine Weile gut, plötzlich geht der Speicherverbrauch durch das Dach und ein Speicherfehler tritt auf oder die System stürzt ab. Wenn Sie auf dasselbe Problem stoßen, können Sie möglicherweise dieselbe Lösung verwenden: den Expat-Parser . Siehe auch F. Lundh oder das folgende Beispiel unter Verwendung des XML-Snippets von OP (plus zwei Umlaute, um zu überprüfen, ob Codierungsprobleme vorliegen):

import xml.parsers.expat
from collections import deque

def iter_xml(inpath: str, outpath: str) -> None:
    def handle_cdata_end():
        nonlocal in_cdata
        in_cdata = False

    def handle_cdata_start():
        nonlocal in_cdata
        in_cdata = True

    def handle_data(data: str):
        nonlocal in_cdata
        if not in_cdata and open_tags and open_tags[-1] == 'desc':
            data = data.replace('\\', '\\\\').replace('\n', '\\n')
            outfile.write(data + '\n')

    def handle_endtag(tag: str):
        while open_tags:
            open_tag = open_tags.pop()
            if open_tag == tag:
                break

    def handle_starttag(tag: str, attrs: 'Dict[str, str]'):
        open_tags.append(tag)

    open_tags = deque()
    in_cdata = False
    parser = xml.parsers.expat.ParserCreate()
    parser.CharacterDataHandler = handle_data
    parser.EndCdataSectionHandler = handle_cdata_end
    parser.EndElementHandler = handle_endtag
    parser.StartCdataSectionHandler = handle_cdata_start
    parser.StartElementHandler = handle_starttag
    with open(inpath, 'rb') as infile:
        with open(outpath, 'w', encoding = 'utf-8') as outfile:
            parser.ParseFile(infile)

iter_xml('input.xml', 'output.txt')

input.xml:

<root>
    <item>
    <title>Item 1</title>
    <desc>Description 1ä</desc>
    </item>
    <item>
    <title>Item 2</title>
    <desc>Description 2ü</desc>
    </item>
</root>

output.txt:

Description 1ä
Description 2ü
1
Stefan

Warum verwenden Sie nicht den "Rückruf" -Ansatz von sax ?

1

Beachten Sie, dass iterparse wie beim Parsen immer noch einen Baum erstellt, Sie jedoch beim Parsen Teile des Baums sicher neu anordnen oder entfernen können. Wenn Sie beispielsweise große Dateien analysieren möchten, können Sie Elemente entfernen, sobald Sie sie verarbeitet haben:

for event, elem in iterparse(source): if elem.tag == "record": ... process record elements ... elem.clear() Das obige Muster hat einen Nachteil; Das Stammelement wird nicht gelöscht, sodass Sie ein einzelnes Element mit vielen leeren untergeordneten Elementen erhalten. Wenn Ihre Dateien nicht nur groß, sondern sehr groß sind, ist dies möglicherweise ein Problem. Um dies zu umgehen, müssen Sie das Root-Element in die Hände bekommen. Der einfachste Weg, dies zu tun, besteht darin, Startereignisse zu aktivieren und einen Verweis auf das erste Element in einer Variablen zu speichern:

erhalten Sie eine iterable

context = iterparse(source, events=("start", "end"))

verwandle es in einen Iterator

context = iter(context)

holen Sie sich das Wurzelelement

event, root = context.next()

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

Dies ist also eine Frage des inkrementellen Parsings. Über diesen Link erhalten Sie eine detaillierte Antwort für eine zusammengefasste Antwort, auf die Sie sich oben beziehen können

0
Ash Upadhyay

Das einzige Problem bei der root.clear () -Methode ist, dass NoneTypes zurückgegeben wird. Dies bedeutet, dass Sie beispielsweise nicht bearbeiten können, welche Daten Sie mit String-Methoden wie replace () oder title () analysieren. Dies ist jedoch eine optimale Methode, wenn Sie die Daten nur so analysieren, wie sie sind.

0
Jason Argo