Перейти до вмісту

Пориньте у Python 3/XML

Матеріал з Вікіпідручника

Під час архонства Арістаехмуса Дракон приймав його постанови.
Арістотель

Майже кожен розділ цієї книги обертається навколо шматка демонстраційного коду. Але суть XML не в коді, а в даних. Одним з типових використань XML є "синдикація фідів" які перелічують останні статті блогу, форуму, чи іншого частооновлюваного веб-сайту. Більшість популярних блогових платформ можуть генерувати фід і оновлювати його як тільки нові статті чи обговорення публікуються. Можна читати блог "підписавшись" на його фід, і можна читати кілька блогів за раз, за допомогою спеціальної програми - агрегатора фідів, такої наприклад як Google Reader.

Ось тут дані XML з якими ми будемо працювати в цьому розділі. Це фід, а якщо точніше, то фід Atom.

<?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>


* * *


П'ятихвилинний ввідний курс XML

[ред.]

Якщо ви вже знаєте XML, можете пропустити цей розділ.

XML - це узагальнений спосіб опису даних з ієрархічною структурою. Документ XML містить один чи більше елементів, які обмежуються відкриваючим та закриваючим тегами. Ось завершений (хоча й нудний) документ XML:

<foo>
</foo>

В першому рядку відкриваючий тег, а в другому - відповідний йому закриваючий. Як і дужки при написанні математичних виразів чи коду, кожен відкриваючий тег має бути закритим відповідним закриваючим.

Елементи можна вставляти один в одного довільну кількість разів. Елемент bar всередині елементу foo називається піделементом або дитиною тегу foo.

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

Перший елемент в кожному XML документі називається кореневим. Документ XML може мати лише один кореневий елемент. Внизу ви бачите текст який не є документом XML, тому що він має два кореневі елементи:

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

Елементи можуть мати атрибути, які є парами ключ - значення. Атрибути перелічуються всередині відкриваючого тега елемента і розділяються пропуском. Імена атрибутів не повинні повторюватись в межах одного елемента. Значення атрибутів повинні поміщуватись в лапки. Можна використовувати як одинарні, так і подвійні лапки.

<foo lang='en'>
  <bar id='papayawhip' lang="fr"></bar>
</foo>

Елемент foo має один атрибут який називається lang. Значенням атрибута lang - en. Елемент bar має два атрибути, названі id та lang. Значенням атрибута lang є fr. Це жодним чином не конфліктує з елементом foo, бо кожен елемент має свій власний набір атрибутів.

Порядок атрибутів не має значення. Атрибути елемента утворюють невпорядковану множину пар, на зразок словника мови Python. Немає обмеження на кількість атрибутів які можна описати для кожного елемента.

Елементи можуть мати текстовий вміст.

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

Елементи що не містять тексту і дітей називаються порожніми.

<foo></foo>

Існує скорочення для запису порожніх елементів. Вставляючи символ / наприкінці відкриваючого тегу, ви можете пропустити закриваючий тег. XML з попереднього прикладу може бути записаний наступним чином:

<foo/>

Так як і функції Python можуть бути описаними в різних модулях, елементи XML можуть бути описаними в різних просторах імен. Простори імен зазвичай виглядають як URL. Ви можете використати декларацію xmlns для того щоб задати простір імен за замовчуванням. Ця декларація схожа на атрибут, але вона має іншу мету.

<feed xmlns='http://www.w3.org/2005/Atom'>
  <title>dive into mark</title>
</feed>

Елемент feed знаходиться в просторі імен http://www.w3.org/2005/Atom. Елемент title також знаходиться в просторі імен http://www.w3.org/2005/Atom. Декларація простору імен впливає на елемент в якому вона описується а також на всі дочірні.

Також ви можете використати декларацію xmlns:prefix щоб описати простір імен, і пов'язати його з префіксом prefix. Тепер кожен елемент в тому просторі імен повинен оголошуватись з явним простором імен. Ось так:

<atom:feed xmlns:atom='http://www.w3.org/2005/Atom'>
  <atom:title>dive into mark</atom:title>
</atom:feed>

З точки зору XML парсера попередні два XML документи ідентичні. Простір імен та назва елемента разом дають XML ідентичність. Префікси створені тільки для того щоб посилатись на простори імен, тому саме ім'я префікса (atom:) не має значення. Простори імен співпадають, імена елементів співпадають, атрибути (чи їх відсутність) співпадають, і вміст кожного елемента теж збігається, тому документи XML однакові.

Нарешті, документи XML можуть містити інформацію про кодування в першому рядку, перед кореневим елементом (Якщо вам цікаво як документ може містити інформацію яка повинна бути відомою до того як документ можна буде прочитати, Секція F специфікації XML містить деталі як розв'язати цю пастку 22.)

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

І тепер ви знаєте достатньо XML щоб бути небезпечним.


* * *


Структура фіду 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'>

http://www.w3.org/2005/Atom - задає простір імен Atom.

Будь-який елемент може містити атрибут 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/'/>  

① Заголовок фіду "dive into mark" записується в елементі <title>.

② Підзаголовок, "currently between addictions" - в елементі <subtitle>.

③ Кожен фід повинен мати глобально унікальний ідентифікатор. Дивіться RFC 4151 аби дізнатись як такий створити.

④ Цей фід востаннє був оновлений 27 березня, 2009, в 21:56 за грінвічем. Зазвичай цей час дорівнює часу останнього оновлення найновішої статті.

⑤ Тепер речі стають цікавими. Цей елемент не має текстового вмісту, але має три атрибути: rel, type, and 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>                                                                   

① Елемент author повідомляє хто написав цю статтю: якийсь чувак на ім’я Марк, якого можна знайти за адресою http://diveintomark.org/. (Це така сама адреса як і адреса альтернативного представлення фіду, але не обов’язково повинна бути такою. Багато блогів мають по кілька авторів, кожен з яких може мати власний сайт.)

② Елемент title дає нам заголовок публікації, "Dive into history, 2009 edition".

③ Як і з посиланням на альтернативне представлення фіду, цей елемент link дає нам адресу HTML версії публікації.

④ Публікації, як і фіди, потребують унікального ідентифікатора.

⑤ Публікації мають по дві дати: дату першої публікації (published) та дату останньої зміни (updated).

⑥ Публікації можуть мати довільне число категорій. В цій, наприклад, є категорії: diveintopython, docbook, та html.

⑦ Елемент summary дає нам короткий опис вмісту публікації. (Також існує елемент content, тут не показаний, якщо ви хочете включити повний текст публікації у ваш фід. Цей елемент summary має один специфічний для формату Atom атрибут, type='html', який вказує що даний опис публікації є шматком HTML, а не простим текстом. Це важливо, тому що він містить в собі деякі елементи HTML, такі як (&mdash; та &hellip;) які повинні відображатись як "—" та "…", а не як HTML код.

⑧ Коли ми описали всі дані публікації, потрібно закрити елемент entry.


* * *


Парсинг XML

[ред.]

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

>>> import xml.etree.ElementTree as etree

Бібліотека ElementTree - частина стандартної бібліотеки Python, в пакеті xml.etree.ElementTree.

>>> tree = etree.parse('examples/feed.xml')

Основною точкою входу в бібліотеку ElementTree є функція parse(), яка приймає ім'я файлу, або потоковий об'єкт. Функція парсить ввесь документ за раз. Якщо з пам'яттю туго існують способи парсити XML документ інкрементно.

>>> root = tree.getroot()

Функція parse() повертає об'єкт що являє собою ввесь документ. Це не кореневий елемент. Для того щоб отримати посилання на кореневий елемент - викличте метод getroot().

>>> root
<Element {http://www.w3.org/2005/Atom}feed at cd1eb0>

Як і очікувалось, кореневий елемент - це тег feed у просторі імен http://www.w3.org/2005/Atom. Рядкове представлення цього об'єкта посилює важливу тезу: елемент XML - це комбінація простору імен та назви тегу (також замість тег, іноді кажуть локальне ім'я). Кожен елемент в цьому документі знаходиться у просторі імен Atom, тому кореневий елемент представляється як {http://www.w3.org/2005/Atom}feed.

ElementTree показує елементи XML в форматі {namespace}localname. Ви будете зустрічатись з цим форматом в багатьох місцях в API ElementTree.


Елементи це списки

[ред.]

В API ElementTree, елемент поводиться як список. Елементами списку є його діти.

>>> root.tag
'{http://www.w3.org/2005/Atom}feed'
>>> len(root)
8

Як і в попередньому прикладі, елемент root - це {http://www.w3.org/2005/Atom}feed. "Довжина" елемента - це кількість дочірніх елементів.

>>> 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>

Можна використати елемент як ітератор, для того щоб отримати всі його дочірні елементи. Як можна бачити з виводу, справді є 8 дочірніх елементів: метадані фіду (title, subtitle, id, updated, та link) за якими йдуть три елементи entry.

Ви могли про це самі здогадатись, але я хочу наголосити про це явно: список дочірніх елементів включає лише прямих дітей. Кожен з елементів entry має власних дітей, але вони не включаються в цей список. Їх нема в списку feed, зате вони входять в список дітей кожного елемента entry. Існують способи знаходити елементи не залежно від того як глибоко вони вкладені, і чиїми дітьми є, ми розглянемо два таких способи пізніше в цьому розділі.

Атрибути це словники

[ред.]

XML це не просто набір елементів, це також набір атрибутів цих елементів. Як тільки ми маємо певний елемент, ми можемо просто отримати його атрибути як словник Python.

>>> root.attrib
{'{http://www.w3.org/XML/1998/namespace}lang': 'en'}

Поле attrib - це словник атрибутів елемента. Початковою розміткою було <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>. Префікс xml: вказує на вбудований прострі імен, який кожен XML документ може використовувати без декларування

>>> root[4]
<Element {http://www.w3.org/2005/Atom}link at e181b0>
>>> root[4].attrib
{'href': 'http://diveintomark.org/',
 'type': 'text/html',
 'rel': 'alternate'}

П’ятою дитиною ([4], пам’ятаєте, нумерація в списках починається з нуля) є елемент link. Він має три атрибути: href, type, та rel.

>>> root[3]
<Element {http://www.w3.org/2005/Atom}updated at e2b4e0>
>>> root[3].attrib
{}

Четвертою дитиною є елемент updated. Він не має атрибутів, тому .attrib - порожній словник.


* * *


Пошук вузлів 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>]

Метод findall() знаходить всі дочірні елементи які відповідають певному запиту. (Про формат цього запиту ви дізнаєтесь за хвилину.)

>>> root.tag
'{http://www.w3.org/2005/Atom}feed'
>>> root.findall('{http://www.w3.org/2005/Atom}feed')
[]

Кожен елемент - як кореневий, так і його діти, має метод findall(). Цей метод знаходить всі відповідні запиту елементи серед дітей елемента для якого викликається. Але чому нема жодних результатів? Хоча це й не надто очевидно, даний запит шукає лише серед дітей елемента. А так як кореневий елемент feed не має дітей які називаються feed, запит повертає порожній список.

>>> root.findall('{http://www.w3.org/2005/Atom}author')
[]

Цей результат теж може бути несподіваним для вас. В даному документі є елемент author, навіть три (один в кожному елементі entry). Але ті елементи не є прямими дітьми кореневого, вони є "онуками" (дітьми дітей). Якщо ви хочете знайти елементи 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 (повернутий функцією etree.parse()) має кілька методів що відображають методи кореневого елемента. Результати є такими самими, як і при виклику методу tree.getroot().findall().

>>> tree.findall('{http://www.w3.org/2005/Atom}author')
[]

Можливо дещо несподівано, але цей запит не знаходить елементів author в нашому документі. Чому ні? Тому що це просто скорочений запис tree.getroot().findall('{http://www.w3.org/2005/Atom}author'), який означає "знайти всі елементи author які є дітьми елемента root. Елементи author не є дітьми кореневого елементу, вони є дітьми елементів entry. Тому запит не повертає жодних результатів.

Також існує метод find() який повертає перший знайдений елемент. Це корисно у випадках в яких ви очікуєте знайти лише один елемент, або коли хоча елементів може бути кілька, нам потрібен лише перший.

>>> entries = tree.findall('{http://www.w3.org/2005/Atom}entry')
>>> len(entries)
3

Це ви вже бачили в попередньому прикладі. Запит знаходить всі елементи atom:entry.

>>> title_element = entries[0].find('{http://www.w3.org/2005/Atom}title')
>>> title_element.text
'Dive into history, 2009 edition'

Метод find() приймає запит ElementTree та повертає перший елемент що відповідає запиту.

>>> foo_element = entries[0].find('{http://www.w3.org/2005/Atom}foo')
>>> foo_element
>>> type(foo_element)
<class 'NoneType'>

Всередині цього елемента entry немає елемента що називається foo, тому ми отримуємо результат None.

В методі find() існує підводний камінь на який ви можете наткнутися. В булевому контексті, об’єкти елементів ElementTree мають значення False якщо вони не мають дітей (тобто коли len(element) дорівнює нулю). Це означає що 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>]

Цей запит - //{http://www.w3.org/2005/Atom}link — дуже подібний до попередніх прикладів, якщо не брати до уваги двох слешів на початку запиту. Ці два слеші означають "не перебирати лише прямих дітей, перевіряти всі елементи не залежно від рівня вкладеності". Тому результатом є список з чотирьох елементів link, а не тільки з одного.

>>> all_links[0].attrib
{'href': 'http://diveintomark.org/',
 'type': 'text/html',
 'rel': 'alternate'}

Перший результат є дитиною кореневого елемента. Як можна побачити з атрибутів - це посилання на альтернативне представленя фіду, яке вказує на HTML версію сайту що фід описує.

>>> 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'}

Інші три результати є посиланнями на альтернативне представлення для кожної публікації. Кожна публікація містила одне посилання, і завдяки двом слешам на початку запиту, вони всі потрапили до результатів.

Взагалі, метод findall() бібліотеки ElementTree є дуже потужною функцією, але мова запитів може бути дещо незвичною. Вона описується як "обмежена підтримка виразів XPath". XPath є W3C стандартом здійснення запитів до документів XML. Мова запитів ElementTree достатньо подібна на XPath щоб здійснювати базовий пошук, але достатньо відмінна щоб дратувати вас якщо ви вже знаєте XPath. Зараз давайте поглянемо на сторонню XML бібліотеку яка розширює API ElementTree повною підтримкою XPath.


* * *


Йдемо далі з lxml

[ред.]

lxml - стороння бібліотека з відкритим кодом що базується на популярному парсері libxml2. Вона надає стовідсотково сумісне з ElementTree API, та розширює його до повної підтримки XPath 1.0 додаючи ще деякі приємності. Існують інсталятори для Windows. Користувачі Linux повинні пробувати використовувати інструменти свого дистрибутиву, такі як yum чи apt-get щоб встановити попередньо скомпільовані бінарники з репозиторіїв. В решті випадків доведеться встановлювати lxml вручну.

>>> from lxml import etree

Після імпортування lxml надає таке саме API як і вбудована ElementTree.

>>> 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>]

Методи parse(), getroot() та findall() - працюють точно так само як і в ElementTree.

Якщо 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>]

Цей запит знаходить всі елементи документа в просторі імен Atom, які мають атрибут href. // на початку документа означає "елементи всюди (а не лише діти кореневого елемента)", {http://www.w3.org/2005/Atom} означає "лише елементи в просторі імен Atom", * означає "елементи з будь-яким ім’ям". Ну а [@href] означає "мають атрибут href".

>>> tree.findall("//{http://www.w3.org/2005/Atom}*[@href='http://diveintomark.org/']")
[<Element {http://www.w3.org/2005/Atom}link at eeb930>]

Цей запит знаходить всі елементи Atom, які мають атрибут href значення якого дорівнює http://diveintomark.org/.

>>> 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>]

Після того як ми використали деяке форматування рядків (без якого запит виглядає занадто довгим), ми написали запит, який шукає елементи Atom author, які мають дочірній елемент Atom uri. Це поверне нам два елементи author, з першої та другої публікації. Елемент author з трерьої публікації містить лише name, а не uri.

Для вас все ще не достатньо? lxml також включає підтримку довільних виразів XPath 1.0. Я не збираюсь тут заглиблюватись в синтаксис XPath, бо про нього можна написати окрему книгу. Але я покажу вам як він інтегрується в lxml.

>>> import lxml.etree
>>> tree = lxml.etree.parse('examples/feed.xml')
>>> NSMAP = {'atom': 'http://www.w3.org/2005/Atom'}

Щоб виконувати запити XPath над елементами з просторами імен, потрібно описати відображення префіксів в простори імен. Це звичайний словник Python.

>>> entries = tree.xpath("//atom:category[@term='accessibility']/..",
...     namespaces=NSMAP)
>>> entries
[<Element {http://www.w3.org/2005/Atom}entry at e2b630>]

Ось тут ми бачимо запит за допомогою XPath. Цей вираз шукає елементи category (у просторі імен Atom), що містять атрибут term зі значенням accessibility. Але це насправді не буде результатом запиту. Подивіться на кінець виразу, ви помітили частину /..? Це означає "а потім поверніть батьківський елемент елемента category який ми щойно знайшли". Тому цей запит XPath знайде всі публікації що містять дочірній елемент <category term='accessibility'>.

Результатом роботи фукнції xpath() є список об’єктів ElementTree. В даному документі є лише одна публікація що відповідає потрібним критеріям.

>>> entry = entries[0]
>>> entry.xpath('./atom:title/text()', namespaces=NSMAP) 
['Accessibility is a harsh mistress']

Вирази XPath не завжди повертають список елементів. Технічно, DOM-дерево XML-документа не містить елементів, воно містить вузли. Залежно від їх типу, вузли можуть бути елементами, атрибутами, чи навіть текстовим вмістом. Результатом запиту XPath є список вузлів. Цей запит повертає список текстових вузлів: текстовий вміст (text()) елемента title (atom:title) що є дитиною поточного елемента (./).


* * *


Генерація XML

[ред.]

Підтримка XML в Python не обмежується читанням існуючих документів. Ви також можете їх створювати.

>>> 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'})

Щоб створити новий елемент потрібно створити екземпляр класу Element. Ім’я елемента (простір імен та локальне ім’я) можна передати першим елементом. У видеописаному випадку створюється елемент feed в просторі імен Atom. Це буде кореневим елементом нашого документа. Атрибути при створенні елемента передаються у словнику, за допомогою параметра attrib. Зауважте що імена атрибутів повинні бути в стандартному форматі ElementTree: {простір_імен}локальне_ім’я.

>>> print(etree.tostring(new_feed))
<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>

В будь-який момент можна серіалізувати будь-який елемент (разом з дітьми) за допомогою функції бібліотеки ElementTree tostring().

Результат серіалізації став для вас несподіванкою? Спосіб, яким 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: до кожного відкриваючого та закриваючого тега всього документа який давався як приклад на початку розділу, це б додало чотири символи на тег × 79 тегів + 4 символи для задання самого простору імен, що загалом складає 320 символів. Припускаючи кодування UTF-8, це 320 додаткових байтів. (Після стиснення з Gzip різниця падає до 21-го байта, але все одно, 21 байт, це 21 байт). Можливо для вас це й нічого не означає, але для такої штуки як фід Atom, яку можуть завантажувати кілька тисяч разів при кожній зміні, економія кількох байт на один запит може швидко акумулюватись.

Вбудована бібліотека ElementTree не надає можливості тонкого налаштування серіалізації елементів з просторами імен, але lxml надає.

>>> import lxml.etree
>>> NSMAP = {None: 'http://www.w3.org/2005/Atom'}

Для початку, опишіть словник просторів імен. Значеннями словника будуть простори імен, ключами - префікси. Використання None замість префікса задає простір імен за замовчуванянм.

>>> new_feed = lxml.etree.Element('feed', nsmap=NSMAP)

Тепер, при створенні елемента можна передавати специфічний для lxml аргумент nsmap, і lxml не обділить увагою простори імен що ви задали.

>>> print(lxml.etree.tounicode(new_feed))
<feed xmlns='http://www.w3.org/2005/Atom'/>

Як і очікувалось, ця серіалізація опише простір імен Atom, як простір імен за замовчуванням а елемент feed запише без префікса.

>>> 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'/>

Ой, ми забули додати атрибут xml:lang. Але то нічого, атрибути завжди можна додати за допомогою методу set() будь-якого елемента. Він має два аргументи: назву атрибуту в стандартному форматі ElementTree, та значення атрибуту.

Чекайте, хіба документи XML обмежені одним елементом на документ? Ні, звичайно що ні. Ви також можете просто створити дочірні елементи.

>>> title = lxml.etree.SubElement(new_feed, 'title',
...     attrib={'type':'html'})

Щоб створити дочірній елемент для вже існуючого елемента, створіть екземпляр класу SubElement. Обов’язковими аргументами є батьківський елемент (new_feed в нашому прикладі) та ім’я нового елемента. Так як дочірні елементи успадкують словник просторів імен свого батька, нема необхідності передекларовувати префікси просторів імен в цьому конструкторі. Також можна передати словник атрибутів. Ключами є імена атрибутів, значеннями - значення атрибутів.

>>> print(lxml.etree.tounicode(new_feed))
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'/></feed>

Як і очікувалось, новий елемент title був створений в просторі імен Atom, та був вставлений як дитина елементу feed. Так як елемент title не має власного текстового вмісту, lxml серіалізує його як порожній елемент (зі скороченням />).

>>> title.text = 'dive into &hellip;'

Щоб задати елементу текстовий вміст, просто задайте значення атрибуту .text.

>>> 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>

Тепер елемент title серіалізується зі своїм вмістом. Будь-який вміст що містить знаки менше, чи амперсанди повинен екрануватись при серіалізації. lxml проводить таку екранізацію автоматично.

>>> 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>

При серіалізації можна також використати опцію "гарного форматування" (pretty_print), яка додає символи нового рядка після закриваючих тегів, та після відкриваючих тегів елементів які містять дочірні елементи але не текст. В технічних термінах, lxml може додавати незначимі пропуски, щоб зробити вивід більш читабельним.

Можливо ви також захочете оцінити xmlwitch - іншу сторонню бібліотеку для генерації XML. Вона активно використовує конструкцію with, щоб зробити код генерації XML більш читабельним.


* * *


Парсинг пошкодженого XML

[ред.]

Специфікація XML вимагає щоб всі відповідні їй парсери XML застосовували "драконівську обробку помилок". Тобто, вони повинні з шумом припинити роботу, як тільки помітять в XML документі будь-яку помилку оформлення. До помилок оформлення входять неспівпадаючі відкриваючі та закриваючі теги, незадані сутності, недозволені символи юнікоду, та багато інших езотеричних правил. Це різко контрастує з іншими звичними форматами, такими як HTML - ваш браузер не припинить рендеринг веб-сторінки якщо ви забудете закрити 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)

Щоб створити власний парсер створіть екземпляр класу xml.etree.XMLParser. Клас приймає різноманітні іменовані аргументи. Тут ми зацікавлені лише в аргументі recover. Коли йому передати True, парсер XML буде старатись зробити все найкраще аби "відновитись" ("recover") після помилок форматування.

>>> tree = lxml.etree.parse('examples/feed-broken.xml', parser)

Аби відпарсити документ XML вашим парсером, передайте об’єкт parser другим аргументом в функцію parse(). Зауважте що lxml не генерує винятку щодо неописаної сутності &hellip;.

>>> 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 '

Так як він не знав що робити з неописаною сутністю &hellip;, парсер її просто тихенько пропустив. Текстовий вміст елемента title став 'dive into '.

>>> print(lxml.etree.tounicode(tree.getroot()))
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into </title>
.
. [решта серіалізації пропущена для економії місця]
.

Як ви можете бачити з серіалізації, &hellip; справді видалено з документа.

Важливо повторити що між парсерами які "відновлюються" після помилок нема гарантії сумісності. Інший парсер міг вирішити що він розпізнає сутність &hellip; з HTML, і замінити її на &amp;hellip;. Чи це "краще"? Можливо. Чи це "більш правильно"? Ні, вони обоє однаково неправильні. Правильна поведінка (згідно специфікації XML) - аварійно завершити роботу. Якщо ви вирішили цього не робити - тоді ви самі по собі.


* * *


Для подальшого читання

[ред.]

Файли · Серіалізація об'єктів