Ранее мы рассматривали некоторые встроенные в Python XML парсеры. В этой статье, мы рассмотрим один интересный сторонний пакет lxml от codespeak. Он, в частности, использует API ElementTree. Пакет lxml имеет поддержку XPath и XSLT, включая API для SAX и API уровня С для совместимости с модулями C/Pyrex. В статье мы рассмотрим следующее:
- Парсинг XML используя lxml
- Пример рефакторинга
- Как выполнять парсинг XML с lxml.objectify
- Как создавать XML с lxml.objectify
В этой статье, мы используем примеры, основанные на примерах парсинга minidom, и посмотрим, как выполнять парсинг при помощи lxml Python. Вот пример XML из программы, которая была написана для отслеживания назначений:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" ?> <zAppointments reminder="15"> <appointment> <begin>1181251680</begin> <uid>040000008200E000</uid> <alarmTime>1181572063</alarmTime> <state></state> <location></location> <duration>1800</duration> <subject>Bring pizza home</subject> </appointment> <appointment> <begin>1234360800</begin> <duration>1800</duration> <subject>Check MS Office website for updates</subject> <location></location> <uid>604f4792-eb89-478b-a14f-dd34d3cc6c21-1234360800</uid> <state>dismissed</state> </appointment> </zAppointments> |
Давайте посмотрим, как происходит парсинг с использованием lxml.
Парсинг XML с lxml
Данный пример XML показывает два назначения. Время начинается спустя секунды после эпохи. Наш uid сгенерирован на основе хеша начала времени и ключа. Время сигнала – несколько секунд после эпохи, но не раньше начала времени. Состояние – если назначение было отменено или перенесено, или нет, так или иначе. Остальная часть XML, как мы видим, в пояснении не нуждается. Давайте взглянем на то, как делается парсинг:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# -*- coding: utf-8 -*- from lxml import etree def parseXML(xmlFile): """ Парсинг XML """ with open(xmlFile) as fobj: xml = fobj.read() root = etree.fromstring(xml) for appt in root.getchildren(): for elem in appt.getchildren(): if not elem.text: text = "None" else: text = elem.text print(elem.tag + " => " + text) if __name__ == "__main__": parseXML("example.xml") |
Во первых, мы импортировали необходимые модули, а именно модуль etree из пакета lxml Python и функцию StringIO из встроенного модуля StringIO. Наша функция parseXML принимает один аргумент: путь к рассматриваемому файлу XML. Мы открываем файл, читаем и закрываем его. Теперь начинается самое веселое. Мы используем функцию парсинга etree, чтобы парсировать код XML, который вернулся из модуля StringIO. По причинам, которые я не могу полностью понять, функция парсинга требует файловый объект. В любом случае, мы итерируем контекст (другими словами, объект lxml.etree.iterparse) и извлекаем теговые элементы. Далее, мы добавляем условный оператор if, для замещения пустых полей словом “None” для получения более чистой выдачи.
Парсинг на примере книги
Что-ж, результат нашего примера немного скучный. Большую часть времени, вам нужно будет сохранить извлеченные данные, и сделать с ними что-нибудь, а не просто вывести его в stdout. Так что в следующем нашем примере мы создадим структуру данных для сбора результатов. В данном примере структура наших данных будет представлять собой список словарей. Мы используем пример книги MSDN. Сохраните следующий код XML под названием example.xml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?xml version="1.0"?> <catalog> <book id="bk101"> <author>Gambardella, Matthew</author> <title>XML Developer's Guide</title> <genre>Computer</genre> <price>44.95</price> <publish_date>2000-10-01</publish_date> <description>An in-depth look at creating applications with XML.</description> </book> <book id="bk102"> <author>Ralls, Kim</author> <title>Midnight Rain</title> <genre>Fantasy</genre> <price>5.95</price> <publish_date>2000-12-16</publish_date> <description>A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world.</description> </book> <book id="bk103"> <author>Corets, Eva</author> <title>Maeve Ascendant</title> <genre>Fantasy</genre> <price>5.95</price> <publish_date>2000-11-17</publish_date> <description>After the collapse of a nanotechnology society in England, the young survivors lay the foundation for a new society.</description> </book> </catalog> |
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Теперь мы выполним парсинг данного XML и вставим его в нашу структуру данных!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# -*- coding: utf-8 -*- from lxml import etree def parseBookXML(xmlFile): with open(xmlFile) as fobj: xml = fobj.read() root = etree.fromstring(xml) book_dict = {} books = [] for book in root.getchildren(): for elem in book.getchildren(): if not elem.text: text = "None" else: text = elem.text print(elem.tag + " => " + text) book_dict[elem.tag] = text if book.tag == "book": books.append(book_dict) book_dict = {} return books if __name__ == "__main__": parseBookXML("books.xml") |
Данный пример весьма похож на предыдущий, так что мы сосредоточимся только на различиях между ними. Перед началом итерации над контекстом, мы создадим объект пустого словаря и пустой список Python. Далее, в цикле, мы создадим наш словарь вот так:
1 |
book_dict[elem.tag] = text |
Текст может быть как elem.text так и None. Наконец, если тег окажется книгой, тогда мы в конце книжной секции, и нам нужно добавить словарь в наш список, а также сбросить словарь для следующей книги. Как мы видим, это именно то, что мы сделали. Более реалистичным примером будет размещение извлеченных данных в Python класс Book. Ранее я делал последнее с json feeds. Теперь мы готовы к тому, чтобы приступить к парсингу XML с lxml.objectify!
Парсинг XML с lxml.objectify
Модуль lxml содержит модуль, под названием objectify, который превращает документы XML в объекты Python. Для меня данная возможность оказалась весьма кстати во время работы, надеюсь, вам она тоже пригодиться. Вам может понадобиться немного помучаться при его установке, так как pip не работает с lxml в Windows. Обязательно перейдите в Индекс пакетов Python (https://pypi.python.org/pypi) и поищите версию, которая была разработана под вашу версию Python.
Установка lxml: http://lxml.de/installation.html
В любом случае, после установки, мы можем начать исследовать это чудесный кусок XML снова:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" ?> <zAppointments reminder="15"> <appointment> <begin>1181251680</begin> <uid>040000008200E000</uid> <alarmTime>1181572063</alarmTime> <state></state> <location></location> <duration>1800</duration> <subject>Bring pizza home</subject> </appointment> <appointment> <begin>1234360800</begin> <duration>1800</duration> <subject>Check MS Office website for updates</subject> <location></location> <uid>604f4792-eb89-478b-a14f-dd34d3cc6c21-1234360800</uid> <state>dismissed</state> </appointment> </zAppointments> |
Теперь нам нужно написать код, который может выполнять парсинг и модифицировать XML. Давайте взглянем на это небольшое демо, которое показывает кучу чудесных возможностей, которые нам дает objectify.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# -*- coding: utf-8 -*- from lxml import etree, objectify def parseXML(xmlFile): """Parse the XML file""" with open(xmlFile) as f: xml = f.read() root = objectify.fromstring(xml) # возвращаем атрибуты как словарь. attrib = root.attrib # извлекаем данные данные. begin = root.appointment.begin uid = root.appointment.uid # в цикле выводим всю информацию про элементы (тэги и текст). for appt in root.getchildren(): for e in appt.getchildren(): print("%s => %s" % (e.tag, e.text)) print() # пример как менять текст внутри элемента. root.appointment.begin = "something else" print(root.appointment.begin) # добавление нового элемента. root.appointment.new_element = "new data" # удаляем аннотации. objectify.deannotate(root) etree.cleanup_namespaces(root) obj_xml = etree.tostring(root, pretty_print=True) print(obj_xml) # сохраняем данные в файл. with open("new.xml", "w") as f: f.write(obj_xml) if __name__ == "__main__": f = r'path\to\sample.xml' parseXML(f) |
Данный код детально прокомментирован, но мы потратим немного времени на том, чтобы исключить все недопонимания. В начале, мы передали наш пример файла XML и использовали objectify. Если вам нужно получить доступ к атрибутам тега, используйте свойство attrib. Оно вернет словарь атрибутов тега. Для доступа к под-теговым элементам, вам нужно использовать точечную нотацию. Как вы видите, для того, чтобы попасть к началу значения тега, мы можем просто сделать что-то на подобии этого:
1 |
begin = root.appointment.begin |
Есть один момент, которого стоит опасаться, это когда значение имеет ведущие нули, так что они могут быть усечены после возврата значения. Если это имеет значение для вас, тогда вам нужно использовать следующий синтаксис:
1 |
begin = root.appointment.begin.text |
Если вам нужно выполнить итерацию над дочерними элементами, вы можете использовать метод iterchildren. Возможно, вам придется использовать вложенную структуру цикла for, чтобы получить все необходимое. Изменение значения элемента это также просто, как присвоение ему нового значения.
1 |
root.appointment.new_element = "new data" |
Теперь мы готовы к тому, чтобы перейти к созданию XML при помощи lxml.objectify.
Создание XML при помощи lxml.objectify
Субпакет lxml.objectify очень удобный инструмент для парсинга и создания XML. В данном разделе мы рассмотрим, как создавать XML при помощи модуля lxml.objectify. Мы начнем с примера простого XML кода и попробуем копировать его. Приступим!
Мы будем использовать следующий XML в качестве нашего примера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" ?> <zAppointments reminder="15"> <appointment> <begin>1181251680</begin> <uid>040000008200E000</uid> <alarmTime>1181572063</alarmTime> <state></state> <location></location> <duration>1800</duration> <subject>Bring pizza home</subject> </appointment> <appointment> <begin>1234360800</begin> <duration>1800</duration> <subject>Check MS Office website for updates</subject> <location></location> <uid>604f4792-eb89-478b-a14f-dd34d3cc6c21-1234360800</uid> <state>dismissed</state> </appointment> </zAppointments> |
Давайте посмотрим, как мы будем использовать lxml.objectify для воссоздания XML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# -*- coding: utf-8 -*- from lxml import etree, objectify def create_appt(data): """ Создаем изначальную структуру XML. """ appt = objectify.Element("appointment") appt.begin = data["begin"] appt.uid = data["uid"] appt.alarmTime = data["alarmTime"] appt.state = data["state"] appt.location = data["location"] appt.duration = data["duration"] appt.subject = data["subject"] return appt def create_xml(): """ Создаем XML файл. """ xml = '''<?xml version="1.0" encoding="UTF-8"?> <zAppointments> </zAppointments> ''' root = objectify.fromstring(xml) root.set("reminder", "15") appt = create_appt({"begin":1181251680, "uid":"040000008200E000", "alarmTime":1181572063, "state":"", "location":"", "duration":1800, "subject":"Bring pizza home"} ) root.append(appt) uid = "604f4792-eb89-478b-a14f-dd34d3cc6c21-1234360800" appt = create_appt({"begin":1234360800, "uid":uid, "alarmTime":1181572063, "state":"dismissed", "location":"", "duration":1800, "subject":"Check MS Office website for updates"} ) root.append(appt) # удаляем все lxml аннотации. objectify.deannotate(root) etree.cleanup_namespaces(root) # конвертируем все в привычную нам xml структуру. obj_xml = etree.tostring(root, pretty_print=True, xml_declaration=True ) try: with open("example.xml", "wb") as xml_writer: xml_writer.write(obj_xml) except IOError: pass if __name__ == "__main__": create_xml() |
Давайте немного разъясним код. Мы начнем с функции create_xml. В ней мы создаем корневой объект XML при помощи функции fromstring модуля objectify. Корневой объект будет содержать zAppointment в качестве его тега. Мы настроим корневой атрибут напоминания, после чего вызовем нашу функцию create_appt, используя словарь Python для её аргумента. В функции create_appt мы создаем экземпляр Element (технически, ObjectifiedElement), который мы назначаем нашей переменной apt. Здесь мы используем точечную нотацию для создания тегов для данного элемента. Наконец, мы возвращаем элемент appt назад и присоединяем его к нашему объекту root. Мы повторяем процесс для второго экземпляра назначения. Следующая секция функции create_xml удалит аннотацию lxml. Если этого не сделать, ваш XML будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" ?> <zAppointments py:pytype="TREE" reminder="15"> <appointment py:pytype="TREE"> <begin py:pytype="int">1181251680</begin> <uid py:pytype="str">040000008200E000</uid> <alarmTime py:pytype="int">1181572063</alarmTime> <state py:pytype="str"/> <location py:pytype="str"/> <duration py:pytype="int">1800</duration> <subject py:pytype="str">Bring pizza home</subject> </appointment><appointment py:pytype="TREE"> <begin py:pytype="int">1234360800</begin> <uid py:pytype="str">604f4792-eb89-478b-a14f-dd34d3cc6c21-1234360800</uid> <alarmTime py:pytype="int">1181572063</alarmTime> <state py:pytype="str">dismissed</state> <location py:pytype="str"/> <duration py:pytype="int">1800</duration> <subject py:pytype="str">Check MS Office website for updates</subject> </appointment> </zAppointments> |
Для удаления всех ненужных аннотаций, мы можем вызвать две следующие функции:
- objectify.deannotate(root)
- etree.cleanup_namespaces(root)
Последняя часть головоломки заключается в том, чтобы позволить lxml генерировать XML самостоятельно. Для этого мы используем модуль lxml etree, чтобы выполнить эту непростую задачу.
1 2 3 4 5 |
obj_xml = etree.tostring( root, pretty_print=True, xml_declaration=True ) |
Функция tostring возвращает опрятную строку XML, и если вы установите pretty_print на True, это также вернет XML в приемлемом формате. Аргумент ключа xml_declaration указывает модулю etree включать или убирать первую строку декларации (другими словами, ).
Подведем итоги
Теперь вы знаете, как использовать модуль lxml etree и модули objectify для парсинга XML. Вы также научились использовать objectify для создания XML. Навыки использования более одного модуля для решения одной задачи может быть очень полезным, если нужно взглянуть на проблему под разными углами. Также это позволит вам понять, какой нужно выбрать инструмент для решения проблемы.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»