Создание XML в Python 3

Published: 15.07.2015

Создать XML в Python довольно просто.

Самые популярные методы можно легко найти на Stackowerflow: Best way to generate xml in python?

В лидерах две библиотеки: built-in библиотека ElementTree и сторонная lxml.

Они имеют приблизительно одинаковый синтаксис, и в большинстве случаев предпочтительнее использовать ElementTree, чтобы одной внешней библиотекой было меньше. Однако, ElementTree не имеет метода для создания текстового узла CDATA, а также нет возможности отформатировать создаваемый XML в виде prettyprint, т.е., с вложенными отступами и переносами строк для удобного визуального просмотра.

Так что, если вам внезапно захотелось создать XML только встроенными средствами языка Python, но нужны такие возможности, как CDATA или prettyprint, то выход есть: minidom

В вышеупомянутом вопросе есть и ответ с примером использования minidom. Позволю себе его скопировать целиком, потому что это мой ответ, чего уж греха таить.

minidom_example.py
  1.  
  2.     from xml.dom import minidom
  3.  
  4.     doc = minidom.Document()
  5.  
  6.     root = doc.createElement('root')
  7.     doc.appendChild(root)
  8.  
  9.     leaf = doc.createElement('leaf')
  10.     text = doc.createTextNode('Text element with attributes')
  11.     leaf.appendChild(text)
  12.     leaf.setAttribute('color', 'white')
  13.     root.appendChild(leaf)
  14.  
  15.     leaf_cdata = doc.createElement('leaf_cdata')
  16.     cdata = doc.createCDATASection('<em>CData</em> can contain <strong>HTML tags</strong> without encoding')
  17.     leaf_cdata.appendChild(cdata)
  18.     root.appendChild(leaf_cdata)
  19.  
  20.     branch = doc.createElement('branch')
  21.     branch.appendChild(leaf.cloneNode(True))
  22.     root.appendChild(branch)
  23.  
  24.     mixed = doc.createElement('mixed')
  25.     mixed_leaf = leaf.cloneNode(True)
  26.     mixed_leaf.setAttribute('color', 'black')
  27.     mixed_leaf.setAttribute('state', 'modified')
  28.     mixed.appendChild(mixed_leaf)
  29.     mixed_text = doc.createTextNode('Do not use mixed elements if it possible.')
  30.     mixed.appendChild(mixed_text)
  31.     root.appendChild(mixed)
  32.  
  33.     xml_str = doc.toprettyxml(indent="  ")
  34.     with open("minidom_example.xml", "w") as f:
  35.         f.write(xml_str)

Результатом будет XML файл со следующим содержанием:

minidom_example.xml

<?xml version="1.0" ?>
<root>
  <leaf color="white">Text element with attributes</leaf>
  <leaf_cdata>
<![CDATA[<em>CData</em> can contain <strong>HTML tags</strong> without encoding]]>  </leaf_cdata>
  <branch>
    <leaf color="white">Text element with attributes</leaf>
  </branch>
  <mixed>
    <leaf color="black" state="modified">Text element with attributes</leaf>
    Do not use mixed elements if it possible.
  </mixed>
</root>

Результат получен с учётом всех требований. Но код слегка многословный, почему? Для начала предлагаю взглянуть на созданный XML и ввести несколько определений. С некоторой степенью упрощения элементы в XML-документе можно классифицировать следующим образом:

  • <root> – корневой элемент, может быть только один. Это не отдельный тип, корень может быть как ветвью, так и смешанным и даже листом. И даже пустым.
  • <branch> – 'ветвь', имеет дочерние 'ветви' и 'листья', без текста
  • <leaf> – 'лист', не имеет дочерних элементов, содержит только текстовый узел или ничего (напр.: <leaf />)
  • <mixed> – смешанный элемент, который содержит как текст, так и вложенные элементы. Смешанные элементы усложняют XML структуру и последующий парсинг, поэтому, если вы разработчик XML-формата и в вашей структуре возможны смешанные элементы — одумайтесь, пока это не ушло в продакшн!

Все перечисленные элементы могут иметь произвольное количество аттрибутов.

Теперь взглянем, что нужно сделать, чтобы создать элемент:

– создать элемент
– если это <leaf> (или <mixed>), и он содержит текст, нужно создать текстовый узел (обычный или CData) и добавить его к элементу
– если элемент содержит аттрибуты, то каждый аттрибут должен быть добавлен отдельно с помощью метода setAttribute(name, value). Обратите внимание, что value может быть только строкой.

После создания элемента, его можно присоединить как дочерний к уже созданному.

Так как же можно оптимизировать код? Ответ на поверхности — обернуть эти операции в функцию. И вынести её в отдельный пакет (extended_minidom)

extended_minidom.py
  1.      
  2.     from xml.dom import minidom
  3.      
  4.     def create_tag(name: str=None, text: str=None, attributes: dict=None, *, cdata: bool=False):
  5.         doc = minidom.Document()
  6.    
  7.         if name is None:
  8.             return doc
  9.    
  10.         tag = doc.createElement(name)
  11.    
  12.         if text is not None:
  13.             if cdata is True:
  14.                 tag.appendChild(doc.createCDATASection(text))
  15.             else:
  16.                 tag.appendChild(doc.createTextNode(text))
  17.    
  18.         if attributes is not None:
  19.             for k, v in attributes.items():
  20.                 tag.setAttribute(k, str(v))
  21.    
  22.         return tag
  23.  

Подключив функцию в основной файл, мы имеем универсальный инструмент для создания элементов, передавая (или не передавая) в параметры функции необходимые значения:
    name - имя элемента (если не задано, то вернёт minidom.Document()),
    text - текст,
    attributes - список аттрибутов, значения аттрибутов могут быть числовыми,
    cdata=True (если надо текст обернуть в блок CData).

Результатом будет сокращение основного кода, особенно при сложной структуре документа.

app.py
  1.      
  2.     from extended_minidom import create_tag
  3.      
  4.     doc = create_tag()
  5.      
  6.     root = create_tag('root')
  7.     doc.appendChild(root)
  8.      
  9.     leaf = create_tag('leaf', 'Text element with attributes', {'color': 'white'})
  10.     root.appendChild(leaf)
  11.      
  12.     leaf_cdata = create_tag('leaf_cdata', '<em>CData</em> can contain <strong>HTML tags</strong> without encoding', cdata=True)
  13.     root.appendChild(leaf_cdata)
  14.      
  15.     branch = create_tag('branch')
  16.     branch.appendChild(leaf.cloneNode(True))
  17.     root.appendChild(branch)
  18.      
  19.     mixed = create_tag('mixed', 'Do not use mixed elements if it possible.')
  20.     mixed_leaf = create_tag('leaf', 'Text element with attributes', {'color': 'black', 'state': 'modified'})
  21.     mixed.appendChild(mixed_leaf)
  22.     root.appendChild(mixed)
  23.      
  24.     xml_str = doc.toprettyxml(indent="  ")
  25.     with open("minidom_example2.xml", "w") as f:
  26.         f.write(xml_str)

В некоторых случаях может быть удобно листья сразу создавать внутри метода appendChild() для ветвей:

app.py
  1.      
  2.     from extended_minidom import create_tag
  3.      
  4.     doc = create_tag()
  5.  
  6.     root = create_tag('root')
  7.     doc.appendChild(root)
  8.  
  9.     branch = create_tag('branch')
  10.     branch.appendChild(create_tag('leaf', 'Text element with attributes', {'color': 'white'}))
  11.  
  12.     mixed = create_tag('mixed', 'Do not use mixed elements if it possible.')
  13.     mixed.appendChild(create_tag('leaf', 'Text element with attributes', {'number': 9000}))
  14.  
  15.     root.appendChild(create_tag('leaf', 'Text element with attributes', {'color': 'white'}))
  16.     root.appendChild(create_tag('leaf_cdata', '<em>CData</em> can contain <strong>HTML tags</strong> without encoding', cdata=True))
  17.     root.appendChild(branch)
  18.     root.appendChild(mixed)
  19.  
  20.     xml_str = doc.toprettyxml(indent="  ")
  21.     with open("minidom_example2.xml", "w") as f:
  22.         f.write(xml_str)
minidom_example.xml

  1. <?xml version="1.0" ?>
  2. <root>
  3.   <leaf color="white">Text element with attributes</leaf>
  4.   <leaf_cdata>
  5. <![CDATA[<em>CData</em> can contain <strong>HTML tags</strong> without encoding]]>  </leaf_cdata>
  6.   <branch>
  7.     <leaf color="white">Text element with attributes</leaf>
  8.   </branch>
  9.   <mixed>
  10.     Do not use mixed elements if it possible.
  11.     <leaf number="9000">Text element with attributes</leaf>
  12.   </mixed>
  13. </root>

Если необходим простой XML, не нужно лезть в дебри неймспейсов и прочих XML-заморочек, то выгода очевидна: получилось удобнее, чем lxml и ElementTree, и мы остались в рамках Питона. К тому же, каждый может допилить функцию create_tag под свои нужды.