Python: работа с XML-файлами и модуль xml.etree.ElementTree

Автор: | 04/18/2015
 

PythonВ стандартной библиотеке Python имеется две реализации этого модуля — xml.etree.ElementTree и xml.etree.cElementTree.

xml.etree.ElementTree — реализация API для работы с XML файлами на чистом Python, а xml.etree.cElementTree — то же, но на C, и даёт существенный прирост производительности при обработке больших файлов.

Можно импортировать их так:

#!/usr/bin/env python

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

print ET

Результат:

$ ./xml_par.py
<module 'xml.etree.cElementTree' from '/usr/lib64/python2.6/xml/etree/cElementTree.pyc'>

В Python версии 3.3 и выше необходимости в такой try/except нет, т.к. интерпретатор самостоятельно будет выполнять поиск cElementTree при импорте ElementTree (да и в Python 2.6 и 2.7 cElementTree импортируется без проблем).

Парсинг XML-файла

Для примера возьмём простой XML-файл с таким содержимым:

<TreeRoot>
    <Element1 value1="Value1">
        <SubElement1>SubElement1</SubElement1>
        <SubElement2>SubElement2</SubElement2>
    </Element1>

    <Element2 value2="Value2">
        <SubElement3>SubElement3</SubElement3>
        <SubElement4>SubElement4</SubElement4>
    </Element2>
</TreeRoot>

Изменим скрипт:

import os

import xml.etree.cElementTree as ET

XML_FILE = os.path.join(os.environ['HOME'], 'xmlfile.xml')

try:
    tree = ET.ElementTree(file=XML_FILE)
    print help(tree)
except IOError as e:
    print 'nERROR - cant find file: %sn' % e

В результате — мы должны получить список доступных методов для объекта tree:

Help on instance of ElementTree in module __builtin__:

class ElementTree(xml.etree.ElementTree.ElementTree)
 |  Methods defined here:
 |
 |  parse(self, source, parser=None)
 |
 |  ----------------------------------------------------------------------
 |  Methods inherited from xml.etree.ElementTree.ElementTree:
 |
 |  __init__(self, element=None, file=None)
 |
 |  find(self, path)
 |
 |  findall(self, path)
 |
 |  findtext(self, path, default=None)
 |
 |  getiterator(self, tag=None)
 |
 |  getroot(self)
 |
 |  write(self, file, encoding='us-ascii')

В случае ошибки синтаксиса XML — будет вызвана ошибка с указанием точного места:

cElementTree.ParseError: mismatched tag: line 11, column 2

Тогда файл стоит проверить в XML-валидаторе, например — тут>>>.

Для получения корневого элемента — используется метод getroot():

...
try:
    tree = ET.ElementTree(file=XML_FILE)
    print tree.getroot()
    print type(tree.getroot())
...
$ ./xml_par.py
<Element 'TreeRoot' at 0x6ffffd96120>
<type 'Element'>

Каждый элемент содержит несколько параметров:

  • tag — строка, отображающая тип данных, которые представляет элемент;
  • attrib — атрибуты элемента, которые сохраняются в словарь Python;
  • text — текстовое значение элемента;
  • дочерние элементы.

Например — получить тег корневого элемента можно так:

...
try:
    tree = ET.ElementTree(file=XML_FILE)
    root = tree.getroot()

    print root.tag
...
$ ./xml_par.py
TreeRoot

Что бы получить список прямых потомков корневого элемента — можно просто вызвать цикл:

...
try:
    tree = ET.ElementTree(file=XML_FILE)
    root = tree.getroot()

    for child_of_root in root:
        print child_of_root.tag, child_of_root.attrib
...

Результат:

$ ./xml_par.py
TreeRoot
Element1 {'value1': 'Value1'}
Element2 {'value2': 'Value2'}

Можно так же вывести только ключи, или ключи:значения:

...
    for child_of_root in root:
        print child_of_root.tag, child_of_root.keys(), child_of_root.items()
...

Результат:

$ ./xml_par.py
Element1 ['value1'] [('value1', 'Value1')]
Element2 ['value2'] [('value2', 'Value2')]

Поиск элементов в файле

Что бы перебрать все элементы в файле — можно воспользоваться методом iter():

...
try:
    tree = ET.ElementTree(file=XML_FILE)
    root = tree.getroot()

    for child_of_root in root.iter():
        print 'Tag: %snKeys: %snItems: %snText: %sn' % (child_of_root.tag, child_of_root.keys(), child_of_root.items(), child_of_root.text)
...

Результат:

$ ./xml_par.py
Tag: TreeRoot
Keys: []
Items: []
Text:

Tag: Element1
Keys: ['value1']
Items: [('value1', 'Value1')]
Text:

Tag: SubElement1
Keys: []
Items: []
Text: SubElement1

Tag: SubElement2
Keys: []
Items: []
Text: SubElement2

Tag: Element2
Keys: ['value2']
Items: [('value2', 'Value2')]
Text:

Tag: SubElement3
Keys: []
Items: []
Text: SubElement3

Tag: SubElement4
Keys: []
Items: []
Text: SubElement4

А что бы найти только один элемент — его тег можно передать аргументом этому методу:

...
    for child_of_root in root.iter('SubElement1'):
        print 'Tag: %snKeys: %snItems: %snText: %sn' % (child_of_root.tag, child_of_root.keys(), child_of_root.items(), child_of_root.text)
...

Результат:

$ ./xml_par.py
Tag: SubElement1
Keys: []
Items: []
Text: SubElement1

Можно выполнить поиск с помощью XPath.

Например, что бы отобразить корневой элемент:

...
try:
    tree = ET.ElementTree(file=XML_FILE)
    root = tree.getroot()

    for item in root.iterfind('.'):
        print 'Find: %sn' % item.tag
...

Результат:

$ ./xml_par.py
Find: TreeRoot

С помощью // можно найти все вложенные элементы:

...
    for item in root.iterfind('.//'):
        print 'Find: %sn' % item.tag
...
$ ./xml_par.py
Find: Element1

Find: SubElement1

Find: SubElement2

Find: Element2

Find: SubElement3

Find: SubElement4

Или вложенные элементы вложенного элемента:

    ...
    for item in root.iterfind('./Element1//'):
        print 'Find: %sn' % item.tag
    ...

Результат:

$ ./xml_par.py
Find: SubElement1

Find: SubElement2

Добавление записей в файл

Для начала — добавим функцию prettify:

from xml.dom import minidom

def prettify(elem):
    """Return a pretty-printed XML string for the Element.
    """
    rough_string = ET.tostring(elem, 'utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent='t')

Теперь — отредактируем код:

#!/usr/bin/env python

import os

import xml.etree.cElementTree as ET
from xml.dom import minidom

def prettify(elem):
    """Return a pretty-printed XML string for the Element.
    """
    rough_string = ET.tostring(elem, 'utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent='t')

XML_FILE = os.path.join(os.environ['HOME'], 'xmlfile.xml')

try:
    tree = ET.parse(XML_FILE)
    root = tree.getroot()

    new_element = ET.Element('NewElement')
    new_subelement = ET.SubElement(new_element, 'NewSubelement')
    new_subelement.text = 'NewSubelement'
    root.append(new_element)

    print prettify(root)

#  пока оставим так
#    tree.write(XML_FILE)

except IOError as e:
    print 'nERROR - cant find file: %sn' % e
  • с помощью ET.Element мы создаём новый элемент с именем NewElement;
  • затем — с помощью SubElement — мы добавляем новый вложенный элемент с имменем NewSubelement внутрь элемента в объекте new_element;
  • далее — мы определяем параметр text объекта new_subelement, и задаём занчение 'NewSubelement';
  • после этого — мы добавляем новый элемент new_element со всем содержимым к корню нашего файла;
  • последним — вызываем print() и функцию prettify().

Результат:

$ ./xml_par.py
<?xml version="1.0" ?>
<TreeRoot>
        <Element1 value1="Value1">

                <SubElement1>SubElement1</SubElement1>

                <SubElement2>SubElement2</SubElement2>

        </Element1>

        <Element2 value2="Value2">

                <SubElement3>SubElement3</SubElement3>

                <SubElement4>SubElement4</SubElement4>

        </Element2>
        <NewElement>
                <NewSubelement>NewSubelement</NewSubelement>
        </NewElement>
</TreeRoot>

Для того, что бы в таком виде записать в файл — можно использовать функцию indent():

def indent(elem, level=0):
    i = "n" + level*"  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
        for elem in elem:
            indent(elem, level+1)
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i

И вызвать её, передав корневой элемент, перед запись в файл:

...
    indent(root)

    tree.write(XML_FILE)
...

Результат:

$ cat xmlfile.xml
<TreeRoot>
  <Element1 value1="Value1">
    <SubElement1>SubElement1</SubElement1>
    <SubElement2>SubElement2</SubElement2>
  </Element1>
  <Element2 value2="Value2">
    <SubElement3>SubElement3</SubElement3>
    <SubElement4>SubElement4</SubElement4>
  </Element2>
  <NewElement>
    <NewSubelement>NewSubelement</NewSubelement>
  </NewElement>
</TreeRoot>

Файл с функциями можно скачать тут>>>.

Что бы добавить новый элемент внутрь уже имеющегося — можно указать его индекс в корне, например:

...
    new_element = ET.Element('NewElementInElement2')
    new_subelement = ET.SubElement(new_element, 'NewSubelementInElement2')
    new_subelement.text = 'NewSubelementInElement2'
    root[1].append(new_element)
...

Результат:

$ ./xml_par.py
<?xml version="1.0" ?>
<TreeRoot>

        <Element1 value1="Value1">

                <SubElement1>SubElement1</SubElement1>

                <SubElement2>SubElement2</SubElement2>

        </Element1>

        <Element2 value2="Value2">

                <SubElement3>SubElement3</SubElement3>

                <SubElement4>SubElement4</SubElement4>

                <NewElementInElement2>
                        <NewSubelementInElement2>NewSubelementInElement2</NewSubelementInElement2>
                </NewElementInElement2>
        </Element2>

        <NewElement>

                <NewSubelement>NewSubelement</NewSubelement>

        </NewElement>

</TreeRoot>

Ссылки по теме

http://effbot.org

https://docs.python.org

http://effbot.org

http://eli.thegreenplace.net

http://pymotw.com