You are currently viewing Дорожная карта для синтаксических анализаторов XML на Python

Дорожная карта для синтаксических анализаторов XML на Python

Содержание

Если вы когда-либо раньше пытались проанализировать XML-документ на Python, то вы знаете, насколько удивительно сложной может быть такая задача. С одной стороны, дзен Python обещает только один очевидный способ достижения вашей цели. В то же время стандартная библиотека следует девизу «Батарейки в комплекте», позволяя вам выбирать не из одного, а из нескольких синтаксических анализаторов XML. К счастью, сообщество Python решило эту проблему избытка, создав еще больше библиотек синтаксического анализа XML.

Шутки в сторону, все синтаксические анализаторы XML имеют свое место в мире, полном меньших или больших проблем. Стоит ознакомиться с доступными инструментами.

В этом уроке вы узнаете, как:

  • Выберите правильную модель синтаксического анализа XML
  • Используйте синтаксические анализаторы XML в стандартной библиотеке
  • Используйте основные библиотеки синтаксического анализа XML
  • Декларативно анализировать XML — документы с использованием привязки данных
  • Используйте безопасные анализаторы XML для устранения уязвимостей в системе безопасности

Вы можете использовать этот учебник в качестве дорожной карты, которая проведет вас через запутанный мир синтаксических анализаторов XML на Python. К концу его вы сможете выбрать правильный синтаксический анализатор XML для данной проблемы. Чтобы извлечь максимальную пользу из этого урока, вы уже должны быть знакомы с XML и его строительными блоками, а также с тем, как работать с файлами на Python.

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

Оказывается, вы можете обрабатывать XML-документы, используя несколько стратегий, не зависящих от языка. Каждый из них демонстрирует различные компромиссы между памятью и скоростью, что может частично оправдать широкий спектр анализаторов XML, доступных в Python. В следующем разделе вы узнаете об их различиях и сильных сторонах.

Объектная модель документа (DOM)

Исторически сложилось так, что первой и наиболее распространенной моделью для анализа XML была DOM, или Объектная модель документа, первоначально определенная Консорциумом World Wide Web (W3C). Возможно, вы уже слышали о DOM, потому что веб-браузеры предоставляют интерфейс DOM через JavaScript, позволяющий вам манипулировать HTML-кодом ваших веб-сайтов. И XML, и HTML принадлежат к одному семейству языков разметки, что делает возможным анализ XML с помощью DOM.

DOM, возможно, является самой простой и универсальной моделью для использования. Он определяет несколько стандартных операций для обхода и изменения элементов документа, расположенных в иерархии объектов. Абстрактное представление всего дерева документов хранится в памяти, предоставляя вам произвольный доступ к отдельным элементам.

В то время как дерево DOM обеспечивает быструю и всенаправленную навигацию, построение его абстрактного представления в первую очередь может занять много времени. Более того, XML анализируется сразу, в целом, поэтому он должен быть достаточно маленьким, чтобы вместить доступную память. Это делает DOM подходящим только для файлов конфигурации среднего размера, а не для многогигабайтных XML-баз данных.

Используйте синтаксический анализатор DOM, когда удобство важнее времени обработки и когда память не является проблемой. Некоторые типичные случаи использования-это когда вам нужно проанализировать относительно небольшой документ или когда вам нужно выполнять анализ нечасто.

Простой API для XML (SAX)

Чтобы устранить недостатки DOM, сообщество Java совместными усилиями создало библиотеку, которая затем стала альтернативной моделью для анализа XML на других языках. Не было никакой официальной спецификации, только органические обсуждения в списке рассылки. Конечным результатом стал потоковый API на основе событий, который последовательно работает с отдельными элементами, а не со всем деревом.

Элементы обрабатываются сверху вниз в том же порядке, в каком они отображаются в документе. Анализатор запускает определяемые пользователем обратные вызовы для обработки определенных узлов XML по мере их нахождения в документе. Этот подход известен как “принудительный” синтаксический анализ, потому что элементы передаются в ваши функции анализатором.

SAX также позволяет отбрасывать элементы, если они вас не интересуют. Это означает, что он занимает гораздо меньше памяти, чем DOM, и может работать со сколь угодно большими файлами, что отлично подходит для однопроходной обработки, такой как индексирование, преобразование в другие форматы и так далее.

Однако поиск или изменение узлов случайного дерева является громоздким, поскольку обычно требуется несколько проходов по документу и отслеживание посещенных узлов. SAX также неудобен для обработки глубоко вложенных элементов. Наконец, модель SAX позволяет выполнять синтаксический анализ только для чтения.

Короче говоря, SAX дешев с точки зрения пространства и времени, но в большинстве случаев сложнее в использовании, чем DOM. Он хорошо работает для анализа очень больших документов или анализа входящих XML-данных в режиме реального времени.

Потоковый API для XML (StAX)

Хотя этот третий подход к анализу XML несколько менее популярен в Python, он основан на SAX. Он расширяет идею потоковой передачи, но вместо этого использует модель синтаксического анализа “вытягивания”, что дает вам больше контроля. Вы можете рассматривать StAX как итератор, продвигающий объект курсора через XML-документ, где пользовательские обработчики вызывают анализатор по требованию, а не наоборот.

Примечание. Можно объединить несколько моделей синтаксического анализа XML. Например, вы можете использовать SAX или StAX, чтобы быстро найти интересный фрагмент данных в документе, а затем построить представление DOM только для этой конкретной ветви в памяти.

Использование StAX дает вам больший контроль над процессом синтаксического анализа и позволяет более удобно управлять состоянием. События в потоке используются только по запросу, что позволяет выполнять отложенную оценку. Кроме того, его производительность должна быть на уровне SAX, в зависимости от реализации синтаксического анализатора.

Узнайте о синтаксических анализаторах XML в стандартной библиотеке Python

В этом разделе вы познакомитесь со встроенными синтаксическими анализаторами XML Python, которые доступны вам почти в каждом дистрибутиве Python. Вы собираетесь сравнить эти анализаторы с образцом изображения масштабируемой векторной графики (SVG), которое представляет собой формат на основе XML. Обрабатывая один и тот же документ с помощью разных анализаторов, вы сможете выбрать тот, который подходит вам лучше всего.

Пример изображения, которое вы собираетесь сохранить в локальном файле для справки, изображает смайлик. Он состоит из следующего XML-содержимого:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
    <!ENTITY custom_entity "Hello">
]>
<svg xmlns="http://www.w3.org/2000/svg"
  xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
  viewBox="-105 -100 210 270" width="210" height="270">
  <inkscape:custom x="42" inkscape:z="555">Some value</inkscape:custom>
  <defs>
    <linearGradient id="skin" x1="0" x2="0" y1="0" y2="1">
      <stop offset="0%" stop-color="yellow" stop-opacity="1.0"/>
      <stop offset="75%" stop-color="gold" stop-opacity="1.0"/>
      <stop offset="100%" stop-color="orange" stop-opacity="1"/>
    </linearGradient>
  </defs>
  <g id="smiley" inkscape:groupmode="layer" inkscape:label="Smiley">
    <!-- Head -->
    <circle cx="0" cy="0" r="50"
      fill="url(#skin)" stroke="orange" stroke-width="2"/>
    <!-- Eyes -->
    <ellipse cx="-20" cy="-10" rx="6" ry="8" fill="black" stroke="none"/>
    <ellipse cx="20" cy="-10" rx="6" ry="8" fill="black" stroke="none"/>
    <!-- Mouth -->
    <path d="M-20 20 A25 25 0 0 0 20 20"
      fill="white" stroke="black" stroke-width="3"/>
  </g>
  <text x="-40" y="75">&custom_entity; &lt;svg&gt;!</text>
  <script>
    <![CDATA[
      console.log("CDATA disables XML parsing: <svg>")
      const smiley = document.getElementById("smiley")
      const eyes = document.querySelectorAll("ellipse")
      const setRadius = r => e => eyes.forEach(x => x.setAttribute("ry", r))
      smiley.addEventListener("mouseenter", setRadius(2))
      smiley.addEventListener("mouseleave", setRadius(8))
    ]]>
  </script>
</svg>

Он начинается с объявления XML, за которым следует Определение типа документа (DTD) и <svg> корневой элемент. DTD является необязательным, но он может помочь проверить структуру документа, если вы решите использовать XML-валидатор. Корневой элемент определяет пространство имен по умолчанию xmlns, а также пространство имен с префиксами xmlns:inkscape для элементов и атрибутов, специфичных для редактора. В документе также содержится:

  • Вложенные элементы
  • Атрибуты
  • Комментарии
  • Символьные данные (CDATA)
  • Предопределенные и пользовательские сущности

Продолжайте, сохраните XML-файл в файле с именем smiley.svgи откройте его с помощью современного веб-браузера, который запустит фрагмент JavaScript, присутствующий в конце:

Код добавляет интерактивный компонент к изображению. Когда вы наводите курсор мыши на смайлик, он моргает глазами. Если вы хотите отредактировать смайлик с помощью удобного графического интерфейса пользователя (GUI), вы можете открыть файл с помощью редактора векторной графики, такого как Adobe Illustrator или Inkscape.

Примечание: В отличие от JSON или YAML, некоторые функции XML могут быть использованы хакерами. Стандартные синтаксические анализаторы XML, доступные в xml пакете на Python, небезопасны и уязвимы для множества атак. Чтобы безопасно анализировать XML-документы из ненадежного источника, отдавайте предпочтение безопасным альтернативам. Вы можете перейти к последнему разделу этого руководства для получения более подробной информации.

Стоит отметить, что стандартная библиотека Python определяет абстрактные интерфейсы для анализа XML-документов, позволяя вам предоставлять конкретную реализацию синтаксического анализатора. На практике вы редко делаете это, потому что Python связывает привязку для библиотеки Expat, которая является широко используемым синтаксическим анализатором XML с открытым исходным кодом, написанным на C. Все следующие модули Python в стандартной библиотеке по умолчанию используют Expat под капотом.

К сожалению, хотя анализатор экспатов может сообщить вам, правильно ли сформирован ваш документ, он не может проверить структуру ваших документов на соответствие определению схемы XML (XSD) или определению типа документа (DTD). Для этого вам придется использовать одну из сторонних библиотек, которые будут рассмотрены позже.

xml.dom.minidom: Минимальная реализация DOM

Учитывая, что синтаксический анализ XML-документов с использованием DOM, возможно, является наиболее простым, вы не будете так удивлены, обнаружив анализатор DOM в стандартной библиотеке Python. Что удивительно, однако, так это то, что на самом деле существует два парсера DOM.

xml.domПакет содержит два модуля для работы с DOM на Python:

  1. xml.dom.minidom
  2. xml.dom.pulldom

Первый-это урезанная реализация интерфейса DOM, соответствующая относительно старой версии спецификации W3C. Он предоставляет общие объекты, определенные API DOM , такие как Document,Element, и Attr. Этот модуль плохо документирован и имеет довольно ограниченную полезность, как вы скоро узнаете.

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

В нем есть две функции, minidom которые позволяют анализировать XML-данные из различных источников данных. Один принимает либо имя файла, либо объект файла, в то время как другой ожидает строку Python:>>>

>>> from xml.dom.minidom import parse, parseString

>>> # Parse XML from a filename
>>> document = parse("smiley.svg")

>>> # Parse XML from a file object
>>> with open("smiley.svg") as file:
...     document = parse(file)
...

>>> # Parse XML from a Python string
>>> document = parseString("""\
... <svg viewBox="-105 -100 210 270">
...   <!-- More content goes here... -->
... </svg>
... """)

Строка в тройных кавычках помогает встроить многострочный строковый литерал без использования символа продолжения (\) в конце каждой строки. В любом случае, вы получите Document экземпляр, который демонстрирует знакомый интерфейс DOM, позволяющий вам перемещаться по дереву.

Кроме того, вы сможете получить доступ к XML-объявлению, DTD и корневому элементу:>>>

>>> document = parse("smiley.svg")

>>> # XML Declaration
>>> document.version, document.encoding, document.standalone
('1.0', 'UTF-8', False)

>>> # Document Type Definition (DTD)
>>> dtd = document.doctype
>>> dtd.entities["custom_entity"].childNodes
[<DOM Text node "'Hello'">]

>>> # Document Root
>>> document.documentElement
<DOM Element: svg at 0x7fc78c62d790>

Как вы можете видеть, даже несмотря на то, что синтаксический анализатор XML по умолчанию в Python не может проверять документы , он все равно позволяет проверять .doctype DTD, если он присутствует. Обратите внимание, что объявление XML и DTD являются необязательными. Если объявление XML или данный атрибут XML отсутствуют, то будут соответствующие атрибуты Python None.

Чтобы найти элемент по идентификатору, необходимо использовать Document экземпляр, а не конкретный родительский Element элемент . В образце SVG-изображения есть два узла с id атрибутом, но вы не можете найти ни один из них:>>>

>>> document.getElementById("skin") is None
True
>>> document.getElementById("smiley") is None
True

Это может быть удивительно для тех, кто работал только с HTML и JavaScript, но раньше не работал с XML. В то время как HTML определяет семантику для определенных элементов и атрибутов , таких как <body> или id, XML не придает никакого значения своим строительным блокам. Вам нужно пометить атрибут как идентификатор явно с помощью DTD или путем вызова .setIdAttribute() Python, например:

Стиль определенияРеализация
DTD<!--ATTLIST linearGradient id ID #IMPLIED-->
ПитонlinearGradient.setIdAttribute("id")

Однако использования DTD недостаточно для устранения проблемы, если в вашем документе есть пространство имен по умолчанию, как в случае с образцом SVG-изображения. Чтобы решить эту проблему, вы можете рекурсивно посетить все элементы в Python, проверить, есть ли у них id атрибут, и указать его в качестве идентификатора на одном дыхании:>>>

>>> from xml.dom.minidom import parse, Node

>>> def set_id_attribute(parent, attribute_name="id"):
...     if parent.nodeType == Node.ELEMENT_NODE:
...         if parent.hasAttribute(attribute_name):
...             parent.setIdAttribute(attribute_name)
...     for child in parent.childNodes:
...         set_id_attribute(child, attribute_name)
...
>>> document = parse("smiley.svg")
>>> set_id_attribute(document)

Ваша пользовательская set_id_attribute() функция принимает родительский элемент и необязательное имя для атрибута identity, значение которого по умолчанию "id" равно . Когда вы вызываете эту функцию в своем документе SVG, все дочерние элементы, имеющие id атрибут, становятся доступными через API DOM:>>>

>>> document.getElementById("skin")
<DOM Element: linearGradient at 0x7f82247703a0>

>>> document.getElementById("smiley")
<DOM Element: g at 0x7f8224770940>

Теперь вы получаете ожидаемый XML-элемент, соответствующий значению id атрибута.

Использование идентификатора позволяет найти не более одного уникального элемента, но вы также можете найти коллекцию похожих элементов по их имени тега. В отличие от .getElementById() метода, вы можете обратиться .getElementsByTagName() к документу или определенному родительскому элементу, чтобы уменьшить область поиска:>>>

>>> document.getElementsByTagName("ellipse")
[
    <DOM Element: ellipse at 0x7fa2c944f430>,
    <DOM Element: ellipse at 0x7fa2c944f4c0>
]

>>> root = document.documentElement
>>> root.getElementsByTagName("ellipse")
[
    <DOM Element: ellipse at 0x7fa2c944f430>,
    <DOM Element: ellipse at 0x7fa2c944f4c0>
]

Обратите внимание, что .getElementsByTagName() всегда возвращает список элементов вместо одного элемента или None. Забывание об этом при переключении между обоими методами является распространенным источником ошибок.

К сожалению, такие элементы <inkscape:custom> с префиксом идентификатора пространства имен не будут включены. Их необходимо искать с помощью .getElementsByTagNameNS(), который ожидает разные аргументы:>>>

>>> document.getElementsByTagNameNS(
...     "http://www.inkscape.org/namespaces/inkscape",
...     "custom"
... )
...
[<DOM Element: inkscape:custom at 0x7f97e3f2a3a0>]

>>> document.getElementsByTagNameNS("*", "custom")
[<DOM Element: inkscape:custom at 0x7f97e3f2a3a0>]

Первым аргументом должно быть пространство имен XML, которое обычно имеет форму доменного имени, в то время как вторым аргументом является имя тега. Обратите внимание, что префикс пространства имен не имеет значения! Для поиска во всех пространствах имен можно указать подстановочный знак (*).

Примечание. Чтобы найти пространства имен, объявленные в вашем XML-документе, вы можете проверить атрибуты корневого элемента. Теоретически они могут быть объявлены в любом элементе, но элемент верхнего уровня находится там, где вы обычно их находите.

Как только вы найдете интересующий вас элемент, вы можете использовать его для обхода дерева. Однако еще одна неприятная особенность minidom заключается в том, как он обрабатывает пробелы между элементами:>>>

>>> element = document.getElementById("smiley")

>>> element.parentNode
<DOM Element: svg at 0x7fc78c62d790>

>>> element.firstChild
<DOM Text node "'\n    '">

>>> element.lastChild
<DOM Text node "'\n  '">

>>> element.nextSibling
<DOM Text node "'\n  '">

>>> element.previousSibling
<DOM Text node "'\n  '">

Символы новой строки и начальный отступ записываются как отдельные элементы дерева, что и требуется в спецификации. Некоторые парсеры позволяют вам игнорировать их, но не Python. Однако вы можете вручную свернуть пробелы в таких узлах:>>>

>>> def remove_whitespace(node):
...     if node.nodeType == Node.TEXT_NODE:
...         if node.nodeValue.strip() == "":
...             node.nodeValue = ""
...     for child in node.childNodes:
...         remove_whitespace(child)
...
>>> document = parse("smiley.svg")
>>> set_id_attribute(document)
>>> remove_whitespace(document)
>>> document.normalize()

Обратите внимание, что вам также нужно в .normalize() документе объединить соседние текстовые узлы. В противном случае вы можете получить кучу избыточных XML-элементов с одним пробелом. Опять же, рекурсия-это единственный способ посетить элементы дерева, поскольку вы не можете выполнять итерацию по документу и его элементам с помощью цикла. Наконец, это должно дать вам ожидаемый результат:>>>

>>> element = document.getElementById("smiley")

>>> element.parentNode
<DOM Element: svg at 0x7fc78c62d790>

>>> element.firstChild
<DOM Comment node "' Head '">

>>> element.lastChild
<DOM Element: path at 0x7f8beea0f670>

>>> element.nextSibling
<DOM Element: text at 0x7f8beea0f700>

>>> element.previousSibling
<DOM Element: defs at 0x7f8beea0f160>

>>> element.childNodes
[
    <DOM Comment node "' Head '">,
    <DOM Element: circle at 0x7f8beea0f4c0>,
    <DOM Comment node "' Eyes '">,
    <DOM Element: ellipse at 0x7fa2c944f430>,
    <DOM Element: ellipse at 0x7fa2c944f4c0>,
    <DOM Comment node "' Mouth '">,
    <DOM Element: path at 0x7f8beea0f670>
]

Элементы предоставляют несколько полезных методов и свойств, позволяющих запрашивать их сведения:>>>

>>> element = document.getElementsByTagNameNS("*", "custom")[0]

>>> element.prefix
'inkscape'

>>> element.tagName
'inkscape:custom'

>>> element.attributes
<xml.dom.minidom.NamedNodeMap object at 0x7f6c9d83ba80>

>>> dict(element.attributes.items())
{'x': '42', 'inkscape:z': '555'}

>>> element.hasChildNodes()
True

>>> element.hasAttributes()
True

>>> element.hasAttribute("x")
True

>>> element.getAttribute("x")
'42'

>>> element.getAttributeNode("x")
<xml.dom.minidom.Attr object at 0x7f82244a05f0>

>>> element.getAttribute("missing-attribute")
''

Например, вы можете проверить пространство имен элемента, имя тега или атрибуты. Если вы запросите отсутствующий атрибут, то получите пустую строку ('').

Работа с атрибутами пространства имен не сильно отличается. Вам просто нужно не забыть соответствующим образом указать префикс имени атрибута или указать доменное имя:>>>

>>> element.hasAttribute("z")
False

>>> element.hasAttribute("inkscape:z")
True

>>> element.hasAttributeNS(
...     "http://www.inkscape.org/namespaces/inkscape",
...     "z"
... )
...
True

>>> element.hasAttributeNS("*", "z")
False

Как ни странно, подстановочный знак (*) здесь работает не так, как в .getElementsByTagNameNS() предыдущем методе.

Поскольку этот учебник посвящен только синтаксическому анализу XML, вам нужно будет проверить minidom документацию на наличие методов, изменяющих дерево DOM. Они в основном соответствуют спецификации W3C.

Как вы можете видеть, minidom модуль не очень удобен. Его главное преимущество заключается в том, что он является частью стандартной библиотеки, что означает, что вам не нужно устанавливать какие-либо внешние зависимости в вашем проекте для работы с DOM.

xml.sax: Интерфейс SAX для Python

Чтобы начать работать с SAX в Python, вы можете использовать те же parse() parseString() удобные функции, что и раньше, но xml.sax вместо этого из пакета. Вы также должны предоставить по крайней мере еще один обязательный аргумент, который должен быть экземпляром обработчика содержимого. В духе Java вы предоставляете его, создавая подкласс определенного базового класса:

from xml.sax import parse
from xml.sax.handler import ContentHandler

class SVGHandler(ContentHandler):
    pass

parse("smiley.svg", SVGHandler())

Обработчик содержимого получает поток событий, соответствующих элементам в вашем документе, по мере его анализа. Запуск этого кода пока не принесет ничего полезного, потому что ваш класс обработчика пуст. Чтобы это сработало, вам нужно будет перегрузить один или несколько методов обратного вызова из суперкласса.

Запустите свой любимый редактор, введите следующий код и сохраните его в файле с именем svg_handler.py:

# svg_handler.py

from xml.sax.handler import ContentHandler

class SVGHandler(ContentHandler):

    def startElement(self, name, attrs):
        print(f"BEGIN: <{name}>, {attrs.keys()}")

    def endElement(self, name):
        print(f"END: </{name}>")

    def characters(self, content):
        if content.strip() != "":
            print("CONTENT:", repr(content))

Этот обработчик измененного содержимого выводит несколько событий на стандартный вывод. Синтаксический анализатор SAX вызовет для вас эти три метода в ответ на поиск начального тега, конечного тега и некоторого текста между ними. Когда вы открываете интерактивный сеанс интерпретатора Python, импортируйте обработчик содержимого и проведите тест-драйв. Это должно привести к следующему результату:>>>

>>> from xml.sax import parse
>>> from svg_handler import SVGHandler
>>> parse("smiley.svg", SVGHandler())
BEGIN: <svg>, ['xmlns', 'xmlns:inkscape', 'viewBox', 'width', 'height']
BEGIN: <inkscape:custom>, ['x', 'inkscape:z']
CONTENT: 'Some value'
END: </inkscape:custom>
BEGIN: <defs>, []
BEGIN: <linearGradient>, ['id', 'x1', 'x2', 'y1', 'y2']
BEGIN: <stop>, ['offset', 'stop-color', 'stop-opacity']
END: </stop>
⋮

Это, по сути, шаблон проектирования наблюдателя, который позволяет постепенно переводить XML в другой иерархический формат. Допустим, вы хотите преобразовать этот SVG-файл в упрощенное представление JSON. Во-первых, вы захотите сохранить объект обработчика содержимого в отдельной переменной, чтобы позже извлечь из него информацию:>>>

>>> from xml.sax import parse
>>> from svg_handler import SVGHandler
>>> handler = SVGHandler()
>>> parse("smiley.svg", handler)

Поскольку синтаксический анализатор SAX выдает события без предоставления какого-либо контекста о найденном элементе, вам необходимо отслеживать, где вы находитесь в дереве. Поэтому имеет смысл поместить и вставить текущий элемент в стек, который вы можете смоделировать с помощью обычного списка Python. Вы также можете определить вспомогательное свойство.current_element, которое будет возвращать последний элемент, помещенный в верхнюю часть стека:

# svg_handler.py

# ...

class SVGHandler(ContentHandler):

    def __init__(self):
        super().__init__()
        self.element_stack = []

    @property
    def current_element(self):
        return self.element_stack[-1]

    # ...

Когда анализатор SAX находит новый элемент, вы можете сразу же записать его имя тега и атрибуты, одновременно создавая заполнители для дочерних элементов и значения, оба из которых являются необязательными. На данный момент вы можете хранить каждый элемент как dict объект. Замените существующий .startElement() метод новой реализацией:

# svg_handler.py

# ...

class SVGHandler(ContentHandler):

    # ...

    def startElement(self, name, attrs):
        self.element_stack.append({
            "name": name,
            "attributes": dict(attrs),
            "children": [],
            "value": ""
        })

Синтаксический анализатор SAX предоставляет вам атрибуты в виде сопоставления, которые вы можете преобразовать в обычный словарь Python с помощью вызова dict() функции. Значение элемента часто распределяется по нескольким фрагментам, которые можно объединить с помощью оператора plus (+) или соответствующего расширенного оператора присваивания:

# svg_handler.py

# ...

class SVGHandler(ContentHandler):

    # ...

    def characters(self, content):
        self.current_element["value"] += content

Агрегирование текста таким образом гарантирует, что многострочный контент окажется в текущем элементе. Например, <script> тег в образце файла SVG содержит шесть строк кода JavaScript, которые запускают отдельные вызовы characters() обратного вызова.

Наконец, как только анализатор наткнется на закрывающий тег, вы можете извлечь текущий элемент из стека и добавить его к дочерним элементам его родителя. Если остался только один элемент, то это будет корень вашего документа, который вы должны сохранить на потом. Кроме этого, вы можете захотеть очистить текущий элемент, удалив ключи с пустыми значениями:

# svg_handler.py

# ...

class SVGHandler(ContentHandler):

    # ...

    def endElement(self, name):
        clean(self.current_element)
        if len(self.element_stack) > 1:
            child = self.element_stack.pop()
            self.current_element["children"].append(child)

def clean(element):
    element["value"] = element["value"].strip()
    for key in ("attributes", "children", "value"):
        if not element[key]:
            del element[key]

Обратите внимание, что clean() это функция, определенная вне тела класса. Очистка должна быть выполнена в конце, так как невозможно заранее узнать, сколько фрагментов текста может быть объединено. Вы можете развернуть складной раздел ниже, чтобы получить полный код обработчика содержимого.

Обработчик саксофона для конвертера SVG в JSONПоказать/Скрыть

Теперь пришло время проверить все на практике, проанализировав XML, извлекив корневой элемент из обработчика содержимого и сбросив его в строку JSON:>>>

>>> from xml.sax import parse
>>> from svg_handler import SVGHandler
>>> handler = SVGHandler()
>>> parse("smiley.svg", handler)
>>> root = handler.current_element

>>> import json
>>> print(json.dumps(root, indent=4))
{
    "name": "svg",
    "attributes": {
        "xmlns": "http://www.w3.org/2000/svg",
        "xmlns:inkscape": "http://www.inkscape.org/namespaces/inkscape",
        "viewBox": "-105 -100 210 270",
        "width": "210",
        "height": "270"
    },
    "children": [
        {
            "name": "inkscape:custom",
            "attributes": {
                "x": "42",
                "inkscape:z": "555"
            },
            "value": "Some value"
        },
⋮

Стоит отметить, что эта реализация не увеличивает объем памяти по сравнению с DOM, поскольку она, как и раньше, создает абстрактное представление всего документа. Разница в том, что вы создали пользовательское представление словаря вместо стандартного дерева DOM. Однако вы можете представить себе запись непосредственно в файл или базу данных вместо памяти при получении событий SAX. Это эффективно повысит лимит памяти вашего компьютера.

Если вы хотите анализировать пространства имен XML, вам нужно будет самостоятельно создать и настроить синтаксический анализатор SAX с помощью стандартного кода, а также реализовать несколько иные обратные вызовы:

# svg_handler.py

from xml.sax.handler import ContentHandler

class SVGHandler(ContentHandler):

    def startPrefixMapping(self, prefix, uri):
        print(f"startPrefixMapping: {prefix=}, {uri=}")

    def endPrefixMapping(self, prefix):
        print(f"endPrefixMapping: {prefix=}")

    def startElementNS(self, name, qname, attrs):
        print(f"startElementNS: {name=}")

    def endElementNS(self, name, qname):
        print(f"endElementNS: {name=}")

Эти обратные вызовы получают дополнительные параметры о пространстве имен элемента. Чтобы синтаксический анализатор SAX действительно запускал эти обратные вызовы вместо некоторых предыдущих, необходимо явно включить поддержку пространства имен XML:>>>

>>> from xml.sax import make_parser
>>> from xml.sax.handler import feature_namespaces
>>> from svg_handler import SVGHandler

>>> parser = make_parser()
>>> parser.setFeature(feature_namespaces, True)
>>> parser.setContentHandler(SVGHandler())

>>> parser.parse("smiley.svg")
startPrefixMapping: prefix=None, uri='http://www.w3.org/2000/svg'
startPrefixMapping: prefix='inkscape', uri='http://www.inkscape.org/namespaces/inkscape'
startElementNS: name=('http://www.w3.org/2000/svg', 'svg')
⋮
endElementNS: name=('http://www.w3.org/2000/svg', 'svg')
endPrefixMapping: prefix='inkscape'
endPrefixMapping: prefix=None

Установка этой функции превращает элемент name в кортеж, состоящий из доменного имени пространства имен и имени тега.

xml.sax Пакет предлагает достойный интерфейс синтаксического анализа XML на основе событий, смоделированный по образцу оригинального Java API. Это несколько ограничено по сравнению с DOM, но должно быть достаточно для реализации базового push-анализатора потоковой передачи XML, не прибегая к сторонним библиотекам. Имея это в виду, в Python доступен менее подробный синтаксический анализатор, который вы изучите далее.

xml.dom.pulldom: Парсер Потоковой Передачи

Парсеры в стандартной библиотеке Python часто работают вместе. Например, xml.dom.pulldom модуль обертывает синтаксический xml.sax анализатор, чтобы воспользоваться преимуществами буферизации и прочитать документ по частям. В то же время он использует реализацию DOM по умолчанию от xml.dom.minidom для представления элементов документа. Однако эти элементы обрабатываются по одному за раз, не имея никакой связи, пока вы не попросите об этом явно.

Примечание: Поддержка пространства имен XML включена по умолчанию в xml.dom.pulldom.

В то время как модель SAX следует шаблону наблюдателя, вы можете рассматривать StAX как шаблон проектирования итератора, который позволяет вам зацикливаться на плоском потоке событий. Еще раз, вы можете вызвать знакомые parse() или parseString() функции, импортированные из модуля, для анализа изображения SVG:>>>

>>> from xml.dom.pulldom import parse
>>> event_stream = parse("smiley.svg")
>>> for event, node in event_stream:
...     print(event, node)
...
START_DOCUMENT <xml.dom.minidom.Document object at 0x7f74f9283e80>
START_ELEMENT <DOM Element: svg at 0x7f74fde18040>
CHARACTERS <DOM Text node "'\n'">
⋮
END_ELEMENT <DOM Element: script at 0x7f74f92b3c10>
CHARACTERS <DOM Text node "'\n'">
END_ELEMENT <DOM Element: svg at 0x7f74fde18040>

Для анализа документа требуется всего несколько строк кода. Самое поразительное различие между xml.sax и xml.dom.pulldom — это отсутствие обратных вызовов, так как вы управляете всем процессом. У вас гораздо больше свободы в структурировании кода, и вам не нужно использовать классы, если вы этого не хотите.

Обратите внимание, что узлы XML, извлеченные из потока, имеют типы, определенные в xml.dom.minidom. Но если бы вы проверили их родителей, братьев, сестер и детей, то обнаружили бы, что они ничего не знают друг о друге:>>>

>>> from xml.dom.pulldom import parse, START_ELEMENT
>>> event_stream = parse("smiley.svg")
>>> for event, node in event_stream:
...     if event == START_ELEMENT:
...         print(node.parentNode, node.previousSibling, node.childNodes)
<xml.dom.minidom.Document object at 0x7f90864f6e80> None []
None None []
None None []
None None []
⋮

Соответствующие атрибуты пусты. В любом случае, анализатор извлечения может помочь в гибридном подходе быстро найти какой-либо родительский элемент и построить дерево DOM только для ветви, укорененной в нем:

from xml.dom.pulldom import parse, START_ELEMENT

def process_group(parent):
    left_eye, right_eye = parent.getElementsByTagName("ellipse")
    # ...

event_stream = parse("smiley.svg")
for event, node in event_stream:
    if event == START_ELEMENT:
        if node.tagName == "g":
            event_stream.expandNode(node)
            process_group(node)

Вызывая .expandNode() поток событий, вы, по сути, перемещаете итератор вперед и рекурсивно анализируете XML-узлы, пока не найдете соответствующий закрывающий тег родительского элемента. Результирующий узел будет иметь дочерние элементы с правильно инициализированными атрибутами. Более того, вы сможете использовать на них методы DOM.

Анализатор pull предлагает интересную альтернативу DOM и SAX, сочетая лучшее из обоих миров. Он эффективен, гибок и прост в использовании, что приводит к более компактному и удобочитаемому коду. Вы также можете использовать его для более простой обработки нескольких XML-файлов одновременно. Тем не менее, ни один из упомянутых до сих пор синтаксических анализаторов XML не может сравниться с элегантностью, простотой и полнотой последнего, поступившего в стандартную библиотеку Python.

xml.etree.ElementTree: Легкая, Питоническая Альтернатива

Синтаксические анализаторы XML, с которыми вы познакомились до сих пор, выполняют свою работу. Однако они не очень хорошо вписываются в философию Python, и это не случайно. В то время как DOM следует спецификации W3C, а SAX был смоделирован по образцу Java API, ни один из них не кажется особенно питоническим.

Что еще хуже, и парсеры DOM, и парсеры SAX чувствуют себя устаревшими, поскольку часть их кода в интерпретаторе CPython не менялась более двух десятилетий! На момент написания этой статьи их реализация все еще не завершена и в ней отсутствуют типизированные заглушки, что нарушает завершение кода в редакторах кода.

Тем временем Python 2.5 привнес новый взгляд на синтаксический анализ и написание XML—документов-API ElementTree. Это легкий, эффективный, элегантный и многофункциональный интерфейс, на котором основаны даже некоторые сторонние библиотеки. Чтобы начать с этого, вы должны импортировать xml.etree.ElementTree модуль, что немного утомительно. Поэтому принято определять псевдоним следующим образом:

import xml.etree.ElementTree as ET

В немного более старом коде вы, возможно, видели cElementTree вместо этого импортированный модуль. Это была реализация в несколько раз быстрее, чем тот же интерфейс, написанный на C. Сегодня обычный модуль использует быструю реализацию, когда это возможно, так что вам больше не нужно беспокоиться.

Вы можете использовать API ElementTree, используя различные стратегии синтаксического анализа:

Не инкрементныйИнкрементный (Блокирующий)Инкрементный (неблокирующий)
ET.parse()✔️
ET.fromstring()✔️
ET.iterparse()✔️
ET.XMLPullParser✔️

Неинкрементная стратегия загружает весь документ в память в режиме, подобном DOM. В модуле есть две функции с соответствующими именами, которые позволяют анализировать файл или строку Python с XML-содержимым:>>>

>>> import xml.etree.ElementTree as ET

>>> # Parse XML from a filename
>>> ET.parse("smiley.svg")
<xml.etree.ElementTree.ElementTree object at 0x7fa4c980a6a0>

>>> # Parse XML from a file object
>>> with open("smiley.svg") as file:
...     ET.parse(file)
...
<xml.etree.ElementTree.ElementTree object at 0x7fa4c96df340>

>>> # Parse XML from a Python string
>>> ET.fromstring("""\
... <svg viewBox="-105 -100 210 270">
...   <!-- More content goes here... -->
... </svg>
... """)
<Element 'svg' at 0x7fa4c987a1d0>

Анализ объекта файла или имени файла с parse() помощью возвращает экземпляр ET.ElementTree класса, который представляет всю иерархию элементов. С другой стороны, анализ строки с fromstring() помощью вернет определенный корень ET.Element.

Кроме того, вы можете постепенно считывать XML-документ с помощью анализатора потоковой передачи, который выдает последовательность событий и элементов:>>>

>>> for event, element in ET.iterparse("smiley.svg"):
...     print(event, element.tag)
...
end {http://www.inkscape.org/namespaces/inkscape}custom
end {http://www.w3.org/2000/svg}stop
end {http://www.w3.org/2000/svg}stop
end {http://www.w3.org/2000/svg}stop
end {http://www.w3.org/2000/svg}linearGradient
⋮

По умолчанию iterparse() выдает только end события, связанные с закрывающим XML-тегом. Однако вы также можете подписаться на другие мероприятия. Вы можете найти их с помощью строковых констант, таких как"comment":>>>

>>> import xml.etree.ElementTree as ET
>>> for event, element in ET.iterparse("smiley.svg", ["comment"]):
...     print(element.text.strip())
...
Head
Eyes
Mouth

Вот список всех доступных типов событий:

  • start: Начало элемента
  • end: Конец элемента
  • comment: Элемент комментария
  • pi: Инструкция по обработке, как в XSL
  • start-ns: Начало пространства имен
  • end-ns: Конец пространства имен

Недостатком iterparse() является то, что он использует блокирующие вызовы для чтения следующего фрагмента данных, что может быть непригодно для асинхронного кода, выполняемого в одном потоке выполнения. Чтобы облегчить это, вы можете изучить XMLPullParser, что немного более подробно:

import xml.etree.ElementTree as ET

async def receive_data(url):
    """Download chunks of bytes from the URL asynchronously."""
    yield b"<svg "
    yield b"viewBox=\"-105 -100 210 270\""
    yield b"></svg>"

async def parse(url, events=None):
    parser = ET.XMLPullParser(events)
    async for chunk in receive_data(url):
        parser.feed(chunk)
        for event, element in parser.read_events():
            yield event, element

Этот гипотетический пример передает синтаксическому анализатору фрагменты XML, которые могут поступать с интервалом в несколько секунд. Как только содержимого будет достаточно, вы сможете повторить последовательность событий и элементов, буферизованных анализатором. Эта неблокирующая стратегия инкрементного анализа позволяет по-настоящему одновременно анализировать несколько XML-документов на лету во время их загрузки.

Элементы в дереве являются изменяемыми, повторяющимися и индексируемыми последовательностями. Они имеют длину, соответствующую числу их ближайших детей:>>>

>>> import xml.etree.ElementTree as ET
>>> tree = ET.parse("smiley.svg")
>>> root = tree.getroot()

>>> # The length of an element equals the number of its children.
>>> len(root)
5

>>> # The square brackets let you access a child by an index.
>>> root[1]
<Element '{http://www.w3.org/2000/svg}defs' at 0x7fe05d2e8860>
>>> root[2]
<Element '{http://www.w3.org/2000/svg}g' at 0x7fa4c9848400>

>>> # Elements are mutable. For example, you can swap their children.
>>> root[2], root[1] = root[1], root[2]

>>> # You can iterate over an element's children.
>>> for child in root:
...     print(child.tag)
...
{http://www.inkscape.org/namespaces/inkscape}custom
{http://www.w3.org/2000/svg}g
{http://www.w3.org/2000/svg}defs
{http://www.w3.org/2000/svg}text
{http://www.w3.org/2000/svg}script

Имена тегов могут иметь префикс с необязательным пространством имен, заключенным в пару фигурных скобок ({}). Пространство имен XML по умолчанию также отображается там, когда оно определено. Обратите внимание, как назначение подкачки в выделенной строке заставило <g> элемент появиться раньше <defs>. Это показывает изменчивую природу последовательности.

Вот еще несколько атрибутов и методов элементов, о которых стоит упомянуть:>>>

>>> element = root[0]

>>> element.tag
'{http://www.inkscape.org/namespaces/inkscape}custom'

>>> element.text
'Some value'

>>> element.attrib
{'x': '42', '{http://www.inkscape.org/namespaces/inkscape}z': '555'}

>>> element.get("x")
'42'

Одним из преимуществ этого API является то, как он использует собственные типы данных Python. Выше он использует словарь Python для атрибутов элемента. В предыдущих модулях они были завернуты в менее удобные адаптеры. В отличие от DOM, API ElementTree не предоставляет методов или свойств для обхода дерева в любом направлении, но есть несколько лучших альтернатив.

Как вы видели ранее, экземпляры Element класса реализуют протокол последовательности, позволяя вам перебирать их прямые дочерние элементы с помощью цикла:>>>

>>> for child in root:
...     print(child.tag)
...
{http://www.inkscape.org/namespaces/inkscape}custom
{http://www.w3.org/2000/svg}defs
{http://www.w3.org/2000/svg}g
{http://www.w3.org/2000/svg}text
{http://www.w3.org/2000/svg}script

Вы получаете последовательность непосредственных потомков корня. Однако, чтобы углубиться во вложенные потомки, вам придется вызвать .iter() метод для элемента-предка:>>>

>>> for descendant in root.iter():
...     print(descendant.tag)
...
{http://www.w3.org/2000/svg}svg
{http://www.inkscape.org/namespaces/inkscape}custom
{http://www.w3.org/2000/svg}defs
{http://www.w3.org/2000/svg}linearGradient
{http://www.w3.org/2000/svg}stop
{http://www.w3.org/2000/svg}stop
{http://www.w3.org/2000/svg}stop
{http://www.w3.org/2000/svg}g
{http://www.w3.org/2000/svg}circle
{http://www.w3.org/2000/svg}ellipse
{http://www.w3.org/2000/svg}ellipse
{http://www.w3.org/2000/svg}path
{http://www.w3.org/2000/svg}text
{http://www.w3.org/2000/svg}script

У корневого элемента всего пять потомков, но всего тринадцать потомков. Также можно сузить список потомков, отфильтровав только определенные имена тегов с помощью необязательного tag аргумента:>>>

>>> tag_name = "{http://www.w3.org/2000/svg}ellipse"
>>> for descendant in root.iter(tag_name):
...     print(descendant)
...
<Element '{http://www.w3.org/2000/svg}ellipse' at 0x7f430baa03b0>
<Element '{http://www.w3.org/2000/svg}ellipse' at 0x7f430baa0450>

На этот раз у вас есть только два <ellipse> элемента. Не забудьте включить пространство имен XML, например {http://www.w3.org/2000/svg}, в имя тега—до тех пор, пока оно определено. В противном случае, если вы укажете только имя тега без правильного пространства имен, у вас может оказаться меньше или больше элементов-потомков, чем первоначально предполагалось.

Работа с пространствами имен более удобна при использовании .iterfind(), которое допускает необязательное сопоставление префиксов с доменными именами. Чтобы указать пространство имен по умолчанию, вы можете оставить ключ пустым или назначить произвольный префикс, который позже необходимо будет использовать в имени тега:>>>

>>> namespaces = {
...     "": "http://www.w3.org/2000/svg",
...     "custom": "http://www.w3.org/2000/svg"
... }

>>> for descendant in root.iterfind("g", namespaces):
...     print(descendant)
...
<Element '{http://www.w3.org/2000/svg}g' at 0x7f430baa0270>

>>> for descendant in root.iterfind("custom:g", namespaces):
...     print(descendant)
...
<Element '{http://www.w3.org/2000/svg}g' at 0x7f430baa0270>

Сопоставление пространства имен позволяет ссылаться на один и тот же элемент с разными префиксами. Удивительно, но если вы попытаетесь найти эти вложенные <ellipse> элементы, как и раньше, то .iterfind() ничего не вернет, потому что ожидает выражение XPath, а не простое имя тега:>>>

>>> for descendant in root.iterfind("ellipse", namespaces):
...     print(descendant)
...

>>> for descendant in root.iterfind("g/ellipse", namespaces):
...     print(descendant)
...
<Element '{http://www.w3.org/2000/svg}ellipse' at 0x7f430baa03b0>
<Element '{http://www.w3.org/2000/svg}ellipse' at 0x7f430baa0450>

По совпадению, строка "g" оказывается допустимым путем относительно текущего root элемента, поэтому функция ранее возвращала непустой результат. Однако, чтобы найти многоточия, вложенные на один уровень глубже в иерархии XML, вам нужно более подробное выражение пути.

ElementTree имеет ограниченную синтаксическую поддержку мини-языка XPath, который можно использовать для запроса элементов в XML, аналогично селекторам CSS в HTML. Существуют и другие методы, которые принимают такое выражение:>>>

>>> namespaces = {"": "http://www.w3.org/2000/svg"}

>>> root.iterfind("defs", namespaces)
<generator object prepare_child.<locals>.select at 0x7f430ba6d190>

>>> root.findall("defs", namespaces)
[<Element '{http://www.w3.org/2000/svg}defs' at 0x7f430ba09e00>]

>>> root.find("defs", namespaces)
<Element '{http://www.w3.org/2000/svg}defs' at 0x7f430ba09e00>

В то время .iterfind() как лениво выдает совпадающие элементы, .findall() возвращает список и .find() возвращает только первый совпадающий элемент. Аналогично, вы можете извлечь текст, заключенный между открывающими и закрывающими тегами элементов, используя .findtext() или получить внутренний текст всего документа с помощью .itertext():>>>

>>> namespaces = {"i": "http://www.inkscape.org/namespaces/inkscape"}

>>> root.findtext("i:custom", namespaces=namespaces)
'Some value'

>>> for text in root.itertext():
...     if text.strip() != "":
...         print(text.strip())
...
Some value
Hello <svg>!
console.log("CDATA disables XML parsing: <svg>")
⋮

Сначала вы ищете текст, вложенный в определенный элемент XML, а затем повсюду во всем документе. Поиск по тексту-это мощная функция API ElementTree. Его можно воспроизвести с помощью других встроенных анализаторов, но за счет увеличения сложности кода и меньшего удобства.

API ElementTree, вероятно, является наиболее интуитивно понятным из всех. Он питонический, эффективный, надежный и универсальный. Если у вас нет особых причин использовать DOM или SAX, это должно быть вашим выбором по умолчанию.

Изучите Библиотеки Сторонних Анализаторов XML

Иногда, потянувшись за синтаксическими анализаторами XML в стандартной библиотеке, вы можете почувствовать, что берете кувалду, чтобы расколоть орех. В других случаях все наоборот, и вам нужен парсер, который мог бы сделать гораздо больше. Например, вы можете проверить XML на соответствие схеме или использовать расширенные выражения XPath. В таких ситуациях лучше всего проверить внешние библиотеки, доступные в PyPI.

Ниже вы найдете подборку внешних библиотек различной степени сложности и сложности.

untangle: Преобразование XML в объект Python

Если вы ищете однострочный текст, который мог бы превратить ваш XML-документ в объект Python, то не смотрите дальше. Хотя она не обновлялась в течение нескольких лет, untangle библиотека вскоре может стать вашим любимым способом анализа XML на Python. Есть только одна функция, которую нужно запомнить, и она принимает URL-адрес, имя файла, объект файла или строку XML:>>>

>>> import untangle

>>> # Parse XML from a URL
>>> untangle.parse("http://localhost:8000/smiley.svg")
Element(name = None, attributes = None, cdata = )

>>> # Parse XML from a filename
>>> untangle.parse("smiley.svg")
Element(name = None, attributes = None, cdata = )

>>> # Parse XML from a file object
>>> with open("smiley.svg") as file:
...     untangle.parse(file)
...
Element(name = None, attributes = None, cdata = )

>>> # Parse XML from a Python string
>>> untangle.parse("""\
... <svg viewBox="-105 -100 210 270">
...   <!-- More content goes here... -->
... </svg>
... """)
Element(name = None, attributes = None, cdata = )

В каждом случае он возвращает экземпляр Element класса. Вы можете использовать оператор точки для доступа к его дочерним узлам и синтаксис квадратных скобок для получения атрибутов XML или одного из дочерних узлов по индексу. Например, чтобы получить корневой элемент документа, вы можете получить к нему доступ, как если бы он был свойством объекта. Чтобы получить один из XML-атрибутов элемента, вы можете передать его имя в качестве ключа словаря:>>>

>>> import untangle
>>> document = untangle.parse("smiley.svg")

>>> document.svg
Element(name = svg, attributes = {'xmlns': ...}, ...)

>>> document.svg["viewBox"]
'-105 -100 210 270'

Здесь нет имен функций или методов, которые нужно запомнить. Вместо этого каждый анализируемый объект уникален, поэтому вам действительно нужно знать структуру базового XML-документа, чтобы просмотреть его untangle.

Чтобы узнать, как называется корневой элемент, обратитесь dir() к документу:>>>

>>> dir(document)
['svg']

Это показывает имена непосредственных потомков элемента. Обратите внимание, что это untangle переопределяет значение dir() для его проанализированных документов. Обычно вы вызываете эту встроенную функцию для проверки класса или модуля Python. Реализация по умолчанию будет возвращать список имен атрибутов, а не дочерние элементы XML-документа.

Если существует несколько дочерних элементов с заданным именем тега, вы можете перебирать их с помощью цикла или ссылаться на один из них по индексу:>>>

>>> dir(document.svg)
['defs', 'g', 'inkscape_custom', 'script', 'text']

>>> dir(document.svg.defs.linearGradient)
['stop', 'stop', 'stop']

>>> for stop in document.svg.defs.linearGradient.stop:
...     print(stop)
...
Element <stop> with attributes {'offset': ...}, ...
Element <stop> with attributes {'offset': ...}, ...
Element <stop> with attributes {'offset': ...}, ...

>>> document.svg.defs.linearGradient.stop[1]
Element(name = stop, attributes = {'offset': ...}, ...)

Возможно, вы заметили, что <inkscape:custom> элемент был переименован в inkscape_custom. К сожалению, библиотека не может хорошо обрабатывать пространства имен XML, поэтому, если вам нужно на это положиться, вам следует поискать в другом месте.

Из-за точечной нотации имена элементов в XML-документах должны быть допустимыми идентификаторами Python. Если это не так, то untangle автоматически перепишет их имена, заменив запрещенные символы подчеркиванием:>>>

>>> dir(untangle.parse("<com:company.web-app></com:company.web-app>"))
['com_company_web_app']

Имена детских тегов-не единственные свойства объектов, к которым вы можете получить доступ. Элементы имеют несколько предопределенных атрибутов объекта, которые могут быть показаны при вызове vars():>>>

>>> element = document.svg.text

>>> list(vars(element).keys())
['_name', '_attributes', 'children', 'is_root', 'cdata']

>>> element._name
'text'

>>> element._attributes
{'x': '-40', 'y': '75'}

>>> element.children
[]

>>> element.is_root
False

>>> element.cdata
'Hello <svg>!'

За кулисами untangle используется встроенный синтаксический анализатор SAX, но поскольку библиотека реализована на чистом Python и создает множество тяжелых объектов, она имеет значительно низкую производительность. Хотя он предназначен для чтения крошечных документов, вы все равно можете комбинировать его с другим подходом для чтения многогигабайтных XML-файлов.

Вот как. Если вы зайдете в архив Википедии, вы сможете загрузить один из их сжатых XML-файлов. Тот, что вверху, должен содержать снимок тезисов статей:

<feed>
  <doc>
    <title>Wikipedia: Anarchism</title>
    <url>https://en.wikipedia.org/wiki/Anarchism</url>
    <abstract>Anarchism is a political philosophy...</abstract>
    <links>
      <sublink linktype="nav">
        <anchor>Etymology, terminology and definition</anchor>
        <link>https://en.wikipedia.org/wiki/Anarchism#Etymology...</link>
      </sublink>
      <sublink linktype="nav">
        <anchor>History</anchor>
        <link>https://en.wikipedia.org/wiki/Anarchism#History</link>
      </sublink>
      ⋮
    </links>
  </doc>
  ⋮
</feed>

После загрузки он имеет размер более 6 ГБ, что идеально подходит для этого упражнения. Идея состоит в том, чтобы просканировать файл, чтобы найти последовательные открывающие и закрывающие <doc> теги, а затем проанализировать фрагмент XML между ними, используя untangle для удобства.

Встроенный mmap модуль позволяет создавать виртуальное представление содержимого файла, даже если оно не соответствует доступной памяти. Это создает впечатление работы с огромной строкой байтов, которая поддерживает поиск и обычный синтаксис нарезки. Если вас интересует, как инкапсулировать эту логику в класс Python и воспользоваться генератором для ленивой оценки, разверните раздел «сворачиваемый» ниже.

Гибридный подход к анализу XMLПоказать/Скрыть

Не вдаваясь в подробности, вот как вы можете использовать этот пользовательский класс для быстрого просмотра большого XML-файла, более тщательно проверяя конкретные элементы с помощью untangle:>>>

>>> with XMLTagStream("abstract.xml", "doc") as stream:
...     for doc in stream:
...         print(doc.title.cdata.center(50, "="))
...         for sublink in doc.links.sublink:
...             print("-", sublink.anchor.cdata)
...         if "q" == input("Press [q] to exit or any key to continue..."):
...             break
...
===============Wikipedia: Anarchism===============
- Etymology, terminology and definition
- History
- Pre-modern era
⋮
Press [q] to exit or any key to continue...
================Wikipedia: Autism=================
- Characteristics
- Social development
- Communication
⋮
Press [q] to exit or any key to continue...

Сначала вы открываете файл для чтения и указываете имя тега, который хотите найти. Затем вы повторяете эти элементы и получаете проанализированный фрагмент XML-документа. Это почти то же самое, что смотреть в крошечное окошко, двигаясь по бесконечно длинному листу бумаги. Это относительно поверхностный пример, в котором игнорируются некоторые детали, но он должен дать вам общее представление о том, как использовать такую гибридную стратегию синтаксического анализа.

xmltodict: Преобразование XML в словарь Python

Если вам нравится JSON, но вы не поклонник XML , тогда ознакомьтесь с xmltodict ним, который пытается преодолеть разрыв между обоими форматами данных. Как следует из названия, библиотека может анализировать XML-документ и представлять его в виде словаря Python, который также является целевым типом данных для документов JSON в Python. Это делает возможным преобразование между XML и JSON.

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

В отличие от остальных синтаксических анализаторов XML до сих пор, этот ожидает либо строку Python, либо файлоподобный объект, открытый для чтения в двоичном режиме:>>>

>>> import xmltodict

>>> xmltodict.parse("""\
... <svg viewBox="-105 -100 210 270">
...   <!-- More content goes here... -->
... </svg>
... """)
OrderedDict([('svg', OrderedDict([('@viewBox', '-105 -100 210 270')]))])

>>> with open("smiley.svg", "rb") as file:
...     xmltodict.parse(file)
...
OrderedDict([('svg', ...)])

По умолчанию библиотека возвращает экземпляр OrderedDict коллекции, чтобы сохранить порядок элементов. Однако, начиная с Python 3.6, простые словари также сохраняют порядок вставки. Если вы хотите вместо этого работать с обычными словарями, то передайте dict в качестве dict_constructor аргумента parse() функции:>>>

>>> import xmltodict

>>> with open("smiley.svg", "rb") as file:
...     xmltodict.parse(file, dict_constructor=dict)
...
{'svg': ...}

Теперь parse() возвращает обычный старый словарь со знакомым текстовым представлением.

Чтобы избежать конфликтов имен между XML-элементами и их атрибутами, библиотека автоматически добавляет к последним префиксы с @ символом. Вы также можете полностью игнорировать атрибуты, установив соответствующий xml_attribs флаг:>>>

>>> import xmltodict

>>> # Rename attributes by default
>>> with open("smiley.svg", "rb") as file:
...     document = xmltodict.parse(file)
...     print([x for x in document["svg"] if x.startswith("@")])
...
['@xmlns', '@xmlns:inkscape', '@viewBox', '@width', '@height']

>>> # Ignore attributes when requested
>>> with open("smiley.svg", "rb") as file:
...     document = xmltodict.parse(file, xml_attribs=False)
...     print([x for x in document["svg"] if x.startswith("@")])
...
[]

Еще одна информация, которая по умолчанию игнорируется, — это объявление пространства имен XML. Они рассматриваются как обычные атрибуты, в то время как соответствующие префиксы становятся частью имени тега. Однако вы можете расширить, переименовать или пропустить некоторые пространства имен, если хотите:>>>

>>> import xmltodict

>>> # Ignore namespaces by default
>>> with open("smiley.svg", "rb") as file:
...     document = xmltodict.parse(file)
...     print(document.keys())
...
odict_keys(['svg'])

>>> # Process namespaces when requested
>>> with open("smiley.svg", "rb") as file:
...     document = xmltodict.parse(file, process_namespaces=True)
...     print(document.keys())
...
odict_keys(['http://www.w3.org/2000/svg:svg'])

>>> # Rename and skip some namespaces
>>> namespaces = {
...     "http://www.w3.org/2000/svg": "svg",
...     "http://www.inkscape.org/namespaces/inkscape": None,
... }
>>> with open("smiley.svg", "rb") as file:
...     document = xmltodict.parse(
...         file, process_namespaces=True, namespaces=namespaces
...     )
...     print(document.keys())
...     print("custom" in document["svg:svg"])
...     print("inkscape:custom" in document["svg:svg"])
...
odict_keys(['svg:svg'])
True
False

В первом примере выше имена тегов не включают префикс пространства имен XML. Во втором примере они это делают, потому что вы попросили их обработать. Наконец, в третьем примере вы свернули пространство имен по умолчанию svg в, подавляя пространство имен Inkscape с None помощью .

Строковое представление словаря Python по умолчанию может быть недостаточно разборчивым. Чтобы улучшить его презентацию, вы можете распечатать его или преобразовать в другой формат, такой как JSON или YAML:>>>

>>> import xmltodict
>>> with open("smiley.svg", "rb") as file:
...     document = xmltodict.parse(file, dict_constructor=dict)
...

>>> from pprint import pprint as pp
>>> pp(document)
{'svg': {'@height': '270',
         '@viewBox': '-105 -100 210 270',
         '@width': '210',
         '@xmlns': 'http://www.w3.org/2000/svg',
         '@xmlns:inkscape': 'http://www.inkscape.org/namespaces/inkscape',
         'defs': {'linearGradient': {'@id': 'skin',
         ⋮

>>> import json
>>> print(json.dumps(document, indent=4, sort_keys=True))
{
    "svg": {
        "@height": "270",
        "@viewBox": "-105 -100 210 270",
        "@width": "210",
        "@xmlns": "http://www.w3.org/2000/svg",
        "@xmlns:inkscape": "http://www.inkscape.org/namespaces/inkscape",
        "defs": {
            "linearGradient": {
             ⋮

>>> import yaml  # Install first with 'pip install PyYAML'
>>> print(yaml.dump(document))
svg:
  '@height': '270'
  '@viewBox': -105 -100 210 270
  '@width': '210'
  '@xmlns': http://www.w3.org/2000/svg
  '@xmlns:inkscape': http://www.inkscape.org/namespaces/inkscape
  defs:
    linearGradient:
    ⋮

xmltodict Библиотека позволяет конвертировать документ наоборот—то есть из словаря Python обратно в строку XML:>>>

>>> import xmltodict

>>> with open("smiley.svg", "rb") as file:
...     document = xmltodict.parse(file, dict_constructor=dict)
...

>>> xmltodict.unparse(document)
'<?xml version="1.0" encoding="utf-8"?>\n<svg...'

Словарь может пригодиться в качестве промежуточного формата при преобразовании данных из JSON или YAML в XML, если возникнет такая необходимость.

В библиотеке есть еще множество функций xmltodict, таких как потоковая передача, поэтому не стесняйтесь изучать их самостоятельно. Однако эта библиотека тоже немного устарела. Кроме того, это следующая библиотека, которая должна быть на вашем радаре, если вы действительно ищете расширенные функции синтаксического анализа XML.

lxml: Используйте ElementTree на стероидах

Если вы хотите получить максимальную производительность, широчайший спектр функциональных возможностей и наиболее знакомый интерфейс в одном пакете, установите lxml и забудьте об остальных библиотеках. Это привязка Python для библиотек C libxml2 и libxslt, которые поддерживают несколько стандартов, включая XPath, XML-схему и XSLT.

Библиотека совместима с API ElementTree Python, о котором вы узнали ранее в этом руководстве. Это означает, что вы можете повторно использовать существующий код, заменив только одну инструкцию импорта:

import lxml.etree as ET

Это даст вам отличный прирост производительности. Кроме того, lxml библиотека поставляется с широким набором функций и предоставляет различные способы их использования. Например, он позволяет проверять ваши XML-документы на нескольких языках схем, одним из которых является определение схемы XML:>>>

>>> import lxml.etree as ET

>>> xml_schema = ET.XMLSchema(
...     ET.fromstring("""\
...         <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
...             <xsd:element name="parent"/>
...             <xsd:complexType name="SomeType">
...                 <xsd:sequence>
...                     <xsd:element name="child" type="xsd:string"/>
...                 </xsd:sequence>
...             </xsd:complexType>
...         </xsd:schema>"""))

>>> valid = ET.fromstring("<parent><child></child></parent>")
>>> invalid = ET.fromstring("<child><parent></parent></child>")

>>> xml_schema.validate(valid)
True

>>> xml_schema.validate(invalid)
False

Ни один из анализаторов XML в стандартной библиотеке Python не имеет возможности проверять документы. Между тем, lxml позволяет определить XMLSchema объект и запускать через него документы, оставаясь в значительной степени совместимым с API ElementTree.

Помимо API ElementTree, lxml поддерживается альтернативный интерфейс lxml.objectify, о котором вы расскажете позже в разделе привязка данных.

BeautifulSoup: Работа С Искаженным XML

Обычно вы не будете использовать последнюю библиотеку в этом сравнении для анализа XML, так как в основном вы сталкиваетесь с ней при очистке HTML-документов. Тем не менее, он также способен анализировать XML. BeautifulSoup поставляется с подключаемой архитектурой, которая позволяет вам выбирать базовый синтаксический анализатор. Описанный lxml ранее на самом деле рекомендован официальной документацией и в настоящее время является единственным анализатором XML, поддерживаемым библиотекой.

В зависимости от типа документов, которые вы хотите проанализировать, желаемой эффективности и доступности функций, вы можете выбрать один из этих анализаторов:

Тип документаИмя синтаксического анализатораБиблиотека PythonСкорость
HTML"html.parser"Умеренный
HTML"html5lib"html5libМедленный
HTML"lxml"lxmlБыстро
XML"lxml-xml" или "xml"lxmlБыстро

Помимо скорости, существуют заметные различия между отдельными анализаторами. Например, некоторые из них более снисходительны, чем другие, когда дело доходит до искаженных элементов, в то время как другие лучше эмулируют веб-браузеры.

Забавный факт: Название библиотеки относится к тегу soup, который описывает синтаксически или структурно неверный HTML-код.

Предполагая, что вы уже установили библиотеки lxml и beautifulsoup4 в свою активную виртуальную среду, вы можете сразу же приступить к анализу XML-документов. Вам нужно только импортировать BeautifulSoup:

from bs4 import BeautifulSoup

# Parse XML from a file object
with open("smiley.svg") as file:
    soup = BeautifulSoup(file, features="lxml-xml")

# Parse XML from a Python string
soup = BeautifulSoup("""\
<svg viewBox="-105 -100 210 270">
  <!-- More content goes here... -->
</svg>
""", features="lxml-xml")

Если вы случайно указали другой синтаксический анализатор, скажем lxml, то библиотека добавит отсутствующие HTML-теги, например, <body> в анализируемый документ для вас. Вероятно, это не то, что вы имели в виду в данном случае, поэтому будьте осторожны при указании имени синтаксического анализатора.

BeautifulSoup-это мощный инструмент для анализа XML-документов, поскольку он может обрабатывать недопустимое содержимое и имеет богатый API для извлечения информации. Посмотрите, как он справляется с неправильно вложенными тегами, запрещенными символами и плохо размещенным текстом:>>>

>>> from bs4 import BeautifulSoup

>>> soup = BeautifulSoup("""\
... <parent>
...     <child>Forbidden < character </parent>
...     </child>
... ignored
... """, features="lxml-xml")

>>> print(soup.prettify())
<?xml version="1.0" encoding="utf-8"?>
<parent>
 <child>
  Forbidden
 </child>
</parent>

Другой анализатор выдаст исключение и сдастся, как только обнаружит что-то не так с документом. Здесь он не только проигнорировал проблемы, но и нашел разумные способы исправить некоторые из них. Теперь элементы правильно вложены и не имеют недопустимого содержимого.

Существует слишком много методов определения местоположения элементов с помощью BeautifulSoup, чтобы охватить их все здесь. Обычно вы вызываете вариант .find() или .findall() на элементе супа:>>>

>>> from bs4 import BeautifulSoup

>>> with open("smiley.svg") as file:
...     soup = BeautifulSoup(file, features="lxml-xml")
...

>>> soup.find_all("ellipse", limit=1)
[<ellipse cx="-20" cy="-10" fill="black" rx="6" ry="8" stroke="none"/>]

>>> soup.find(x=42)
<inkscape:custom inkscape:z="555" x="42">Some value</inkscape:custom>

>>> soup.find("stop", {"stop-color": "gold"})
<stop offset="75%" stop-color="gold" stop-opacity="1.0"/>

>>> soup.find(text=lambda x: "value" in x).parent
<inkscape:custom inkscape:z="555" x="42">Some value</inkscape:custom>

limit Параметр аналогичен LIMIT предложению в MySQL, которое позволяет вам решить, сколько результатов вы хотите получить максимум. Он вернет указанное количество результатов или меньше. Это не совпадение. Вы можете рассматривать эти методы поиска как простой язык запросов с мощными фильтрами.

Интерфейс поиска очень гибкий, но выходит за рамки этого руководства. Вы можете проверить документацию библиотеки для получения более подробной информации или прочитать еще один учебник по веб-очистке на Python, который касается BeautifulSoup.

Привязка XML — данных к объектам Python

Допустим, вы хотите использовать канал данных в реальном времени через соединение WebSocket с низкой задержкой для обмена сообщениями в формате XML. Для целей этой презентации вы собираетесь использовать веб-браузер для трансляции событий мыши и клавиатуры на сервер Python. Вы создадите пользовательский протокол и будете использовать привязку данных для преобразования XML в собственные объекты Python.

Идея привязки данных состоит в том, чтобы декларативно определить модель данных, позволяя программе выяснить, как извлечь ценную часть информации из XML во время выполнения. Если вы когда-либо работали с моделями Django, то эта концепция должна звучать знакомо.

Во-первых, начните с разработки своей модели данных. Он будет состоять из двух типов мероприятий:

  1. KeyboardEvent
  2. MouseEvent

Каждый из них может представлять несколько специализированных подтипов, таких как нажатие клавиши или отпускание клавиши для клавиатуры и щелчок или щелчок правой кнопкой мыши для мыши. Вот пример XML-сообщения, полученного в ответ на удержание Shift2комбинации клавиш + :

<KeyboardEvent>
    <Type>keydown</Type>
    <Timestamp>253459.17999999982</Timestamp>
    <Key>
        <Code>Digit2</Code>
        <Unicode>@</Unicode>
    </Key>
    <Modifiers>
        <Alt>false</Alt>
        <Ctrl>false</Ctrl>
        <Shift>true</Shift>
        <Meta>false</Meta>
    </Modifiers>
</KeyboardEvent>

Это сообщение содержит определенный тип события клавиатуры, метку времени, код клавиши и ее Юникод, а также клавиши-модификаторы, такие как Alt, Ctrl, или ShiftМета-клавиша обычно WinCmd является клавишей или, в зависимости от вашей раскладки клавиатуры.

Аналогично, событие мыши может выглядеть следующим образом:

<MouseEvent>
    <Type>mousemove</Type>
    <Timestamp>52489.07000000145</Timestamp>
    <Cursor>
        <Delta x="-4" y="8"/>
        <Window x="171" y="480"/>
        <Screen x="586" y="690"/>
    </Cursor>
    <Buttons bitField="0"/>
    <Modifiers>
        <Alt>false</Alt>
        <Ctrl>true</Ctrl>
        <Shift>false</Shift>
        <Meta>false</Meta>
    </Modifiers>
</MouseEvent>

Однако вместо клавиши есть положение курсора мыши и битовое поле, кодирующее кнопки мыши, нажатые во время события. Нулевое битовое поле указывает на то, что кнопка не была нажата.

Как только клиент установит соединение, он начнет заполнять сервер сообщениями. Протокол не будет состоять из каких-либо рукопожатий, сердцебиений, изящных отключений, подписок на темы или управляющих сообщений. Вы можете закодировать это в JavaScript, зарегистрировав обработчики событий и создав WebSocket объект менее чем в пятидесяти строках кода.

Однако реализация клиента не является целью этого упражнения. Поскольку вам не нужно это понимать, просто разверните раздел «сворачивание» ниже, чтобы показать HTML-код со встроенным JavaScript и сохраните его в файле с именем, которое вам нравится.

Клиент WebSocket в JavaScript и HTML

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Real-Time Data Feed</title>
</head>
<body>
    <script>
        const ws = new WebSocket("ws://localhost:8000")
        ws.onopen = event => {
            ["keydown", "keyup"].forEach(name =>
                window.addEventListener(name, event =>
                    ws.send(`\
<KeyboardEvent>
    <Type>${event.type}</Type>
    <Timestamp>${event.timeStamp}</Timestamp>
    <Key>
        <Code>${event.code}</Code>
        <Unicode>${event.key}</Unicode>
    </Key>
    <Modifiers>
        <Alt>${event.altKey}</Alt>
        <Ctrl>${event.ctrlKey}</Ctrl>
        <Shift>${event.shiftKey}</Shift>
        <Meta>${event.metaKey}</Meta>
    </Modifiers>
</KeyboardEvent>`))
            );
            ["mousedown", "mouseup", "mousemove"].forEach(name =>
                window.addEventListener(name, event =>
                    ws.send(`\
<MouseEvent>
    <Type>${event.type}</Type>
    <Timestamp>${event.timeStamp}</Timestamp>
    <Cursor>
        <Delta x="${event.movementX}" y="${event.movementY}"/>
        <Window x="${event.clientX}" y="${event.clientY}"/>
        <Screen x="${event.screenX}" y="${event.screenY}"/>
    </Cursor>
    <Buttons bitField="${event.buttons}"/>
    <Modifiers>
        <Alt>${event.altKey}</Alt>
        <Ctrl>${event.ctrlKey}</Ctrl>
        <Shift>${event.shiftKey}</Shift>
        <Meta>${event.metaKey}</Meta>
    </Modifiers>
</MouseEvent>`))
            )
        }
    </script>
</body>
</html>

Клиент подключается к локальному серверу, прослушивающему порт 8000. Как только вы сохраните HTML-код в файле, вы сможете открыть его в своем любимом веб-браузере. Но перед этим вам нужно будет реализовать сервер.

Python не поставляется с поддержкой WebSocket, но вы можете установить websockets библиотеку в свою активную виртуальную среду. Вам также понадобится lxml позже, так что это хороший момент для установки обеих зависимостей за один раз:

$ python -m pip install websockets lxml

Наконец, вы можете создать минимальный асинхронный веб — сервер:

# server.py

import asyncio
import websockets

async def handle_connection(websocket, path):
    async for message in websocket:
        print(message)

if __name__ == "__main__":
    future = websockets.serve(handle_connection, "localhost", 8000)
    asyncio.get_event_loop().run_until_complete(future)
    asyncio.get_event_loop().run_forever()

Когда вы запускаете сервер и открываете сохраненный HTML-файл в веб-браузере, вы должны видеть, как XML-сообщения появляются в стандартном выводе в ответ на ваши движения мыши и нажатия клавиш. Вы можете открыть клиент на нескольких вкладках или даже в нескольких браузерах одновременно!

Определение моделей с помощью выражений XPath

Прямо сейчас ваши сообщения поступают в обычном строковом формате. Работать с сообщениями в таком формате не очень удобно. К счастью, вы можете превратить их в составные объекты Python с помощью одной строки кода, используя lxml.objectify модуль:

# server.py

import asyncio
import websockets
import lxml.objectify

async def handle_connection(websocket, path):
    async for message in websocket:
        try:
            xml = lxml.objectify.fromstring(message)
        except SyntaxError:
            print("Malformed XML message:", repr(message))
        else:
            if xml.tag == "KeyboardEvent":
                if xml.Type == "keyup":
                    print("Key:", xml.Key.Unicode)
            elif xml.tag == "MouseEvent":
                screen = xml.Cursor.Screen
                print("Mouse:", screen.get("x"), screen.get("y"))
            else:
                print("Unrecognized event type")

# ...

Если синтаксический анализ XML прошел успешно, вы можете проверить обычные свойства корневого элемента, такие как имя тега, атрибуты, внутренний текст и так далее. Вы сможете использовать оператор точки для навигации вглубь дерева элементов. В большинстве случаев библиотека распознает подходящий тип данных Python и преобразует значение для вас.

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

$ python server.py
Mouse: 820 121
Mouse: 820 122
Mouse: 820 123
Mouse: 820 124
Mouse: 820 125
Key: a
Mouse: 820 125
Mouse: 820 125
Key: a
Key: A
Key: Shift
Mouse: 821 125
Mouse: 821 125
Mouse: 820 123
⋮

Иногда XML может содержать имена тегов, которые не являются допустимыми идентификаторами Python, или вам может потребоваться адаптировать структуру сообщений в соответствии с вашей моделью данных. В таком случае интересным вариантом было бы определение пользовательских классов моделей с дескрипторами, которые объявляют, как искать информацию с помощью выражений XPath. Это та часть, которая начинает напоминать модели Django или определения схем Pydantic.

Вы собираетесь использовать пользовательский XPath дескриптор и сопутствующий Model класс, которые предоставляют повторно используемые свойства для ваших моделей данных. Дескриптор ожидает выражение XPath для поиска элементов в полученном сообщении. Базовая реализация немного продвинута, поэтому не стесняйтесь копировать код из разборного раздела ниже.

Дескриптор XPath и класс модели

import lxml.objectify

class XPath:
    def __init__(self, expression, /, default=None, multiple=False):
        self.expression = expression
        self.default = default
        self.multiple = multiple

    def __set_name__(self, owner, name):
        self.attribute_name = name
        self.annotation = owner.__annotations__.get(name)

    def __get__(self, instance, owner):
        value = self.extract(instance.xml)
        instance.__dict__[self.attribute_name] = value
        return value

    def extract(self, xml):
        elements = xml.xpath(self.expression)
        if elements:
            if self.multiple:
                if self.annotation:
                    return [self.annotation(x) for x in elements]
                else:
                    return elements
            else:
                first = elements[0]
                if self.annotation:
                    return self.annotation(first)
                else:
                    return first
        else:
            return self.default

class Model:
    """Abstract base class for your models."""
    def __init__(self, data):
        if isinstance(data, str):
            self.xml = lxml.objectify.fromstring(data)
        elif isinstance(data, lxml.objectify.ObjectifiedElement):
            self.xml = data
        else:
            raise TypeError("Unsupported data type:", type(data))

Предполагая, что у вас уже есть нужный XPath дескриптор и Model абстрактный базовый класс в вашем модуле, вы можете использовать их для определения KeyboardEventMouseEvent типов сообщений и сообщений вместе с многоразовыми строительными блоками, чтобы избежать повторения. Существует бесконечное множество способов сделать это, но вот один пример:

# ...

class Event(Model):
    """Base class for event messages with common elements."""
    type_: str = XPath("./Type")
    timestamp: float = XPath("./Timestamp")

class Modifiers(Model):
    alt: bool = XPath("./Alt")
    ctrl: bool = XPath("./Ctrl")
    shift: bool = XPath("./Shift")
    meta: bool = XPath("./Meta")

class KeyboardEvent(Event):
    key: str = XPath("./Key/Code")
    modifiers: Modifiers = XPath("./Modifiers")

class MouseEvent(Event):
    x: int = XPath("./Cursor/Screen/@x")
    y: int = XPath("./Cursor/Screen/@y")
    modifiers: Modifiers = XPath("./Modifiers")

XPath Дескриптор позволяет выполнять отложенную оценку, так что элементы XML-сообщений просматриваются только по запросу. Более конкретно, они отображаются только при доступе к свойству объекта события. Кроме того, результаты кэшируются, чтобы избежать выполнения одного и того же запроса XPath более одного раза. Дескриптор также учитывает аннотации типов и автоматически преобразует десериализованные данные в нужный тип Python.

Использование этих объектов событий не сильно отличается от тех, которые автоматически создавались lxml.objectify ранее:

if xml.tag == "KeyboardEvent":
    event = KeyboardEvent(xml)
    if event.type_ == "keyup":
        print("Key:", event.key)
elif xml.tag == "MouseEvent":
    event = MouseEvent(xml)
    print("Mouse:", event.x, event.y)
else:
    print("Unrecognized event type")

Существует дополнительный шаг создания новых объектов определенного типа события. Но кроме этого, это дает вам большую гибкость с точки зрения структурирования вашей модели независимо от протокола XML. Кроме того, можно получить новые атрибуты модели на основе атрибутов в полученных сообщениях и добавить дополнительные методы в дополнение к этому.

Создание Моделей на основе XML-Схемы

Реализация классов моделей — утомительная и подверженная ошибкам задача. Однако, пока ваша модель отражает XML-сообщения, вы можете воспользоваться автоматическим инструментом для создания необходимого кода на основе XML-схемы. Недостатком такого кода является то, что он обычно менее читаем, чем если бы был написан от руки.

Одним из старейших сторонних модулей, позволяющих это, был PyXB, который имитирует популярную библиотеку Java JAXB. К сожалению, последний раз он был выпущен несколько лет назад и предназначался для устаревших версий Python. Вы можете изучить аналогичную, но активно поддерживаемую generateDS альтернативу, которая генерирует структуры данных из XML-схемы.

Допустим, у вас есть этот models.xsd файл схемы, описывающий ваше KeyboardEvent сообщение:

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xsd:element name="KeyboardEvent" type="KeyboardEventType"/>
    <xsd:complexType name="KeyboardEventType">
        <xsd:sequence>
            <xsd:element type="xsd:string" name="Type"/>
            <xsd:element type="xsd:float" name="Timestamp"/>
            <xsd:element type="KeyType" name="Key"/>
            <xsd:element type="ModifiersType" name="Modifiers"/>
        </xsd:sequence>
    </xsd:complexType>
    <xsd:complexType name="KeyType">
        <xsd:sequence>
            <xsd:element type="xsd:string" name="Code"/>
            <xsd:element type="xsd:string" name="Unicode"/>
        </xsd:sequence>
    </xsd:complexType>
    <xsd:complexType name="ModifiersType">
        <xsd:sequence>
            <xsd:element type="xsd:string" name="Alt"/>
            <xsd:element type="xsd:string" name="Ctrl"/>
            <xsd:element type="xsd:string" name="Shift"/>
            <xsd:element type="xsd:string" name="Meta"/>
        </xsd:sequence>
    </xsd:complexType>
</xsd:schema>

Схема сообщает синтаксическому анализатору XML, какие элементы следует ожидать, их порядок и уровень в дереве. Он также ограничивает допустимые значения атрибутов XML. Любые расхождения между этими объявлениями и фактическим XML-документом должны сделать его недействительным и заставить анализатор отклонить документ.

Кроме того, некоторые инструменты могут использовать эту информацию для создания фрагмента кода, который скрывает от вас детали синтаксического анализа XML. После установки библиотеки вы сможете запустить generateDS команду в своей активной виртуальной среде:

$ generateDS -o models.py models.xsd

Он создаст новый файл с именем models.py в том же каталоге, что и сгенерированный исходный код Python. Затем вы можете импортировать этот модуль и использовать его для анализа входящих сообщений:>>>

>>> from models import parseString

>>> event = parseString("""\
... <KeyboardEvent>
...     <Type>keydown</Type>
...     <Timestamp>253459.17999999982</Timestamp>
...     <Key>
...         <Code>Digit2</Code>
...         <Unicode>@</Unicode>
...     </Key>
...     <Modifiers>
...         <Alt>false</Alt>
...         <Ctrl>false</Ctrl>
...         <Shift>true</Shift>
...         <Meta>false</Meta>
...     </Modifiers>
... </KeyboardEvent>""", silence=True)

>>> event.Type, event.Key.Code
('keydown', 'Digit2')

Это похоже на lxml.objectify пример, показанный ранее. Разница в том, что использование привязки данных обеспечивает соответствие схеме, в то lxml.objectify время как объекты создаются динамически, независимо от того, являются ли они семантически правильными.

Обезвредьте XML-Бомбу с помощью безопасных анализаторов

Синтаксические анализаторы XML в стандартной библиотеке Python уязвимы для множества угроз безопасности, которые в лучшем случае могут привести к отказу в обслуживании (DoS) или потере данных. Честно говоря, это не их вина. Они просто следуют спецификации стандарта XML, который является более сложным и мощным, чем знает большинство людей.

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

Одной из наиболее распространенных атак является XML-бомба, также известная как атака на миллиард смехов. Атака использует расширение сущности в DTD, чтобы увеличить объем памяти и занять процессор как можно дольше. Все, что вам нужно, чтобы остановить незащищенный веб-сервер от получения нового трафика, — это эти несколько строк XML-кода:

import xml.etree.ElementTree as ET
ET.fromstring("""\
<?xml version="1.0"?>
<!DOCTYPE lolz [
 <!ENTITY lol "lol">
 <!ELEMENT lolz (#PCDATA)>
 <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
 <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
 <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
 <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
 <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
 <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
 <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
 <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
 <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>""")

Наивный синтаксический анализатор попытается разрешить пользовательскую сущность &lol9;, размещенную в корневом каталоге документа, путем проверки DTD. Однако сама эта сущность несколько раз ссылается на другую сущность, которая ссылается на еще одну сущность и так далее. Когда вы запустите приведенный выше сценарий, вы заметите, что что-то беспокоит вашу память и процессор:https://player.vimeo.com/video/563603395?background=1

Посмотрите, как основная память и раздел подкачки исчерпываются всего за считанные секунды, в то время как один из процессоров работает на 100% своей емкости. Запись резко прекращается, когда системная память заполняется, а затем возобновляется после завершения процесса Python.

Другой популярный тип атаки, известный как XXE, использует общие внешние объекты для чтения локальных файлов и выполнения сетевых запросов. Тем не менее, начиная с Python 3.7.1, эта функция по умолчанию отключена для повышения безопасности. Если вы доверяете своим данным, то вы можете в любом случае попросить синтаксический анализатор SAX обрабатывать внешние объекты:>>>

>>> from xml.sax import make_parser
>>> from xml.sax.handler import feature_external_ges

>>> parser = make_parser()
>>> parser.setFeature(feature_external_ges, True)

Этот анализатор сможет считывать локальные файлы на вашем компьютере. Он может извлекать имена пользователей в Unix-подобной операционной системе, например:>>>

>>> from xml.dom.minidom import parseString

>>> xml = """\
... <?xml version="1.0" encoding="UTF-8"?>
... <!DOCTYPE root [
...     <!ENTITY usernames SYSTEM "/etc/passwd">
... ]>
... <root>&usernames;</root>"""

>>> document = parseString(xml, parser)
>>> print(document.documentElement.toxml())
<root>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
⋮
realpython:x:1001:1001:Real Python,,,:/home/realpython:/bin/bash
</root>

Вполне возможно отправить эти данные по сети на удаленный сервер!

Итак, как вы можете защитить себя от таких атак? Официальная документация Python явно предупреждает вас о рисках использования встроенных анализаторов XML и рекомендует переключиться на внешний пакет в критически важных приложениях. Хотя он не распространяется с Python, defusedxml он является заменой всех анализаторов в стандартной библиотеке.

Библиотека накладывает строгие ограничения и отключает множество опасных функций XML. Это должно остановить большинство хорошо известных атак, включая две только что описанные. Чтобы использовать его, возьмите библиотеку из PyPI и соответствующим образом замените инструкции импорта:>>>

>>> import defusedxml.ElementTree as ET
>>> ET.parse("bomb.xml")
Traceback (most recent call last):
  ...
    raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
defusedxml.common.EntitiesForbidden:
 EntitiesForbidden(name='lol', system_id=None, public_id=None)

Вот и все! Запрещенные функции больше не пройдут.

Вывод

Формат данных XML-это зрелый и удивительно мощный стандарт, который все еще используется сегодня, особенно в условиях предприятия. Выбор правильного синтаксического анализатора XML имеет решающее значение для нахождения оптимального соотношения между производительностью, безопасностью, соответствием требованиям и удобством.

Этот учебник дает вам в руки подробную дорожную карту для навигации по запутанному лабиринту синтаксических анализаторов XML на Python. Вы знаете, где срезать путь и как избежать тупиков, что сэкономит вам много времени.

В этом уроке вы узнали, как:

  • Выберите правильную модель синтаксического анализа XML
  • Используйте синтаксические анализаторы XML в стандартной библиотеке
  • Используйте основные библиотеки синтаксического анализа XML
  • Декларативно анализировать XML — документы с использованием привязки данных
  • Используйте безопасные анализаторы XML для устранения уязвимостей в системе безопасности

Теперь вы понимаете различные стратегии анализа XML-документов, а также их сильные и слабые стороны. Обладая этими знаниями, вы сможете выбрать наиболее подходящий синтаксический анализатор XML для вашего конкретного случая использования и даже объединить более одного, чтобы быстрее считывать многогигабайтные XML-файлы.