Python

индекс
250,37

Как я учился работать с XML

image
Честно говоря, я довольно сильно удивился, не найдя статьи по подобной теме на хабре. А тема-то довольно актуальная и нужная, поэтому возьму на себя смелость немного ее осветить.


Краткий экскурс


Работая с xml в питоне многие пользуются довольно удобным встроенным модулем xml.dom.minidom. Вся информация в нем, включая содержимое тегов, представляется как эдакие ноды, работа с которыми ведется напрямую. Вот кусок кода обработки xml-файла:
Copy Source | Copy HTML
  1. app_xml = xmlp.parse("base.xml")
  2. id = app_xml.createElement(att)
  3. node = app_xml.createTextNode("simple.App")
  4. id.appendChild(node)
  5. root.appendChild(id)
  6. res = open("base.xml""w")
  7. res.writelines(app_xml.toprettyxml())
  8. res.close()

Весь этот код, как несложно догадаться, открывает существующий xml-файл, парсит и добавляет к нему один-единственный тег id со строкой «simple.App». Громоздко? Да не то слово. Мало того, обнаружился еще один неприятный баг — со времен Python 2.4 функция toprettyxml(), предназначенная для выдачи содержимого ноды или дерева нод в текстовом виде, зачем-то добавляет к каждой строчке символ перевода каретки, в результате чего вместо
<id>simple.App</id>

выдается
<id>
   simple.App
</id>

На первый взгляд, это не критично, поскольку значение остается нетронутым, однако в некоторых случаях и для некоторых парсеров (если, например, собираетесь использовать сгенеренный XML в других разработках) — при обработке числовых данных будет выводиться ошибка. В частности, этим грешит сборщик Adobe AIR-приложений, биндинги к которому я, собственно, и писал.
Поиски в интернете дали результат. Получалось, что либо я должен использовать в своем коде хак в виде лишней функции строчек эдак на двадцать, либо использовать стандартный toxml(), который хоть и генерит валидные файлы — но всю инфу в них располагает в одну строчку, то есть весь мой дескриптор превратился в кашу вида
Copy Source | Copy HTML
  1. <?xml version='1.0' encoding='utf-8'?><application xmlns="http://ns.adobe.com/air/application/1.5"><id>simple.test.program</id><version>0.1</version><filename>testapp</filename><name>testapp</name><initialWindow><title>Test AIR Application</title><content>test.html</content><height>320</height><width>240</width><visible>true</visible><resizable>true</resizable></initialWindow></application>

Назовите меня эстетом, но при больших объемах файла (да еще и с комментариями) искать в подобной куче ошибочные значения — то еще удовольствие.
И тогда я стал искать альтернативный вариант.

Свет в конце тоннеля


И этот вариант пришел в виде модуля lxml. Он упоминался как раз в теме, в которой ругали xml.dom.minidom за творимое им безобразие :)
А теперь давайте взглянем на код генерации хэндла приложения без всяких добавлений и переписываний:
Copy Source | Copy HTML
  1. root = etree.Element("initialWindow")
  2. etree.SubElement(root, "title").text = title
  3. etree.SubElement(root, "content").text = content
  4. etree.SubElement(root, "height").text = str(height)
  5. etree.SubElement(root, "width").text = str(width)
  6. app_window = etree.tostring(root)
  7. ...
  8. root = etree.Element("application", xmlns="http://ns.adobe.com/air/application/1.5")
  9. etree.SubElement(root, "id").text = id
  10. etree.SubElement(root, "version").text = version
  11. etree.SubElement(root, "filename").text = filename
  12. etree.SubElement(root, "name").text = self.name
  13. root.append(etree.XML(app_window))
  14. handle = etree.tostring(root, pretty_print=True, encoding='utf-8', xml_declaration=True)
  15. applic = open(self.fullpath+"/"+self.name+"-app.xml", "w")
  16. applic.writelines(handle)
  17. applic.close()

Куда понятнее и нагляднее, не находите? Этот код позволяет сгенерировать вот такой чистенький и красивенький XML:
Copy Source | Copy HTML
  1. <?xml version='1.0' encoding='utf-8'?>
  2. <application xmlns="http://ns.adobe.com/air/application/1.5">
  3.   <id>simple.test.program</id>
  4.   <version>0.1</version>
  5.   <filename>testapp</filename>
  6.   <name>testapp</name>
  7.   <initialWindow>
  8.     <title>Test AIR Application</title>
  9.     <content>test.html</content>
  10.     <height>320</height>
  11.     <width>240</width>
  12.   </initialWindow>
  13. </application>

Думаю, 90% кода в объяснении не нуждаются. Вся уличная магия заключена в строчке handle = etree.tostring(root, pretty_print=True, encoding='utf-8', xml_declaration=True). Здесь pretty_print — замена того самого злосчастного toprettyxml(), только, в отличие от него, работающая нормально. Также мы задаем кодировку и приделываем стандартную строку заголовка XML-документа.

По утверждениям, этот модуль, как ни странно, работает раза в два быстрее, чем стандартный. Устанавливается он элементарно через setuptools:
$ sudo easy_install lxml

Нормальный туториал лежит на официальном сайте, вот прямая ссылка.

Да здравствует красивый, удобный и валидный код! Удачи вам всем, пишите комментарии.
+61
6 июня 2009, 21:36
111

комментарии (55)

+1
qmax #
да, elementree конечно вкуснее, чем стандартный dom api.

но скажите, зачем вот это?:
app_window = etree.tostring(root)

root.append(etree.XML(app_window))

0
qmax #
поглядел в мануал.
у _ElementInterface есть методы эмуляции списков, и есть метод .append(childelement)
+1
enchantner #
Это конкретно у меня в коде, ибо эти две части находятся в разных функциях, а передавать строку нагляднее и проще. Да и потом, я же тоже изучал новые возможности модуля, вот, собственно, такое простое и красивое преобразование — тоже хорошая вещь :)
+1
INIGroup #
Спасибо! вижу что есть смысл попробовать lxml.
Сам довольно долго промучался с minidom, особенно с проблемами кодировки, однако задача была решена.
Кстати, toprettyxml() является лишь оберткой toxml(), а у последнего есть еще параметры, задающие символы отступа строки (по умолчанию \t) и разделитель самих строк (\n). Однако опять-таки это не всегда может помочь.
0
Ockonal #
В избранное! =) Пока не надо, но уверен, что понадобится.
–2
qmax #
как можно жить в современном мире, не работая с xml?
0
Mezomish #
И уж тем более с Питоном? ;)
+1
Tagire #
Много вещей можно получать в json.
0
qmax #
Много вещей пишутся не для веба.

Хотя, конечно, на JS можно, в принципе, писать и консольные/десктопные/серверные приложения…
Никто не спорит :)
0
Tagire #
Json можно использовать во многих языках, ну и он является подмножеством Yaml.
0
neithere #
YAML, JSON.
а XML не для человеков.
+2
bobry #
для справки, имеется еще один довольно неплохой вариант для работы с XML
под названием Beatiful Soup
но он сильно проигрывает lxml по скорости
0
mrShadow #
Насколько я помню, он предназначен для парсинга HTML с незакрытыми тэгами и прочим говном. Поэтому медленный. И использовать его для XML никто в здравом уме не будет.
0
mrShadow #
Ну и не факт, что он является обёрткой над кодом на Си, как lxml. Отсюда тоже могут быть потери производительности (скорее всего так и есть).
0
bobry #
по моему там вообще без Си обошлось
и насчет предназначения, там и для XML есть парсер) так что видимо создатели считают что он для всего хорош
0
qmax #
а не эффективнее ль будет tidy-фицировать исходный html в xhtml и потом работать как с xml?
+1
mrShadow #
То есть парсить в два прохода? Не думаю, что получится эффективнее.

Если вы о том, чтобы исходный HTML tidy-фицировать заранее, то это не всегда возможно (например, если парсим чужой сайт). Надеюсь, понятно о чём я, не люблю допоздна сидеть, в голове всё путаться начинает.
0
qmax #
я не пользовал Soup, но подозреваю, что тиди-фикация в той или иной форме там внутри присутствует всёравно.
0
lizendir #
Я использовал. Если скорость не критична, то интерфейс BeautifulSoup достаточно удобен.
+5
drJonnie #
обнаружился еще один неприятный баг — со времен Python 2.4 функция toprettyxml() <…> зачем-то добавляет к каждой строчке символ перевода каретки.

Это не баг, она потому и называется pretty, что выводит отформатированный по-человечески xml. Однако ее можно заставить не делать индентацию и не вставлять символ строки: app_xml.toprettyxml(indent='', newl='').
0
enchantner #
Тогда главный вопрос — почему этого не сделали по умолчанию? Это играет роль только тогда, когда вы собираетесь читать потом xml какими-нибудь нестандартными средствами, например, регэкспами. А так — и вы нормально прочитаете, и любой парсер нормально распознает.
+2
drJonnie #
Все pretty-функции в питоне по умолчанию выводят читабельные строки. В вашем случае, лучше было использовать app_xml.toxml(), который бы выдал простую строку. Думаю, здесь надо просто запомнить, что pretty — это всегда для людей. Даже такой модуль есть pprint — от pretty print.
0
drJonnie #
Прошу прощения, хабр тормозил, поэтому ответил два раза.
0
drJonnie #
В питоне все функции, имеющие pretty в названии, предназначены для людей. Это что-то вроде общественного договора. Существует даже специальный модуль pprint (от pretty print), который позволяет по-человечески форматировать и выводить стандартные объекты питона.

Почти всегда существуют неpretty-функции. В вашем случае это app_xml.toxml(), которая выдаст просто обычную строку.
0
centur #
Эмм… а никто не задумывался, что тем самым функция ИЗМЕНЯЕТ содержимое xml node. Все-таки добавление в тело ноды доп. символов перевода каретки и символов табуляции\пробела — это некорректно.

Теги форматировать можно как угодно, но содержимое — нельзя. Потому как с точки зрения любого парсера вот эти 2 ноды:
Мама мыла раму
и
Мама мыла раму

разные, причем сильно. Т.е. ни хранить такой результат ни передавать для дальнейшей обрабтки нельзя -только показать человеку. И машине плевать что человек не видит whitespace символы и перевод каретки, она проводит анализ по своим правилам, «не-человеческим».

И правильно делают парсеры которые на это ругаются.
Я не читал документации по prettyprint, но если доки писались вменяемыми людьми — там должна быть оговорка крупными буквами — недопустимо применять результаты работы функции для какой-либо машинной обработки, только для вывода информации, потому что информация искажена.

Такое поведение (дописывание текста XML ноды), в моем понимании равносильно тому, что в строки базы данных при каждом селекте некая хрень дописывала бы теги форматирования html, мотивируя это тем, что человек то все равно их не увидит…
0
centur #
хабр съел тэги в комментариях — мама мыла раму в обоих случаях — в тегах < x >, в первом без переноса, во втором — с переносом ( или см пример с simple.App в статье)
0
1amer #
+1
Tishka17 #
Хотелось бы увидеть сравнение скорости парсинга xml с помощью различных библиотек.
0
enchantner #
Есть немного по этому поводу на самом сайте lxml: codespeak.net/lxml/dev/performance.html
И вот тут еще немного: www.onlamp.com/pub/wlg/6291?page=last&x-order=date
Преимущество ElementTree — подхода очевидно
+1
drJonnie #
Преимущество ElementTree — подхода очевидно.

Не соглашусь. В первой ссылке сравнивают ElementTree с lxml и данные там представлены количеством милисекунд на каждый проход, т.е. чем меньше, тем лучше. lxml в разы быстрее (~10) при сериализации и примерно в два раза медленее при парсинге, чем cElementTree. Без си-оптимизаций ElementTree вообще не стоит принимать во внимание. Во второй ссылке сравнивают cElementTree и sax. Sax медленне в ~5 раз.

В целом:
Если есть возможность установить libxml2, тогда лучше использовать lxml.
Если нет, стоит обратиться к cElementTree.
Sax можно использовать тогда, когда данных мало; еще он, по-моему, несколько удобнее в обращении.
0
enchantner #
Я про то, что lxml использует тот же подход, просто через сишные библиотеки :) А сам алгоритм получается заметно быстрее dom'а
0
kex #
Sax, в отличие от dom-подходов, не формирует никакой модели документа, это потоковый событийный парсер.
0
snapper #
sax нужен, когда данных много. много-много-много, сотни мегабайт — dom умирает в таких случаях.
0
drJonnie #
Ну, не умирает. Просто потребление памяти будет в 1.5-2 раза выше. В случае с lxml — где-то в 4, т.к. приходится приходится дублировать структуру документа libxml в объектах питона.
0
snapper #
возьмите машину с 4 gb памяти и отпарсите на ней файл размером гигабайта в 2 через dom. умереть не умрёт, конечно, но на жизнь это будет мало похоже.
0
drJonnie #
И часто у вас такое происходит?)
0
snapper #
Нет, потому как лечим, ищем обходные пути, выкидываем xml вообще и живём счастливо.
0
bobry #
blog.ianbicking.org/2008/03/30/python-html-parser-performance/
и опять lxml вышел победителем
правда тесту уже больше года
+3
drJonnie #
lxml является не чистым python-модулем, а интерфейсом к C-библиотекам libxml2 и libxslt. Поэтому он очень быстр, что особенно заметно на больших объемах данных.
0
shandor #
Вот из-за всех таких заморочек предпочитаю пользоваться атрибутами, а текстовыми нодами. Хотя в данном случае приходится работать с тем, что есть.

> зачем-то добавляет к каждой строчке символ перевода каретки
Не существует pretty-стандарта xml, так что прямой вины описанного парсера тут нет.

В любом случае, используя pretty вы рискуете накосячить с итоговым xml.
0
mrShadow #
Да, с текстовыми нодами та ещё беда. Иногда манипуляция с ними вообще нелогична. Например, вот случай:

<a>текст1<b/>текст2</a>

Пусть элемент <a> хранится в объекте под именем a, а элемент <a> — в b. Тогда текст1 попадёт в атрибут a.text, а текст2 — в b.tail.
0
mrShadow #
Опечатался.

элемент <b> — в b
0
bobermaniac #
Так, насколько я понимаю, тут вы вообще стандарт нарушаете.

Правилами XML допустима вложенность типа 1<b />2, но никак не some<b />text.

А невалидный xml обрабатывается черте как.
0
bobermaniac #
Парсер — лох.

<a><b>111</b><b /></a>

И, соответственно

<a>some<b />text</a>
0
mrShadow #
Почему это невалидный? Возьмите хоть XHTML для примера: Привет !
0
mrShadow #
Действительно, парсер лох.

<p>Привет <img src=«smile.gif» />! </p>
+1
mocksoul #
Это допускается, т.к. в текст (с точки зрения libxml и xml в целом) — это та же нода, только текстовая. В libxml чистом было бы p.childNodes() == Node[b: TextNode[Привет ], Node[img], TextNode[! ]]. Соответственно «хвост» — это следующий текстовый атрибут после текущего в lxml. Не вижу способа сделать это иным образом, посему lxml молодец и в этом случае.

А вообще с точки зрения xhtml/xml «внутренне» (для браузера) ваш пример аналогичен этому:
<p><span>Привет </span><img src="ass.bmp" /><span>!</span></p>


Вот только парсить его один фиг сложнее чем через text/tail.
0
mrShadow #
Вы правы, действительно иных способов как-то не придумаешь… Наверное мне просто неочевидным когда-то показался факт, что текст надо доставать через предыдущий элемент.
+1
mrShadow #
Ещё во всех питоновских библиотеках для XML, которые я встречал (lxml тоже) не очень приятно работать с XML namespaces.

Если входной XML весь лежит в каком-то безымянном пространстве имён (к примеру, обычный XHTML), то во всех селекторах в коде придётся указывать URI этого пространства имён. Иными словами, плохо, что нет возможности задать дефолтный URI.

Когда-то при генерации XHTML я препопочёл использовать хак и просто задать атрибут xmlns :-)

html = etree.Element('html')
# HACK. Use this because lxml library lacks functions to set default
# XML namespace without inserting namespace URI's into each XML element
# name. For example:
#
# * element.findall('{%s}something' % namespace)
#
# * element.xpath('prefix:something', prefixMap)
#
# Also it seems imposible to use element names that does not have prefix,
# but have namespace (i.e., non-default anonymous namespace) in xpath().
# The xpath() method complains when prefixMap has None key and does not
# consider prefixMap[''] when it is specified… We require anonymous
# namespace in order not to get too verbose output
#
html.set('xmlns', 'http://www.w3.org/1999/xhtml')
head = etree.SubElement(html, 'head')
0
lizendir #
В BeautifulSoup можно работать без NS, он их просто отсекает по умолчанию.
0
siberianlaika #
Угу, чертова валидность.
0
el777 #
Спасибо за новость — уже больше года использую эту либу для парсинга xml :)
Если серьезно, то либа довольно приятная, легко разбирать пропарсенное. Есть только одна серьезная проблема — память подтекает.
0
snapper #
lxml — обвязка над libxml2\libxslt, так что ничего странного в его скорости нет. Ещё не упомянут 4Suite, но поскольку он целиком написан на питоне, то работает на порядок медленее.

В lxml нельзя достучаться до чистого sax-интерфейса, даже при iterparse приходится вставать на голову и применять такие трюки, например, для экономии памяти — www.ibm.com/developerworks/xml/library/x-hiperfparse/

Но у lxml есть очень большое преимущество — активная разработка и обратная связь. Штефан (Stefan Behnel) всегда оперативно реагирует на просьбы о помощи и на feature requests. В общем, мы используем именно lxml и довольны настолько, насколько можно быть довольными при работе с xml.
0
archonoff #
А как прописать доктайп с помощью lxml?
Или просто потом открыть файл для записи и добавить туда строку с доктайпом?
0
siberianlaika #
Так у toprettyxml() в minidom есть параметр newl="\n". Можно заменить его чем хочется. Хотя с elementree/lxml работать всё одно поприятнее.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.