Как я учился работать с 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

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

    Да здравствует красивый, удобный и валидный код! Удачи вам всем, пишите комментарии.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 55
    • +1
      да, elementree конечно вкуснее, чем стандартный dom api.

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

      root.append(etree.XML(app_window))

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

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

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

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

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

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

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

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

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

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

                                    В целом:
                                    Если есть возможность установить libxml2, тогда лучше использовать lxml.
                                    Если нет, стоит обратиться к cElementTree.
                                    Sax можно использовать тогда, когда данных мало; еще он, по-моему, несколько удобнее в обращении.
                                    • 0
                                      Я про то, что lxml использует тот же подход, просто через сишные библиотеки :) А сам алгоритм получается заметно быстрее dom'а
                                      • 0
                                        Sax, в отличие от dom-подходов, не формирует никакой модели документа, это потоковый событийный парсер.
                                        • 0
                                          sax нужен, когда данных много. много-много-много, сотни мегабайт — dom умирает в таких случаях.
                                          • 0
                                            Ну, не умирает. Просто потребление памяти будет в 1.5-2 раза выше. В случае с lxml — где-то в 4, т.к. приходится приходится дублировать структуру документа libxml в объектах питона.
                                            • 0
                                              возьмите машину с 4 gb памяти и отпарсите на ней файл размером гигабайта в 2 через dom. умереть не умрёт, конечно, но на жизнь это будет мало похоже.
                                              • 0
                                                И часто у вас такое происходит?)
                                                • 0
                                                  Нет, потому как лечим, ищем обходные пути, выкидываем xml вообще и живём счастливо.
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                      • +3
                                        lxml является не чистым python-модулем, а интерфейсом к C-библиотекам libxml2 и libxslt. Поэтому он очень быстр, что особенно заметно на больших объемах данных.
                                        • 0
                                          Вот из-за всех таких заморочек предпочитаю пользоваться атрибутами, а текстовыми нодами. Хотя в данном случае приходится работать с тем, что есть.

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

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

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

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

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

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

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

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

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

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

                                                      <p>Привет <img src=«smile.gif» />! </p>
                                                      • +1
                                                        Это допускается, т.к. в текст (с точки зрения 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
                                                          Вы правы, действительно иных способов как-то не придумаешь… Наверное мне просто неочевидным когда-то показался факт, что текст надо доставать через предыдущий элемент.
                                              • +1
                                                Ещё во всех питоновских библиотеках для 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
                                                    Угу, чертова валидность.
                                                  • 0
                                                    Спасибо за новость — уже больше года использую эту либу для парсинга xml :)
                                                    Если серьезно, то либа довольно приятная, легко разбирать пропарсенное. Есть только одна серьезная проблема — память подтекает.
                                                    • 0
                                                      lxml — обвязка над libxml2\libxslt, так что ничего странного в его скорости нет. Ещё не упомянут 4Suite, но поскольку он целиком написан на питоне, то работает на порядок медленее.

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

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

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