Глава 12. XML

Добавлено 5 июля 2020 в 11:59

Содержание главы

Погружение

Большинство глав в этой книге строятся на фрагментах примеров кода. Но xml – это больше данные, нежели код. Один из способов применения xml – это «объединяющие каналы», такие как список последних статей в блоге, на форуме или на другом часто обновляемом сайте. Большинство популярного программного обеспечения для ведения блогов может создавать каналы (ленты, фиды) и обновлять их, когда публикуются новые статьи, темы форума или посты блога. Вы можете следить за блогом, подписавшись на его RSS канал, а также вы можете следить за несколькими блогами при помощи «агрегаторов каналов», таких как Google Reader (сейчас уже закрытый, примечание переводчика).

Итак, ниже представлены XML данные, с которыми мы будем работать в этой главе. Это фид в формате Atom syndication feed.

Скачать файл feed.xml.

<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into mark</title>
  <subtitle>currently between addictions</subtitle>
  <id>tag:diveintomark.org,2001-07-29:/</id>
  <updated>2009-03-27T21:56:07Z</updated>
  <link rel='alternate' type='text/html' href='http://diveintomark.org/'/>
  <link rel='self' type='application/atom+xml' href='http://diveintomark.org/feed/'/>
  <entry>
    <author>
      <name>Mark</name>
      <uri>http://diveintomark.org/</uri>
    </author>
    <title>Dive into history, 2009 edition</title>
    <link rel='alternate' type='text/html'
      href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>
    <id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id>
    <updated>2009-03-27T21:56:07Z</updated>
    <published>2009-03-27T17:20:42Z</published>
    <category scheme='http://diveintomark.org' term='diveintopython'/>
    <category scheme='http://diveintomark.org' term='docbook'/>
    <category scheme='http://diveintomark.org' term='html'/>
  <summary type='html'>Putting an entire chapter on one page sounds
    bloated, but consider this &amp;mdash; my longest chapter so far
    would be 75 printed pages, and it loads in under 5 seconds&amp;hellip;
    On dialup.</summary>
  </entry>
  <entry>
    <author>
      <name>Mark</name>
      <uri>http://diveintomark.org/</uri>
    </author>
    <title>Accessibility is a harsh mistress</title>
    <link rel='alternate' type='text/html'
      href='http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress'/>
    <id>tag:diveintomark.org,2009-03-21:/archives/20090321200928</id>
    <updated>2009-03-22T01:05:37Z</updated>
    <published>2009-03-21T20:09:28Z</published>
    <category scheme='http://diveintomark.org' term='accessibility'/>
    <summary type='html'>The accessibility orthodoxy does not permit people to
      question the value of features that are rarely useful and rarely used.</summary>
  </entry>
  <entry>
    <author>
      <name>Mark</name>
    </author>
    <title>A gentle introduction to video encoding, part 1: container formats</title>
    <link rel='alternate' type='text/html'
      href='http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats'/>
    <id>tag:diveintomark.org,2008-12-18:/archives/20081218155422</id>
    <updated>2009-01-11T19:39:22Z</updated>
    <published>2008-12-18T15:54:22Z</published>
    <category scheme='http://diveintomark.org' term='asf'/>
    <category scheme='http://diveintomark.org' term='avi'/>
    <category scheme='http://diveintomark.org' term='encoding'/>
    <category scheme='http://diveintomark.org' term='flv'/>
    <category scheme='http://diveintomark.org' term='GIVE'/>
    <category scheme='http://diveintomark.org' term='mp4'/>
    <category scheme='http://diveintomark.org' term='ogg'/>
    <category scheme='http://diveintomark.org' term='video'/>
    <summary type='html'>These notes will eventually become part of a
      tech talk on video encoding.</summary>
  </entry>
</feed>

12.2 5-минутный ускоренный курс в XML

Если Вы уже знакомы с XML, то можете пропустить этот раздел.

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

<foo>   ①
</foo>  ②
  1. Строка 1. Это открывающий (начальный) тег элемента foo.
  2. Строка 2. Это соответствующий закрывающий (конечный) тег элемента foo. Как и в математике и языках программирования у каждой открывающей скобки должна быть соответствующая закрывающая скобка, в XML каждый открывающий тег должен быть закрыт соответствующим закрывающим тегом.

Элементы могут быть вложены друг в друга, при этом глубина вложения не ограничена. Так как элемент bar вложен в элемент foo, то его называют подэлементом или дочерним элементом элемента foo.

<foo>
  <bar></bar>
</foo>

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

<foo></foo>
<bar></bar>

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

<foo lang='en'>                          ①
  <bar id='papayawhip' lang="fr"></bar>  ②
</foo>
  1. Строка 1. Элемент foo имеет один атрибут с именем lang. Значение атрибута lang – это строка en.
  2. Строка 2. Элемент bar имеет два атрибута, их имена id и lang. Значение атрибута lang – это fr. Это не приводит к конфликту с атрибутом lang элемента foo, так как каждый элемент имеет свой набор атрибутов.

Если элемент имеет более одного атрибута, то порядок атрибутов не имеет значения. Атрибуты элемента формируют неупорядоченный набор ключей и значений подобно словарям в Python. Для каждого элемента можно указать неограниченное число атрибутов.

Элементы могут иметь текстовый контент.

<foo lang='en'>
  <bar lang='fr'>PapayaWhip</bar>
</foo>

Элементы, которые не содержат текста и дочерних элементов называются пустыми.

<foo></foo>

Для записи пустых элементов существует сокращенный вариант. Поместив слеш (/) в конце открывающего тега, вы можете опустить закрывающий тег. XML документ из предыдущего примера может быть записан следующим образом:

<foo/>

Как функции в Python могут быть объявлены в разных модулях, XML элементы могут быть объявлены в разных пространствах имён (namespace). Пространства имён обычно выглядят как URL. Для объявления пространства имён по умолчанию используется директива xmlns. Объявление пространства имён очень похоже на атрибут, но у него другое назначение.

<feed xmlns='http://www.w3.org/2005/Atom'>  ①
  <title>dive into mark</title>             ②
</feed>
  1. Строка 1. Элемент feed находится в пространстве имён http://www.w3.org/2005/Atom.
  2. Строка 2. Элемент title также находится в пространстве имён http://www.w3.org/2005/Atom. Пространство имён применяется и к элементу, в котором оно было определено, и ко всем его дочерним элементам.

Вы также можете использовать объявление xmlns:prefix, чтобы определить пространство имен и назначить ему префикс. Тогда каждый элемент в данном пространстве имён должен быть явно объявлен с указанием этого префикса.

<atom:feed xmlns:atom='http://www.w3.org/2005/Atom'>  ①
  <atom:title>dive into mark</atom:title>             ②
</atom:feed>
  1. Строка 1. Элемент feed находится в пространстве имён http://www.w3.org/2005/Atom.
  2. Строка 2. Элемент title также находится в пространстве имён http://www.w3.org/2005/Atom.

С точки зрения синтаксического анализатора XML, предыдущие два XML документа идентичны. Пара «пространство имён» + «имя элемента» задают идентичность XML. Префиксы используются только для ссылки на пространство имён, но не изменяют имени атрибута, поэтому данное имя префикса (atom:) не имеет значения. Если пространства имён совпадают, имена элементов совпадают, атрибуты (или их отсутствие) совпадают, и текстовый контент элементов совпадает, то XML документы одинаковы.

И, наконец, XML документы могут содержать информацию о кодировке символов в первой строке до корневого элемента (если вам интересно как документ может содержать информацию, которая должна быть известна до анализа документа, то для разрешения уловки 22 смотрите раздел F спецификации XML).

<?xml version='1.0' encoding='utf-8'?>

Теперь вы знаете об XML достаточно, чтобы «вынести» следующие разделы главы!

12.3 Структура фида Atom

Рассмотрим блог или любой сайт с часто обновляемым контентом, например CNN.com. Сайт содержит заголовок («CNN.com»), подзаголовок («Breaking News, U.S., World, Weather, Entertainment & Video News»), дату последнего изменения («updated 12:43 p.m. EDT, Sat May 16, 2009») и список статей, опубликованных в разное время. Каждая статья, в свою очередь, также имеет заголовок, дату первой публикации (и, возможно, дату последнего обновления, в случае если статья была изменена, или исправлены опечатки) и уникальный URL.

Формат объединения Atom был разработан, чтобы хранить информацию подобного рода стандартным образом. Мой блог и CNN.com абсолютно разные по дизайну, содержанию и аудитории, но они оба имеют сходную структуру. У CNN.com есть заголовок, и у моего блога тоже есть заголовок. CNN.com публикует статьи, и я публикую статьи.

На верхнем уровне находится корневой элемент, который должен быть у каждого фида Atom: элемент feed из пространства имен http://www.w3.org/2005/Atom.

<feed xmlns='http://www.w3.org/2005/Atom'  ①
      xml:lang='en'>                       ②
  1. Строка 1. http://www.w3.org/2005/Atom – это пространство имён Atom
  2. Строка 2. Каждый элемент может содержать атрибут xml:lang, который определяет язык элемента и его дочерних элементов. В данном случае атрибут xml:lang, объявленный в корневом элементе, задаёт английский язык для всего фида.

Фид Atom содержит дополнительную информацию о себе в дочерних элементах корневого элемента feed:

<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into mark</title>                                             ①
  <subtitle>currently between addictions</subtitle>                         ②
  <id>tag:diveintomark.org,2001-07-29:/</id>                                ③
  <updated>2009-03-27T21:56:07Z</updated>                                   ④
  <link rel='alternate' type='text/html' href='http://diveintomark.org/'/>  ⑤
  1. Строка 2. Заголовок title содержит текст 'dive into mark'.
  2. Строка 3. Подзаголовок subtitle фида – это строка 'currently between addictions'.
  3. Строка 4. Каждый фид должен иметь глобальный уникальный идентификатор. RFC 4151 содержит информацию, как создавать такие идентификаторы.
  4. Строка 5. Данный фид был обновлён последний раз 27 марта 2009 в 21:56 GMT. Обычно элемент updated эквивалентен дате последнего изменения последней статьи на сайте.
  5. Строка 6. А вот здесь начинается самое интересное. Элемент ссылки link не имеет текстового контента, но имеет три атрибута: rel, type и href. Значение атрибута rel говорит о типе ссылки; rel='alternate' значит, что это ссылка для альтернативного представления данного фида. Атрибут type='text/html' означает, что это ссылка на HTML страницу. И, собственно, путь ссылки содержится в атрибуте href.

Теперь мы знаем, что представленный выше фид получен с сайта «dive into mark». Сайт доступен по адресу http://diveintomark.org/ и последний раз был обновлён 27 марта 2009.

Хотя в некоторых XML документах порядок элементов может иметь значение, в фидах Atom порядок элементов не важен.

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

<entry>
  <author>                                                                 ①
    <name>Mark</name>
    <uri>http://diveintomark.org/</uri>
  </author>
  <title>Dive into history, 2009 edition</title>                           ②
  <link rel='alternate' type='text/html'                                   ③
    href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>
  <id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id>        ④
  <updated>2009-03-27T21:56:07Z</updated>                                  ⑤
  <published>2009-03-27T17:20:42Z</published>        
  <category scheme='http://diveintomark.org' term='diveintopython'/>       ⑥
  <category scheme='http://diveintomark.org' term='docbook'/>
  <category scheme='http://diveintomark.org' term='html'/>
  <summary type='html'>Putting an entire chapter on one page sounds        ⑦
    bloated, but consider this &amp;mdash; my longest chapter so far
    would be 75 printed pages, and it loads in under 5 seconds&amp;hellip;
    On dialup.</summary>
</entry>                                                                   ⑧
  1. Строка 2. Элемент author сообщает о том, кто написал статью: некий парень по имени Mark, которого вы можете найти бездельничающего на http://diveintomark.org/. (В данном случае ссылка на сайт автора совпадает с альтернативной ссылкой в метаданных фида, но это не всегда так, поскольку многие блоги имеют несколько авторов, у каждого из которых есть свой сайт.)
  2. Строка 6. Элемент title содержит заголовок статьи «Dive into history, 2009 edition».
  3. Строка 7. Как и с альтернативной ссылкой на фид, в элементе link находится адрес HTML версии данной статьи.
  4. Строка 9. Элемент entry, как и фид, имеет уникальный идентификатор.
  5. Строка 10. Элемент entry содержит две даты: дату первой публикации (published) и дату последнего изменения (updated).
  6. Строка 12. Элементы entry могут иметь произвольное количество категорий (элементов category). Рассматриваемая статья попадёт в категории diveintopython, docbook и html.
  7. Строка 15. Элемент summary даёт краткий обзор статьи. (Бывает также не представленный здесь элемент содержания content, предназначенный для включения в фид полного текста статьи.) Данный элемент summary содержит специфичный для фидов Atom атрибут type='html', указывающий, что содержимое элемента представлено в формате HTML, а не в виде простого текста. Это важно, так как HTML-объекты (&mdash; и &hellip;), присутствующие в элементе должны отображаться как «» и «», а не печататься «как есть».
  8. Строка 19. И, наконец, закрывающий тег элемента entry говорит о конце метаданных для данной статьи.

12.4 Синтаксический разбор XML

В Python документы XML могут быть обработаны несколькими способами. Язык имеет традиционные парсеры DOM и SAX, но я сфокусируюсь на другой библиотеке под названием ElementTree.

Скачать файл feed.xml.

>>> import xml.etree.ElementTree as etree    ①
>>> tree = etree.parse('examples/feed.xml')  ②
>>> root = tree.getroot()                    ③
>>> root                                     ④
<Element {http://www.w3.org/2005/Atom}feed at cd1eb0>
  1. Строка 1. Модуль ElementTree входит в стандартную библиотеку Python и находится в xml.etree.ElementTree.
  2. Строка 2. Основная точка входа в библиотеку ElementTree – это функция parse(), которая принимает имя файла или файлоподобный объект. Данная функция выполняет синтаксический анализ документа за раз. Если памяти недостаточно, то есть способы для поэтапного анализа XML-документа.
  3. Строка 3. Функция parse() возвращает объект, представляющий весь документ. Однако объект treeне является корневым элементом. Чтобы получить ссылку на корневой элемент, необходимо вызвать метод getroot().
  4. Строка 4. Как и следовало ожидать, корневой элемент – это элемент feed в пространстве имён http://www.w3.org/2005/Atom. Строковое представление объекта root ещё раз подчёркивает важный момент: XML элемент – это комбинация пространства имён и его имени тега (так же называемого локальным именем). Каждый элемент в данном документе находится в пространстве Atom, поэтому корневой элемент представлен как {http://www.w3.org/2005/Atom}feed.

Модуль ElementTree всегда представляет элементы XML как '{пространство имён}локальное имя'. Вам неоднократно предстоит увидеть и использовать этот формат при использовании API ElementTree.

12.4.1 Элементы – это списки

В API ElementTree элемент действует как встроенный тип Python, список. А элементы списка – это дочерние XML элементы.

# продолжение предыдущего примера
>>> root.tag                        ①
'{http://www.w3.org/2005/Atom}feed'
>>> len(root)                       ②
8
>>> for child in root:              ③
...   print(child)                  ④
... 
<Element {http://www.w3.org/2005/Atom}title at e2b5d0>
<Element {http://www.w3.org/2005/Atom}subtitle at e2b4e0>
<Element {http://www.w3.org/2005/Atom}id at e2b6c0>
<Element {http://www.w3.org/2005/Atom}updated at e2b6f0>
<Element {http://www.w3.org/2005/Atom}link at e2b4b0>
<Element {http://www.w3.org/2005/Atom}entry at e2b720>
<Element {http://www.w3.org/2005/Atom}entry at e2b510>
<Element {http://www.w3.org/2005/Atom}entry at e2b750>
  1. Строка 2. Продолжим предыдущий пример, корневой элемент – это {http://www.w3.org/2005/Atom}feed.
  2. Строка 4. «Длина» корневого элемента root равна количеству дочерних элементов.
  3. Строка 6. Вы можете использовать элемент как итератор, чтобы пройтись по всем дочерним элементам.
  4. Строка 7. Из вывода видно, что в элементе root 8 дочерних элементов: 5 элементов с метаданными о фиде (title, subtitle, id, updated и link) и 3 элемента entry со статьями.

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

12.4.2 Атрибуты – это словари

XML – это не просто набор элементов; каждый элемент также имеет собственный набор атрибутов. Имея ссылку на конкретный XML элемент, вы можете легко получить его атрибуты в виде словаря Python.

# продолжение предыдущего примера
>>> root.attrib                           ①
{'{http://www.w3.org/XML/1998/namespace}lang': 'en'}
>>> root[4]                               ②
<Element {http://www.w3.org/2005/Atom}link at e181b0>
>>> root[4].attrib                        ③
{'href': 'http://diveintomark.org/',
 'type': 'text/html',
 'rel': 'alternate'}
>>> root[3]                               ④
<Element {http://www.w3.org/2005/Atom}updated at e2b4e0>
>>> root[3].attrib                        ⑤
{}
  1. Строка 2. Свойство attrib представляет собой словарь атрибутов элемента. Исходная разметка XML была следующей <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>. Префикс xml: ссылается на стандартное пространство имён, которое любой XML документ может использовать без объявления.
  2. Строка 4. Пятый дочерний элемент (с индексом [4], так как списки Python начинаются с 0) – это элемент link.
  3. Строка 6. Элемент link имеет три атрибута href, type и rel.
  4. Строка 10. Четвёртый дочерний элемент (с индексом [3] в списке, начинающемся с 0) – это элемент updated.
  5. Строка 12. Элемент updated не имеет атрибутов, следовательно, свойство .attrib – это просто пустой словарь.

12.5 Поиск узлов в XML документе

До настоящего момента мы работали с XML документом «сверху вниз», начиная с корневого элемента, затем получая его дочерние элементы, и так далее через весь документ. Однако во многих случаях при работе с XML вам необходимо искать конкретные элементы. etree справится и с этой задачей.

>>> import xml.etree.ElementTree as etree
>>> tree = etree.parse('examples/feed.xml')
>>> root = tree.getroot()
>>> root.findall('{http://www.w3.org/2005/Atom}entry')    ①
[<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b510>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b540>]
>>> root.tag
'{http://www.w3.org/2005/Atom}feed'
>>> root.findall('{http://www.w3.org/2005/Atom}feed')     ②
[]
>>> root.findall('{http://www.w3.org/2005/Atom}author')   ③
[]
  1. Строка 4. Метод findall() выполняет поиск дочерних элементов удовлетворяющих конкретному запросу (формат этого запроса рассматривается ниже).
  2. Строка 10. Метод findall() есть у всех элементов (включая корневой и дочерние). Он ищет среди дочерних все элементы, соответствующие запросу. Почему он ничего не нашел? Хотя это может показаться неочевидным, данный запрос ищет только среди дочерних элементов. Так как корневой элемент feed не имеет дочерних элементов с именем feed, то запрос возвращает пустой список.
  3. Строка 12. Этот результат также может вас удивить. В данном XML документе есть элемент author; на самом деле, их даже три (по одному в каждом элементе entry). Но эти элементы author не являются непосредственными дочерними элементами корневого элемента; они – как бы «внуки» (дочерние элементы дочернего элемента). Если вам нужно найти элементы author на любом уровне вложенности, то вы можете это сделать, но формат запроса будет немного отличаться.
>>> tree.findall('{http://www.w3.org/2005/Atom}entry')    ①
[<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b510>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b540>]
>>> tree.findall('{http://www.w3.org/2005/Atom}author')   ②
[]
  1. Строка 1. Для удобства объект tree (который возвращается функцией etree.parse()) имеет несколько методов, идентичных методам корневого элемента. Результаты будут такими же, как и при вызове метода tree.getroot().findall().
  2. Строка 5. Возможно, для вас это может оказаться сюрпризом, но данный запрос не находит элементов author в данном документе. Почему же? Потому что, этот вызов идентичен вызову tree.getroot().findall('{http://www.w3.org/2005/Atom}author'), что значит, «найти все элементы author, которые являются дочерними элементами корневого элемента». Элементы author не являются дочерними для корневого элемента; они дочерние элементы элементов entry. Таким образом, при выполнении запроса совпадений не найдено.

Помимо метода findall() есть метод find(), который возвращает первый найденный элемент. Это может быть полезно, когда вы ожидаете только одно совпадение, или, если есть несколько совпадений, но вам важен только первый найденных элементов.

>>> entries = tree.findall('{http://www.w3.org/2005/Atom}entry')           ①
>>> len(entries)
3
>>> title_element = entries[0].find('{http://www.w3.org/2005/Atom}title')  ②
>>> title_element.text
'Dive into history, 2009 edition'
>>> foo_element = entries[0].find('{http://www.w3.org/2005/Atom}foo')      ③
>>> foo_element
>>> type(foo_element)
<class 'NoneType'>
  1. Строка 1. Как вы видели в предыдущем примере, findall() возвращает список всех элементов atom:entry.
  2. Строка 4. Метод find() принимает запрос ElementTree и возвращает первый элемент, удовлетворяющий этому запросу.
  3. Строка 7. В элементе foo отсутствуют дочерние элементы, поэтому find() возвращает объект None.

Здесь необходимо отметить «подводный камень» при использовании метода find(). В логическом контексте объекты элементов ElementTree, не содержащие дочерних элементов, равны значению False (т.е. if len(element) вычисляется как 0). Это значит, что код if element.find('...') проверяет не то, что нашёл ли метод find() удовлетворяющий запросу элемент; этот код проверяет, содержит ли найденный элемент дочерние элементы! Чтобы проверить, нашёл ли метод find() элемент, необходимо использовать if element.find('...') is not None.

Рассмотрим поиск внутри элементов-потомков, т.е. дочерних элементов, дочерних элементов уже дочерних элементов («внуков») и так далее, элементов любого уровня вложенности.

>>> all_links = tree.findall('//{http://www.w3.org/2005/Atom}link')  ①
>>> all_links
[<Element {http://www.w3.org/2005/Atom}link at e181b0>,
 <Element {http://www.w3.org/2005/Atom}link at e2b570>,
 <Element {http://www.w3.org/2005/Atom}link at e2b480>,
 <Element {http://www.w3.org/2005/Atom}link at e2b5a0>]
>>> all_links[0].attrib                                              ②
{'href': 'http://diveintomark.org/',
 'type': 'text/html',
 'rel': 'alternate'}
>>> all_links[1].attrib                                              ③
{'href': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
 'type': 'text/html',
 'rel': 'alternate'}
>>> all_links[2].attrib
{'href': 'http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress',
 'type': 'text/html',
 'rel': 'alternate'}
>>> all_links[3].attrib
{'href': 'http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats',
 'type': 'text/html',
 'rel': 'alternate'}
  1. Строка 1. Этот запрос – //{http://www.w3.org/2005/Atom}link – очень похож на запросы из предыдущих примеров, он отличается только двумя слешами // в начале запроса. Эти два слеша означают: «искать не только непосредственных дочерних элементов; я хочу найти все элементы независимо от уровня вложенности». Поэтому метод возвращает список из четырёх элементов link, а не из одного.
  2. Строка 7. Первый элемент в результате – это непосредственный дочерний элемент корневого элемента. Как мы видим из его атрибутов, это альтернативная ссылка уровня элемента feed, которая указывает на HTML версию веб-сайта, который описывается фидом.
  3. Строка 11. Остальные три элемента в результате – это альтернативные ссылки уровня элементов entry. Каждый из элементов entry имеет по одному дочернему элементу link. А так как в начале запроса findall() находился двойной слеш, то этот запрос нашел их всех.

В целом, метод findall() библиотеки ElementTree – это довольно мощный инструмент поиска, однако формат запроса может быть немного непредсказуем. Официально формат запросов ElementTree описан как «ограниченная поддержка выражений XPath». XPath – это стандарт W3C для построения запросов к элементам XML документа. Язык запросов ElementTree при выполнении простого поиска достаточно похож на XPath, но он и отличается от него настолько, что может начать раздражать, если вы уже знаете XPath. Теперь давайте рассмотрим сторонние библиотеки XML, позволяющие расширить API ElementTree до полной поддержки стандарта XPath.

12.6 Продолжаем работать с lxml

lxml – это сторонняя библиотека с открытым исходным кодом, построенная на базе популярного синтаксического анализатора libxml2. Она обеспечивает стопроцентную совместимость с API ElementTree, полностью поддерживает XPath 1.0 и имеет несколько других приятных фишек. Для Windows доступен установщик; пользователям Linux следует проверить наличие скомпилированных пакетов в репозиториях дистрибутива с помощью, например, yum или apt-get. В противном случае вам придётся установить lxml вручную.

>>> from lxml import etree                   ①
>>> tree = etree.parse('examples/feed.xml')  ②
>>> root = tree.getroot()                    ③
>>> root.findall('{http://www.w3.org/2005/Atom}entry')  ④
[<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b510>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b540>]
  1. Строка 1. При импорте lxml предоставляет абсолютно такой же API, как встроенная библиотека ElementTree.
  2. Строка 2. Функция parse() такая же, как в ElementTree.
  3. Строка 3. Метод getroot() тоже такой же.
  4. Строка 4. Метод findall() точно такой же.

При обработке больших XML документов lxml значительно быстрее, чем встроенная библиотека ElementTree. Если вы используете только API ElementTree и хотите, чтобы обработка выполнялась как можно быстрее, то можно попробовать импортировать библиотеку lxml и, в случае её отсутствия, использовать встроенную ElementTree.

try:
    from lxml import etree
except ImportError:
    import xml.etree.ElementTree as etree

Однако библиотека lxml не только быстрее, чем ElementTree. Ее метод findall() поддерживает более сложные выражения.

>>> import lxml.etree                                                                   ①
>>> tree = lxml.etree.parse('examples/feed.xml')
>>> tree.findall('//{http://www.w3.org/2005/Atom}*[@href]')                             ②
[<Element {http://www.w3.org/2005/Atom}link at eeb8a0>,
 <Element {http://www.w3.org/2005/Atom}link at eeb990>,
 <Element {http://www.w3.org/2005/Atom}link at eeb960>,
 <Element {http://www.w3.org/2005/Atom}link at eeb9c0>]
>>> tree.findall("//{http://www.w3.org/2005/Atom}*[@href='http://diveintomark.org/']")  ③
[<Element {http://www.w3.org/2005/Atom}link at eeb930>]
>>> NS = '{http://www.w3.org/2005/Atom}'
>>> tree.findall('//{NS}author[{NS}uri]'.format(NS=NS))                                 ④
[<Element {http://www.w3.org/2005/Atom}author at eeba80>,
 <Element {http://www.w3.org/2005/Atom}author at eebba0>]
  1. Строка 1. В данном примере я импортирую lxml.etree (вместо предыдущего способа: from lxml import etree), чтобы подчеркнуть, что описываемые возможности характерны именно для lxml.
  2. Строка 3. Этот запрос найдёт все элементы в пространстве имён Atom (любой вложенности), которые имеют атрибут href. Двойной слеш // в начале запроса означает «элементы любой вложенности (а не только дочерние элементы корневого элемента)». {http://www.w3.org/2005/Atom} означает «только элементы пространства имён Atom». Символ * значит «элементы с любым локальным именем». И [@href] означает «элемент имеет атрибут href».
  3. Строка 8. Запрос находит все элементы Atom с атрибутом href, значение которого равно http://diveintomark.org/.
  4. Строка 11. После небольшого форматирования строки (иначе составные запросы становятся неимоверно длинными) данный запрос ищет элементы Atom author, имеющие дочерние элементы Atom uri. Запрос возвращает только 2 элемента author: в первом и во втором элементах entry. В последнем элементе entry элемент author содержит только name, uri у него нет.

Вам недостаточно? lxml также имеет встроенную поддержку для выражений XPath 1.0. Мы не будем детально рассматривать синтаксис XPath, так как это тема для отдельной книги. Но мы рассмотрим пример использования XPath в lxml.

>>> import lxml.etree
>>> tree = lxml.etree.parse('examples/feed.xml')
>>> NSMAP = {'atom': 'http://www.w3.org/2005/Atom'}                    ①
>>> entries = tree.xpath("//atom:category[@term='accessibility']/..",  ②
...     namespaces=NSMAP)
>>> entries                                                            ③
[<Element {http://www.w3.org/2005/Atom}entry at e2b630>]
>>> entry = entries[0]
>>> entry.xpath('./atom:title/text()', namespaces=NSMAP)               ④
['Accessibility is a harsh mistress']
  1. Строка 3. Чтобы выполнить XPath запрос элементов из пространства имён, необходимо определить отображение префикса этого пространства имен. На самом деле это обычный словарь Python.
  2. Строка 4. А вот и XPath запрос. Данное выражение выполняет поиск элементов category (пространства имён Atom), содержащие атрибут term со значением accessibility. Но это не совсем то, что возвращает запрос. Посмотрите в самый конец строки запроса. Вы заметили символы /..? Это означает, «а затем верни родительский элемент элемента category, которого ты только что нашел». Таким образом, одним XPath запросом мы найдём все элементы entry с дочерними элементами <category term='accessibility'>.
  3. Строка 6. Функция xpath() возвращает список объектов ElementTree. В данном документе всего один элемент entry с дочерним элементом category, у которого атрибут term равен значению accessibility.
  4. Строка 9. Выражение XPath не всегда возвращает список элементов. Формально, DOM разобранного документа XML не содержит элементов, она содержит узлы (node). В зависимости от типа узлы могут быть элементами, атрибутами или даже текстовым контентом. Результатом запроса XPath всегда является список узлов. Данный запрос возвращает список текстовых узлов: текстовый контент (text()) элемента title (atom:title), который является дочерним элементом текущего элемента (./).

12.7 Создание XML

Поддержка XML в Python не ограничивается только парсингом существующих документов. Вы также можете создавать XML документы «с нуля».

>>> import xml.etree.ElementTree as etree
>>> new_feed = etree.Element('{http://www.w3.org/2005/Atom}feed',     ①
...     attrib={'{http://www.w3.org/XML/1998/namespace}lang': 'en'})  ②
>>> print(etree.tostring(new_feed))                                   ③
<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>
  1. Строка 2. Для создания нового элемента необходимо создать объект класса Element. В качестве первого аргумента мы передаём имя элемента (пространство имён + локальное имя). Данное выражение создаёт элемент feed в пространстве Atom. Это будет корневой элемент нашего нового XML документа.
  2. Строка 3. Чтобы добавить атрибуты к только что созданному элементу, мы передаём словарь имён атрибутов и их значений во втором аргументе attrib. Обратите внимание, что имена атрибутов должны задаваться в формате ElementTree, {пространство_имён}локальное_имя.
  3. Строка 4. В любой момент вы можете сериализовать элемент и его дочерние элементы с помощью функции tostring() библиотеки ElementTree.

Результат сериализации стал для вас неожиданностью? Формально ElementTree сериализует XML элементы правильно, но не оптимально. Пример XML документа в начале главы определял пространство имен по умолчанию (xmlns='http://www.w3.org/2005/Atom'). Определение пространства по умолчанию полезно для документов (таких как фидов Atom), в которых все элементы принадлежат одному пространству имен, поскольку вы можете объявить пространство один раз, а элементы объявлять, используя только их локальные имена (<feed>, <link>, <entry>). Если вы не собираетесь объявлять элементы из другого пространства имён, то нет необходимости использовать префиксы.

Синтаксический анализатор XML не «заметит» разницы между XML документом с пространством имен по умолчанию и документом, использующим префикс пространства имён перед каждым элементом. Итоговая модель DOM этой сериализации

<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>

что идентична модели DOM этой сериализации

<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>

Единственная разница заключается в том, что второй вариант на несколько символов короче. Если мы переделаем весь наш пример, используя префикс ns0: в каждом открывающем и закрывающем тегах, это добавило бы 4 символа на открывающий тег × 79 тегов + 4 символа на объявление собственно пространства имён, всего 320 символов. В кодировке UTF-8 это составило бы 320 байт. (После архивации gzip разница уменьшается до 21 байта; однако, 21 байт – это всё еще 21 байт). Возможно, для вас это не имеет значения, но для чего-то, подобного фидам Atom, которые могут скачиваться несколько тысяч раз при каждом изменении, выигрыш нескольких байт на одном запросе может быстро разрастись.

Встроенная библиотека ElementTree не предоставляет тонкого управления над сериализацией элементов в пространствах имен. И тут снова в игру вступает lxml.

>>> import lxml.etree
>>> NSMAP = {None: 'http://www.w3.org/2005/Atom'}                     ①
>>> new_feed = lxml.etree.Element('feed', nsmap=NSMAP)                ②
>>> print(lxml.etree.tounicode(new_feed))                             ③
<feed xmlns='http://www.w3.org/2005/Atom'/>
>>> new_feed.set('{http://www.w3.org/XML/1998/namespace}lang', 'en')  ④
>>> print(lxml.etree.tounicode(new_feed))
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>
  1. Строка 2. Для начала определим пространство имён, используя словарь. Значения словаря – это пространства имен; ключи словаря – это задаваемый префикс. Используя объект None в качестве префикса, мы задаем пространство имен по умолчанию.
  2. Строка 3. Теперь, при создании элемента, мы можем передать специфичный для lxml аргумент nsmap, используемый для передачи префиксов пространств имён.
  3. Строка 4. Как и ожидалось, данная сериализация определяет пространство имён по умолчанию Atom и объявляет элемент feed без префикса пространства имён.
  4. Строка 6. Упс, мы забыли добавить атрибут xml:lang. Добавить атрибут к любому элементу всегда можно с помощью метода set(). Он принимает два аргумента: имя атрибута в стандартном формате ElementTree и значение атрибута. (Данный метод не специфичен для библиотеки lxml. Единственная особенность, специфичная для lxml, в данном примере – это аргумент nsmap для управления префиксами пространств имён в сериализованном выводе.)

Разве XML документы ограничиваются только одним элементом в документе? Конечно, нет. Мы так же легко можем создавать дочерние элементы.

>>> title = lxml.etree.SubElement(new_feed, 'title',          ①
...     attrib={'type':'html'})                               ②
>>> print(lxml.etree.tounicode(new_feed))                     ③
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'/></feed>
>>> title.text = 'dive into &hellip;'                         ④
>>> print(lxml.etree.tounicode(new_feed))                     ⑤
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'>dive into &amp;hellip;</title></feed>
>>> print(lxml.etree.tounicode(new_feed, pretty_print=True))  ⑥
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title type='html'>dive into&amp;hellip;</title>
</feed>
  1. Строка 1. Для создания дочернего элемента существующего элемента необходимо создать объект класса SubElement. Обязательные аргументы – это родительский элемент (в данном случае new_feed) и имя нового элемента. Поскольку дочерний элемент наследует пространство имён от родителя, то здесь нет необходимости заново объявлять пространство имён или префикс.
  2. Строка 2. Также вы можете передать словарь с атрибутами. Ключи – это имена атрибутов; значения словаря – это значения атрибутов.
  3. Строка 3. Как и ожидалось, новый элемент title был создан в пространстве Atom, и он является дочерним элементом элемента feed. Так как элемент title не имеет текстового контента и дочерних элементов, то lxml сериализует его как пустой элемент (с помощью сокращенной записи />).
  4. Строка 5. Чтобы добавить текстовый контент в элемент, просто задаём его свойство .text.
  5. Строка 6. Теперь элемент title сериализуется со своим текстовым контентом. Если в тексте содержатся символы «меньше чем» или амперсанды, то при сериализации они должны быть экранированы. lxml обрабатывает это экранирование автоматически.
  6. Строка 8. При сериализации вы можете применить «красивую печать» (pretty_print), при которой вставляются разрывы строки после закрывающих тегов и после открывающих тегов элементов, содержащих дочерние элементы, но не имеющих текстового контента. С технической точки зрения, lxml добавляет «незначащие пробельные символы», чтобы сделать вывод более читаемым.

Вам, возможно, будет интересно попробовать xmlwitch, ещё одну стороннюю библиотеку для создания XML. Она повсеместно использует оператор with, чтобы сделать код создания XML более читаемым.

12.8 Синтаксический анализ «сломанного» XML

Спецификация XML предписывает, что все синтаксические анализаторы XML должны выполнять «драконовскую (строгую) обработку ошибок». То есть, они должны остановиться и выкинуть исключение при обнаружении в XML документе «некорректности» любого типа. Ошибки корректности включают в себя несовпадающие открывающий и закрывающий теги, неопределённые объекты, неправильные символы Юникод и другие эзотерические ситуации. Такая обработка ошибок сильно контрастирует на фоне других известных форматов, например, HTML, – ваш браузер не останавливает отрисовку web-страницы, если вы забыли закрыть HTML тег или экранировать амперсанд в значении атрибута. (Существует распространённое заблуждение, что в HTML не оговорена обработка ошибок. На самом деле, обработка HTML ошибок отлично документирована, но она гораздо сложнее, чем просто «остановиться и выдать аварийное сообщение».)

Некоторые (и я в том числе) считают, что со стороны разработчиков формата XML было ошибкой заставлять так строго обрабатывать ошибки. Не поймите меня неправильно, я, конечно же, за упрощение правил обработки ошибок. Однако на практике понятие «корректности» оказывается коварнее, чем кажется, особенно для XML документов (таких как фиды Atom), которые публикуются в интернете и передаются по протоколу HTTP. Несмотря на зрелость XML, который стандартизовал драконовскую обработку ошибок в 1997, исследования постоянно показывают, что значительная часть фидов Atom в интернете содержат ошибки корректности.

У меня есть и теоретические, и практические причины обрабатывать XML документы «любой ценой», то есть не останавливаться и взрываться при первой ошибке. Если вы окажетесь в похожей ситуации, то lxml может вам помочь.

Ниже приведён фрагмент «битого» XML документа.

<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into &hellip;</title>
...
</feed>

Здесь есть ошибка, так как последовательность &hellip; не определена в формате XML (она определена в HTML). Если попробовать разобрать этот битый фид с настройками по умолчанию, lxml споткнётся на неопределённой последовательности.

>>> import lxml.etree
>>> tree = lxml.etree.parse('examples/feed-broken.xml')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "lxml.etree.pyx", line 2693, in lxml.etree.parse (src/lxml/lxml.etree.c:52591)
  File "parser.pxi", line 1478, in lxml.etree._parseDocument (src/lxml/lxml.etree.c:75665)
  File "parser.pxi", line 1507, in lxml.etree._parseDocumentFromURL (src/lxml/lxml.etree.c:75993)
  File "parser.pxi", line 1407, in lxml.etree._parseDocFromFile (src/lxml/lxml.etree.c:75002)
  File "parser.pxi", line 965, in lxml.etree._BaseParser._parseDocFromFile (src/lxml/lxml.etree.c:72023)
  File "parser.pxi", line 539, in lxml.etree._ParserContext._handleParseResultDoc (src/lxml/lxml.etree.c:67830)
  File "parser.pxi", line 625, in lxml.etree._handleParseResult (src/lxml/lxml.etree.c:68877)
  File "parser.pxi", line 565, in lxml.etree._raiseParseError (src/lxml/lxml.etree.c:68125)
lxml.etree.XMLSyntaxError: Entity 'hellip' not defined, line 3, column 28

Чтобы обработать этот битый XML документ, необходимо создать новый синтаксический анализатор XML.

>>> parser = lxml.etree.XMLParser(recover=True)                  ①
>>> tree = lxml.etree.parse('examples/feed-broken.xml', parser)  ②
>>> parser.error_log                                             ③
examples/feed-broken.xml:3:28:FATAL:PARSER:ERR_UNDECLARED_ENTITY: Entity 'hellip' not defined
>>> tree.findall('{http://www.w3.org/2005/Atom}title')
[<Element {http://www.w3.org/2005/Atom}title at ead510>]
>>> title = tree.findall('{http://www.w3.org/2005/Atom}title')[0]
>>> title.text                                                   ④
'dive into '
>>> print(lxml.etree.tounicode(tree.getroot()))                  ⑤
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into </title>
.
. [rest of serialization snipped for brevity]
.
  1. Строка 1. Чтобы создать новый парсер, инициализируем класс lxml.etree.XMLParser. Он может принимать ряд различных именованных аргументов. В данном случае нас интересует аргумент recover. При присвоении этому аргументу значения True парсер XML будет делать всё, чтобы «восстановить» документ и избавиться от ошибки построения.
  2. Строка 2. Чтобы разобрать XML документ новым анализатором, передаём в функцию parse() объект parser в качестве второго аргумента. Обратите внимание, что lxml не выбрасывает исключение при неопределённой последовательности &hellip;.
  3. Строка 3. Анализатор хранит журнал о найденных ошибках корректности (на самом деле это не зависит от включения восстановления).
  4. Строка 8. Так как анализатор не знает, что делать с неопределённым &hellip;, то он просто тихо отбрасывает его. Текстовый контент элемента title превращается в 'dive into '.
  5. Строка 10. Как видно из сериализации, последовательность &hellip; была просто выброшена.

Важно отметить, что нет никакой гарантии совместимости «восстановления» у XML анализаторов. Другой анализатор может быть умнее и распознать, что &hellip; является корректной последовательностью HTML, и заменить её на &amp;hellip;. «Лучше» ли это? Возможно. Является ли это «более правильным»? Нет, так как оба решения с точки зрения формата XML неверны. Правильное поведение (согласно спецификации XML) – прекратить обработку и выдать исключение. Если же вы решили не следовать спецификации, то вы делаете это на свой страх и риск.

12.9 Материалы для дальнейшего чтения

Источник:

  • Mark Pilgrim. Dive Into Python 3

Теги

lxmlPythonXMLВысокоуровневые языки программированияПарсерПрограммированиеЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.