Обработка SimpleXML с помощью PHP

Опубликовано admin в Втр, 16/11/2010 - 10:06

Библиотека для разметки для обработки XML с помощью PHP

Описание:  Познакомьтесь с расширением SimpleXML, которое объединено с PHP версии 5 и позволяет PHP страницам запрашивать, искать, изменять и переделывать XML в дружественном PHP синтаксисе.

PHP версии 5 представляет SimpleXML, новый интерфейс прикладного программирования (API) для чтения и письма в XML. Расширения SimpleXML, такие как

$doc->rss->channel->item->title

выбирают элементы из документа. Пока вы имеете хорошее представление о структуре вашего документа, писать эти выражения легко. Однако, если вы не знаете точно, где интересующие вас элементы появляются (как например, в случае с Docbook, HTML и другими подобными текстовыми документами), SimpleXML может пользоваться выражениями XPath для поиска этих элементов.

Начало работы с SimpleXML

Представим себе, что вы хотите создать страницу PHP, которая конвертирует канал RSS в HTML код. RSS является основным форматом XML для публикации содержания, взятого из нескольких источников. Корневой элемент этого документа – rss, который содержит единственный элемент channel. Элемент channel содержит метаданные о содержимом, включая его заголовок, язык и URL. Он также содержит разнообразные текстовые элементы, вложенные в элементы item. Каждый элемент item имеет элемент link, содержащий URL или title, или description (обычно оба), в которых находится читаемый текст. Области имен не используются. Конечно, ещё много чего можно сказать об RSS, но для целей этой статьи информации достаточно. Листинг 1 показывает типичный пример с парой информационных сообщений.


Листинг 1. Канал RSS
 
                
<?xml version="1.0" encoding="UTF-8"?>
<rss version="0.92">
<channel>
  <title>Mokka mit Schlag</title>
  <link>http://www.elharo.com/blog</link>
  <language>en</language>
  <item>
    <title>Penn Station: Gone but not Forgotten</title>
    <description>
     The old Penn Station in New York was torn down before I was born. 
     Looking at these pictures, that feels like a mistake.  The current site is 
     functional, but no more; really just some office towers and underground 
     corridors of no particular interest or beauty. The new Madison Square...
    </description>
    <link>http://www.elharo.com/blog/new-york/2006/07/31/penn-station</link>
  </item>
  <item>
    <title>Personal for Elliotte Harold</title>
    <description>Some people use very obnoxious spam filters that require you 
     to type some random string in your subject such as E37T to get through. 
     Needless to say neither I nor most other people bother to communicate with 
     these paranoids. They are grossly overreacting to the spam problem. 
     Personally I won't...</description>
 
    <link>http://www.elharo.com/blog/tech/2006/07/28/personal-for-elliotte-harold/</link>
  </item>
</channel>
</rss>

Давайте сделаем страницу PHP, которая форматирует каждый RSS канал как HTML. Листинг 2 показывает структуру будущей страницы.


Листинг 2. Статическая структура для PHP-кода
 
                
<?php // Загрузите и проанализируйте XML-document ?>
<html xml:lang="en" lang="en">
<head>
  <title><?php // Заголовок будет читаться из RSS ?></title>
</head>
<body>
 
<h1><?php // Заголовок снова будет читаться из RSS ?></h1>
 
<?php
// Здесь мы поместим цикл, чтобы включить заголвок каждого элемента и описание
?>
 
</body>
</html>

Анализ XML-документа

Первый шаг – анализ XML-документа и его сохранение в переменной. Это требует написания всего лишь одной строки кода, которая передает URL функции simplexml_load_file():

 
$rss =  simplexml_load_file('http://partners.userland.com/nytRss/nytHomepage.xml');
            

Предупреждение

Схема, использованная здесь, опасно далека от оптимальной. Мне, на самом деле, не следует загружать и анализировать канал RSS каждый раз, когда на страницу заходят. Это замедляет работу читателей страницы и грозит потенциальным отказом в обслуживании каналов RSS, которые я загружаю, так как большинство из них устанавливают максимальную частоту обновлений приблизительно один раз в час. Реальным решением этой проблемы является помещение в кэш-память или созданной страницы HTML, или каналов RSS или и того и другого. Однако этот вопрос идет в разрез с использованием SimpleXML библиотеки, поэтому я здесь немного приукрашиваю.

Для этого примера я взял страницу из Userland канала New York Times по адресу http://partners.userland.com/nytRss/nytHomepage.xml. Конечно, вы можете использовать вместо этого любой другой URL для другого канала RSS.

Заметьте, что несмотря на имя simplexml_load_file(), этой функции будет необходимо анализировать XML документ на удаленном HTTP URL. Но это не единственная неожиданность в этой функции. Возвращаемое значение функции, которое здесь хранится в переменной $rss, не указывает на весь документ, как вы могли бы ожидать, исходя из опыта работы с другими интерфейсами API, такими, как, например, Объектная модель документа (DOM). Скорее оно указывает на корневой элемент документа. Контент, который находится в прологе и эпилоге документа, недоступен из SimpleXML.

Нахождение имени канала

Имя всего канала (в отличие от заголовков отдельных текстовых фрагментов этого канала) находится в дочернем элементе title от элемента channel, порожденного корневым элементом rss. Вы можете загрузить этот заголовок, как будто бы XML-документ был бы просто последовательной формой объекта класса rss с полем channel, которое в свою очередь имело бы поле title. Используя регулярный PHP-синтаксис ссылки на объект, этот оператор находит заголовок:

 
$title =  $rss->channel->title;

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

 
<title><?php echo $title; ?></title>

Эта строка выводит строковое значение элемента, но не весь элемент. То есть текст записывается, а теги нет.

Вы можете даже полностью пропустить промежуточную переменную $title:

 
<title><?php echo $rss->channel->title; ?></title>

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

Выполнение итераций через элементы

Далее вы должны найти элементы канала. Выражение, которое выполняет это задание очевидно:

 
 $rss->channel->item
 

Однако каналы обычно содержат больше, чем один элемент. Или их даже может не быть вовсе. Соответственно, этот оператор возвращает массив, который вы можете повторить с помощью цикла for-each:

 
foreach ($rss->channel->item as $item) {
  echo "<h2>". $item->title. "</h2>";
  echo "<p>". $item->description. "</p>";
} 

Вы можете легко добавить ссылки, считывая значение элемента link из канала RSS. Просто выведите элемент a из PHP и используйте $item->link, чтобы возвратить URL. Листинг 3 добавляет этот элемент и заполняет структуру из Листинга 1.


Листинг 3. Простая, но полная программа считывания PHP RSS
 
                 
<?php // Load and parse the XML document 
$rss =  simplexml_load_file('http://partners.userland.com/nytRss/nytHomepage.xml');
$title =  $rss->channel->title;
?>
<html xml:lang="en" lang="en">
<head>
  <title><?php echo $title; ?></title>
</head>
<body>
 
<h1><?php echo $title; ?></h1>
 
<?php
// Здесь мы поместим цикл, чтобы включить заголовок элемента и описание
foreach ($rss->channel->item as $item) {
  echo "<h2><a href='". $item->link. "'>". $item->title. "</a></h2>";
  echo "<p>". $item->description. "</p>";
}
?>
 
</body>
</html>
 
                

Это все, что нужно, чтобы написать простую программу чтения RSS в PHP – несколько строк HTML и несколько строк PHP. Не считая пробелов, в общей сложности 20 строк. Конечно, это не самая широкофункциональная, оптимизированная или надежная разработка. Давайте посмотрим, что мы можем сделать, чтобы исправить это.


Обработка ошибок

Не все каналы RSS так хорошо сформированы, как это должно быть. Спецификация XML требует, чтобы процессоры прекратили обрабатывать документы, как только обнаружена формальная ошибка, а SimpleXML соответствовал программе обработки XML. Однако это вам особенно не поможет, когда будет обнаружена ошибка. Как правило, программа записывает предупреждение в файл php-ошибок (но без детального сообщения об ошибке), и функция simplexml-load-file() выдает ошибку. Если вы не уверены, что файл, который вы анализируете хорошо выстроен, проверьте на наличие этой ошибки, перед тем как использовать данные файла, как это показано в Листинге 4.


Листинг 4. Остерегайтесь деформированного ввода данных
 
                
<?php
$rss =  simplexml_load_file('http://www.cafeaulait.org/today.rss');
if ($rss) {
  foreach ($rss->xpath('//title') as $title) {
    echo "<h2>". $title. "</h2>";
  }
}
else {
  echo "Упс! Ввод деформирован!";
}
?>

Другая распространенная ошибка случается, когда документ хорошо сформатирован, но не содержит те элементы, которые вы ожидаете там, где вы ожидаете их найти. Что происходит, например, с таким выражением $doc->rss->channel->item->title, когда элементная группа не имеет заголовка (как это случается, по меньшей мере, с одним из ста наиболее частотных RSS-каналов)? Самый простой подход – всегда обращаться с возвращаемым функцией значением как с массивом данных и заключать его в цикл. В этом случае вы защищены от факта наличия большего или меньшего количества элементов, чем вы ожидали. Однако, если вы знаете, что вы хотите первый элемент в документе, даже если там имеется больше чем один, вы можете запросить его через индекс, начинающийся с нуля. Например, для запроса заголовка первой группы элементов, вы можете написать:

 
$doc->rss->channel->item[0]->title[0]

Если первая группа элементов отсутствует, или не имеет названия, она рассматривается так же, как и любой другой, находящийся за установленными рамками индекс, в массиве PHP. То есть результат равен нулю, который превращается в пустую строку, когда вы попытаетесь вставить его в выходной код HTML.

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


Как работать с пространством имен

Многие сайты сегодня переходят с RSS на Atom. Листинг 5 показывает пример документа в Atom. По многим параметрам этот документ идентичен примеру с RSS. Однако здесь больше метаданных, а корневой элемент - feed, вместо - rss. Элемент feed имеет списки вместо элементов. Элемент content заменяет элемент description. Гораздо важнее то, что документ Atom использует пространство имен, в то время как RSS нет. Таким образом, документ Atom может выводить реальное, не урезанное содержание Extensible HTML (XHTML).


Листинг 5. Документ в Atom
 
                
<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US" 
      xml:base="http://www.cafeconleche.org/today.atom">
  <updated>2006-08-04T16:00:04-04:00</updated>
  <id>http://www.cafeconleche.org/</id>
  <title>Cafe con Leche XML News and Resources</title>
  <link rel="self" type="application/atom+xml" href="/today.atom"/>
  <rights>Copyright 2006 Elliotte Rusty Harold</rights>
  <entry>
    <title>Steve Palmer has posted a beta of Vienna 2.1, an open source 
           RSS/Atom client for Mac OS X. 
          </title>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml" 
          id="August_1_2006_25279" class="2006-08-01T07:01:19Z">
 
<p>
 Steve Palmer has posted a beta of <a shape="rect"
 href="http://www.opencommunity.co.uk/vienna21.php">Vienna
 2.1</a>, an open source RSS/Atom client for Mac OS X. Vienna
 is the first reader I've found acceptable for daily use; not
 great but good enough. (Of course my standards for "good
 enough" are pretty high.) 2.1 focuses on improving the user
 interface with a unified layout that lets you scroll through
 several articles, article filtering (e.g. read all articles
 since the last refresh), manual folder reordering, a new get
 info window, and an improved condensed layout.
</p>
 
</div>
    </content>
    <link href="/#August_1_2006_25279"/>
    <id>http://www.cafeconleche.org/#August_1_2006_25279</id>
    <updated>2006-08-01T07:01:19Z</updated>
  </entry>
  <entry>
    <title>Matt Mullenweg has released Wordpress 2.0.4, 
           a blog engine based on PHP and MySQL.
          </title>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml" 
           id="August_1_2006_21750" class="2006-08-01T06:02:30Z">
 
<p>
 Matt Mullenweg has released <a shape="rect"
 href="http://wordpress.org/development/2006/07/wordpress-204
 /">Wordpress 2.0.4</a>, a blog engine based on PHP and
 MySQL. 2.0.4 plugs various security holes, mostly involving
 plugins.
</p>
</div>
    </content>
    <link href="/#August_1_2006_21750"/>
    <id>http://www.cafeconleche.org/#August_1_2006_21750</id>
    <updated>2006-08-01T06:02:30Z</updated>
  </entry>
 
</feed>
 
                

Хотя имена элементов изменились, основной подход к работе с SimpleXML в документах в Atom такой же, как и с RSS. Единственная разница в том, что вам необходимо указать пространство имен, т.е. универсальный идентификатор ресурса (URI), когда вы запрашиваете элемент с именем, так же как и локальное имя. Это двухступенчатый процесс: во-первых, запросите дочерние элементы в данном пространстве имен, передав пространство имен URI функции children(). Затем запросите элементы с правильным местным именем в этом пространстве имени. Представьте, что вы сначала загрузили канал Atom в переменную $feed, следующим образом:

 
$feed = simplexml_load_file('http://www.cafeconleche.org/today.atom');

Эти две строки теперь находят элемент title:

 
$children =  $feed->children('http://www.w3.org/2005/Atom'); $title = $children->title;
            

Вы можете сжать этот код в единое выражение, если хотите, хотя строка становится немного длинной. Все другие элементы в пространствах имен должны быть проработаны подобным же образом. Листинг 6 показывает полную страницу PHP, которая отображает заголовки из поименованного канала Atom.


Листинг 6. Простой ридер PHP-заголовков Atom
 
                
<?php $feed =  simplexml_load_file('http://www.cafeconleche.org/today.atom');
$children =  $feed->children('http://www.w3.org/2005/Atom');
$title = $children->title;
?>
<html xml:lang="en" lang="en">
<head>
  <title><?php echo $title; ?></title>
</head>
<body>
 
<h1><?php echo $title; ?></h1>
 
<?php
 
$entries = $children->entry;
foreach ($entries as $entry) {
 
  $details = $entry->children('http://www.w3.org/2005/Atom');
  echo "<h2>". $details->title. "</h2>";
}
?>
 
</body>
</html>


Смешанный контент

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

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

 
echo "<p>". $details->content->asXML(). "</p>";

В результате получится что-то вроде Листинга 7.


Листинг 7. Выходные данные XML
 
                
<content type="xhtml">
    <div xmlns="http://www.w3.org/1999/xhtml" 
        id="August_7_2006_31098" class="2006-08-07T09:38:18Z">
    <p>
 Nikolai Grigoriev has released <a shape="rect"
 href="http://www.grigoriev.ru/svgmath">SVGMath 0.3</a>, a
 presentation MathML formatter that produces SVG written in
 pure Python and published under an MIT license. According to
 Grigoriev, "The new version can work with multiple-namespace
 documents (e.g. replace all MathML subtrees with SVG in an
 XSL-FO or XHTML document); configuration is made more
 flexible, and several bugs are fixed. There is also a
 stylesheet to adjust the vertical position of the resulting
 SVG image in XSL-FO."
    </p>
    </div>
  </content>
                

Это не чистый XHTML. Элемент content извлекается из Atom документа, и вам бы, на самом деле, лучше его не иметь. Даже хуже, он проникает в неправильное пространство имени, поэтому он не может быть распознан таким, какой он есть. К счастью на практике этот дополнительный элемент не очень сильно вредит, потому что Web-браузеры просто игнорируют любые теги, которые они не распознают. Законченный документ неисправен, но это не имеет большого значения. Если это действительно вас беспокоит, сведите это на нет с помощью строковых операций, например, следующим образом:

 
  $description = $details->content->asXML();
  $tags = array('<content type="xhtml"'>", "</content>");
  $notags  = array("", "");
  $description = str_replace($tags, $notags, $description);
            

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

 
  // тег конца имеет фиксированную форму, поэтому его легко заменить
  $description = str_replace("</content>", "", $description);
  // удалите тег начала, по возможности включая атрибуты и пробел
  $description = ereg_replace("<content[^>]*>", "", $description);
            

Даже с этой модификацией ваш код может выдавать комментарии, вычислительные команды и отрезки CDATA. Так или иначе, вы срезаете это, хотя боюсь, что это больше не так просто. Смешанный контент просто переходит границы, для работы в которых был разработан SimpleXML.


XPath

Такие выражения как $rss->channel->item->title великолепны, только если вы точно знаете, какие элементы находятся в документе, и где точно они находятся. Однако вы далеко не всегда это знаете. Например, в XHTML элементы в заголовке (h1,h2,h3, и т.д.) могут быть дочерними от body, div, table и от нескольких других элементов. Более того, div, table, blockquote и другие элементы могут быть вложены друг в друга множество раз. Для многих менее определенных случаев использования, легче использовать выражения XPath, такие как //h1 или //h1[contains('Ben')]. SimpleXML имеет этот набор функциональных возможностей через функцию xpath().

Листинг 8 показывает страницу PHP, которая содержит все заголовки в документе RSS – как заголовок самого канала, так и заголовки отдельных групп элементов.


Листинг 8. Использование XPath для нахождения элементов заголовков
 
                
<html xml:lang="en" lang="en">
<head>
  <title>XPath Example</title>
</head>
<body>
 
<?php
$rss =  simplexml_load_file('http://partners.userland.com/nytRss/nytHomepage.xml');
foreach ($rss->xpath('//title') as $title) {
  echo "<h2>". $title. "</h2>";
}
?>
 
</body>
</html>
                

SimpleXML поддерживает только строковые выражения XPath, задающие местонахождении файла и объединения этих выражений. Он не поддерживает выражения XPath, которые не возвращают множества узлов, таких как count(//para) илиcontains(title).

Начиная с PHP версии 5.1, SimpleXML может создавать XPath-запросы относительно документов с пространствами имен. Как обычно в XPath, строковое выражение, задающее местонахождение файла, должно использовать префиксы пространств имен, даже если искомый документ использует пространство имен по умолчанию. Функция registerXPathNamespace() ассоциирует префикс с пространством имени URI для использования в следующем запросе. Например, если вы хотите найти все элементы title в документе, вы используете код подобный представленному в Листинге 9.


Листинг 9. Использование XPath с пространствами имен
 
                
$atom =  simplexml_load_file('http://www.cafeconleche.org/today.atom');
$atom->registerXPathNamespace('atm', 'http://www.w3.org/2005/Atom');
$titles = $atom->xpath('//atm:title');
foreach ($titles as $title) {
  echo "<h2>". $title. "</h2>";
}
                

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


Вывод

SimpleXML является полезным дополнением к инструментарию разработчика PHP при условии, что не нужно работать со смешанным контентом. Это покрывает большое количество случаев использования. Особенно хорошо это работает с простыми данными в виде записей. При условии, что документ не слишком глубок, не слишком сложен и не имеет смешанного содержания, SimpleXML гораздо проще, чем его альтернатива DOM. Он также помогает, если вы знаете структуру документа заранее, хотя использование XPath может помочь существенно облегчить это требование. Отсутствие проверки и поддержки смешанного содержания доставляет неудобства, не всегда препятствует работе. Многие простые форматы не имеют смешанного контента, а во многих случаях использования применяются только очень предсказуемые форматы данных. Если именно это характеризует вашу работу, не откажите себе в удовольствие попробовать SimpleXML. Немного внимания ошибкам и немного усилий, направленных на настройку кэш-памяти, позволяют свести проблемы с эффективностью системы к минимуму, SimpleXML может быть надежным, устойчивым к ошибкам, средством обработки XML в рамках PHP.




Статья взята с http://www.ibm.com (_http://www.ibm.com/developerworks/ru/library/x-simplexml/)