Статьи

Обработка XML в Python с ElementTree

Когда дело доходит до синтаксического анализа и манипулирования XML, Python оправдывает свой девиз «батареи включены». Глядя на огромное количество модулей и инструментов, которые он делает доступными «из коробки» в стандартной библиотеке, может быть несколько подавляющим для программистов, плохо знакомых с Python и / или XML.

Несколько месяцев назад между разработчиками ядра Python состоялась интересная дискуссия об относительных достоинствах инструментов XML, доступных для Python, и о том, как лучше всего представить их пользователям. Эта статья (и, надеюсь, несколько других, которые последуют за ней) является моим скромным вкладом, в котором я планирую изложить свое мнение о том, какой инструмент должен быть предпочтительным и почему, а также представить дружественный учебник о том, как его использовать.

Код в этой статье демонстрируется с использованием Python 2.7; он может быть адаптирован для Python 3.x с небольшим количеством модификаций.

Какую библиотеку XML использовать?

Python имеет довольно много инструментов, доступных в стандартной библиотеке для обработки XML. В этом разделе я хочу дать краткий обзор пакетов, предлагаемых Python, и объяснить, почему ElementTree почти наверняка тот, который вы хотите использовать.

Модули xml.dom. * — реализуют API W3C DOM . Если вы привыкли работать с DOM API или у вас есть для этого какие-то требования, этот пакет может вам помочь. Обратите внимание, что в пакете xml.dom есть несколько модулей, представляющих различные компромиссы между производительностью и выразительностью.

Модули xml.sax. * — реализуют SAX API, который торгует удобством для скорости и потребления памяти. SAX — это API на основе событий, предназначенный для анализа огромных документов «на лету» без полной загрузки их в память [1] .

xml.parser.expat — прямой низкоуровневый API для синтаксического анализатора на основе C [2] . Интерфейс экспата основан на обратных вызовах событий, аналогично SAX. Но в отличие от SAX, интерфейс нестандартен и специфичен для библиотеки экспатов.

Наконец, есть xml.etree.ElementTree (отныне ET вкратце). Он предоставляет легкий Pythonic API, поддерживаемый эффективной реализацией C, для анализа и создания XML. По сравнению с DOM, ET гораздо быстрее [3] и имеет более приятный API для работы. По сравнению с SAX существует ET.iterparse, который также обеспечивает синтаксический анализ «на лету» без загрузки всего документа в память. Производительность на уровне SAX, но API более высокого уровня и намного удобнее в использовании; это будет продемонстрировано позже в статье.

Я рекомендую всегда использовать ET для обработки XML в Python, если у вас нет особых потребностей, которые могут потребовать других решений.

ElementTree — один API, две реализации

ElementTree — это API для управления XML, и он имеет две реализации в стандартной библиотеке Python. Одна из них — это чистая реализация Python в xml.etree.ElementTree, а другая — ускоренная реализация C в xml.etree.cElementTree. Важно помнить, что всегда следует использовать реализацию C, поскольку она намного, намного быстрее и потребляет значительно меньше памяти. Если ваш код может работать на платформах, которые могут не иметь доступного модуля расширения _elementtree [4] , вам понадобится заклинание импорта:

try:
    import xml.etree.cElementTree as ET
except ImportError:
    import xml.etree.ElementTree as ET

Это распространенная практика в Python, чтобы выбирать из нескольких реализаций одного и того же API. Хотя есть вероятность, что вам удастся просто импортировать первый модуль, ваш код может в конечном итоге работать на какой-то странной платформе, где это не удастся, так что вам лучше подготовиться к такой возможности. Обратите внимание, что начиная с Python 3.3, это больше не понадобится, поскольку модуль ElementTree будет искать сам ускоритель C и использовать реализацию Python, если она недоступна. Так что будет достаточно просто импортировать xml.etree.ElemenTree. Но пока не выйдет 3.3, и ваш код будет работать на нем, просто используйте двухэтапный импорт, представленный выше.

В любом случае, где бы в этой статье ни упоминался модуль ElementTree (ET), я имею в виду реализацию API ElementTree на языке C.

Разбор XML в дерево

Давайте начнем с основ. XML — это по сути иерархический формат данных, и наиболее естественным способом его представления является дерево. ET имеет два объекта для этой цели — ElementTree представляет весь XML-документ в виде дерева, а Element представляет один узел в этом дереве. Взаимодействия со всем документом (чтение, запись, поиск интересных элементов) обычно выполняются на уровне ElementTree. Взаимодействие с одним элементом XML и его подэлементами осуществляется на уровне элемента. Следующие примеры продемонстрируют основное использование [5] .

Мы собираемся использовать следующий XML-документ для примера кода:

<?xml version="1.0"?>
<doc>
    <branch name="testing" hash="1cdf045c">
        text,source
    </branch>
    <branch name="release01" hash="f200013e">
        <sub-branch name="subrelease01">
            xml,sgml
        </sub-branch>
    </branch>
    <branch name="invalid">
    </branch>
</doc>

Давайте загрузим и проанализируем документ:

>>> import xml.etree.cElementTree as ET
>>> tree = ET.ElementTree(file='doc1.xml')

Теперь давайте выберем корневой элемент:

>>> tree.getroot()
<Element 'doc' at 0x11eb780>

Как и ожидалось, корень является объектом Element. Мы можем изучить некоторые из его атрибутов:

>>> root = tree.getroot()
>>> root.tag, root.attrib
('doc', {})

Правда, корневой элемент не имеет атрибутов [6] . Как и любой элемент, он представляет собой итеративный интерфейс для просмотра своих прямых потомков:

>>> for child_of_root in root:
...   print child_of_root.tag, child_of_root.attrib
...
branch {'hash': '1cdf045c', 'name': 'testing'}
branch {'hash': 'f200013e', 'name': 'release01'}
branch {'name': 'invalid'}

Мы также можем получить доступ к конкретному дочернему элементу по индексу:

>>> root[0].tag, root[0].text
('branch', '\n        text,source\n    ')

Нахождение интересных элементов

Из приведенных выше примеров очевидно, как мы можем получить доступ ко всем элементам в документе XML и запросить их с помощью простой рекурсивной процедуры (для каждого элемента рекурсивно посетите все его дочерние элементы). Однако, поскольку это может быть обычной задачей, ET предлагает несколько полезных инструментов для ее упрощения.

Объект Element имеет метод iter, который обеспечивает итерацию в глубину (DFS) для всех подэлементов под ним. Для удобства объект ElementTree также имеет метод iter, вызывающий iter root. Вот самый простой способ найти все элементы в документе:

>>> for elem in tree.iter():
...   print elem.tag, elem.attrib
...
doc {}
branch {'hash': '1cdf045c', 'name': 'testing'}
branch {'hash': 'f200013e', 'name': 'release01'}
sub-branch {'name': 'subrelease01'}
branch {'name': 'invalid'}

Естественно, это может послужить основой для произвольной итерации дерева — пройтись по всем элементам, изучить те, которые имеют интересные свойства. Однако ET может сделать эту задачу более удобной и эффективной. Для этого метод iter принимает имя тега и выполняет итерацию только для тех элементов, которые имеют обязательный тег:

>>> for elem in tree.iter(tag='branch'):
...   print elem.tag, elem.attrib
...
branch {'hash': '1cdf045c', 'name': 'testing'}
branch {'hash': 'f200013e', 'name': 'release01'}
branch {'name': 'invalid'}

Поддержка XPath для поиска элементов

Гораздо более эффективный способ поиска интересных элементов с помощью ET — это поддержка XPath . Элемент имеет несколько методов поиска, которые могут принимать путь XPath в качестве аргумента. find возвращает первый соответствующий подэлемент, findall все соответствующие подэлементы в списке, а iterfind предоставляет итератор для всех соответствующих элементов. Эти методы также существуют в ElementTree, начиная поиск по корневому элементу.

Вот пример для нашего документа:

>>> for elem in tree.iterfind('branch/sub-branch'):
...   print elem.tag, elem.attrib
...
sub-branch {'name': 'subrelease01'}

Он нашел все элементы в ветке с тегом дерева, которые находятся ниже элемента, называемого ветвью. А вот как найти все элементы ветви с определенным атрибутом имени:

>>> for elem in tree.iterfind('branch[@name="release01"]'):
...   print elem.tag, elem.attrib
...
branch {'hash': 'f200013e', 'name': 'release01'}

Для изучения поддержки синтаксиса XPath ET, смотрите эту страницу .

Создание XML-документов

ET предоставляет простой способ для создания XML-документов и записи их в файлы. Для этого у объекта ElementTree есть метод write.

Теперь, вероятно, есть два основных сценария использования XML-документов. Вы либо прочитаете один, измените его и напишите его обратно, либо создадите новый документ с нуля.

Изменение документов может быть сделано с помощью манипулирования объектами Element. Вот простой пример:

>>> root = tree.getroot()
>>> del root[2]
>>> root[0].set('foo', 'bar')
>>> for subelem in root:
...   print subelem.tag, subelem.attrib
...
branch {'foo': 'bar', 'hash': '1cdf045c', 'name': 'testing'}
branch {'hash': 'f200013e', 'name': 'release01'}

Здесь мы удалили 3-го дочернего элемента корневого элемента и добавили новый атрибут к первому дочернему элементу. Затем дерево можно записать обратно в файл. Вот как будет выглядеть результат:

>>> import sys
>>> tree.write(sys.stdout)   # ET.dump can also serve this purpose
<doc>
    <branch foo="bar" hash="1cdf045c" name="testing">
        text,source
    </branch>
    <branch hash="f200013e" name="release01">
        <sub-branch name="subrelease01">
            xml,sgml
        </sub-branch>
    </branch>
    </doc>

Обратите внимание, что порядок атрибутов отличается от исходного документа. Это связано с тем, что ET хранит атрибуты в словаре, который является неупорядоченной коллекцией. Семантически XML не заботится о порядке атрибутов.

Создать совершенно новые элементы тоже легко. Модуль ET предоставляет фабричную функцию SubElement для упрощения процесса:

>>> a = ET.Element('elem')
>>> c = ET.SubElement(a, 'child1')
>>> c.text = "some text"
>>> d = ET.SubElement(a, 'child2')
>>> b = ET.Element('elem_b')
>>> root = ET.Element('root')
>>> root.extend((a, b))
>>> tree = ET.ElementTree(root)
>>> tree.write(sys.stdout)
<root><elem><child1>some text</child1><child2 /></elem><elem_b /></root>

Синтаксический анализ потока XML с помощью iterparse

Как я упоминал в начале этой статьи, XML-документы имеют тенденцию становиться огромными, и библиотеки, которые полностью считывают их в память, могут иметь проблемы, когда требуется анализ таких документов. Это одна из причин использования SAX API в качестве альтернативы DOM.

Мы только что узнали, как использовать ET для простого считывания XML в дерево в памяти и манипулирования им. Но разве он не страдает от той же проблемы с перехватом памяти, что и DOM при разборе больших документов? Да. Вот почему пакет предоставляет специальный инструмент для SAX-подобного анализа XML на лету. Этот инструмент является редким.

Теперь я буду использовать полный пример, чтобы продемонстрировать, как можно использовать iterparse, а также измерить, как это происходит по сравнению со стандартным синтаксическим анализом дерева. Я автоматически генерирую XML-документ для работы. Вот маленькая часть с самого начала:

<?xml version="1.0" standalone="yes"?>
<site>
  <regions>
    <africa>
      <item id="item0">
        <location>United States</location>    <!-- Counting locations -->
        <quantity>1</quantity>
        <name>duteous nine eighteen </name>
        <payment>Creditcard</payment>
        <description>
          <parlist>
[...]

Я подчеркнул элемент, на который я буду ссылаться в примере с комментарием. Мы увидим простой скрипт, который подсчитывает, сколько таких элементов местоположения есть в документе с текстовым значением «Зимбабве». Вот стандартный подход с использованием ET.parse:

tree = ET.parse(sys.argv[2])

count = 0
for elem in tree.iter(tag='location'):
    if elem.text == 'Zimbabwe':
        count += 1

print count

Все элементы в дереве XML проверяются на наличие желаемой характеристики. При вызове XML-файла размером ~ 100 МБ пиковое использование памяти процессом Python, выполняющим этот сценарий, составляет ~ 560 МБ, а запуск занимает 2,9 секунды.

Обратите внимание, что для этой задачи нам не нужно целое дерево в памяти. Достаточно просто обнаружить элементы местоположения с желаемым значением. Все остальные данные могут быть отброшены. Это где iterparse приходит:

count = 0
for event, elem in ET.iterparse(sys.argv[2]):
    if event == 'end':
        if elem.tag == 'location' and elem.text == 'Zimbabwe':
            count += 1
    elem.clear() # discard the element

print count

Цикл перебирает события iterparse, обнаруживая «конечные» события для тега location, ища нужное значение. Ключевым здесь является вызов elem.clear () — он все еще строит дерево, делая это на лету. Очистка элемента эффективно отбрасывает дерево [7] , освобождая выделенную память.

При запуске в том же файле пиковое использование памяти этим сценарием составляет всего 7 МБ, а запуск занимает 2,5 секунды. Повышение скорости связано с тем, что мы проходим дерево только один раз, пока оно строится. Подход синтаксического анализа сначала строит целое дерево, а затем снова обходит его, чтобы найти интересные элементы.

Производительность iterparse сравнима с SAX, но его API гораздо полезнее — поскольку он строит дерево на лету для вас; SAX просто дает вам события, и вы сами строите дерево.

Вывод

ElementTree действительно выделяется из множества модулей, предлагаемых Python для обработки XML. Он сочетает в себе легкий Pythonic API с превосходной производительностью благодаря своему модулю C-ускорителя. Учитывая все это, почти никогда не имеет смысла не использовать его, если вам нужно проанализировать или произвести XML в Python.

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

http://eli.thegreenplace.net/wp-content/uploads/hline.jpg

[1] В отличие от DOM, который загружает весь документ в память и обеспечивает «произвольный доступ» к его элементам на любой глубине.
[2] Expat — это библиотека C с открытым исходным кодом для анализа XML. Python использует его в своем распространении и служит основой возможностей синтаксического анализа XML в Python.
[3] Фредрик Лунд, первоначальный автор ElementTree, собрал здесь несколько тестов . Прокрутите страницу вниз, чтобы увидеть их.
[4] Когда я упоминаю _elementtree в этой статье, я имею в виду ускоритель C, который используется для cElementTree. _elementtree — это модуль расширения C для Python, который является частью стандартного дистрибутива.
[5] Обязательно имейте под рукой документацию по модулю и найдите методы, которые я вызываю, чтобы лучше понять переданные параметры.
[6] Атрибуты — перегруженный термин здесь. Есть атрибуты объекта Python, и есть атрибуты элемента XML. Надеюсь, это будет очевидно из контекста, который подразумевается.
[7] Чтобы быть точным, корневой элемент дерева все еще жив. В том маловероятном случае, когда корневой элемент очень большой, вы также можете от него отказаться, но для этого потребуется немного больше кода.