Обработка LiveJournal RSS стандартными средствами PHP

RSS

RSS — это стандарт XML-документа, разработанный первоначально Netscape, а далее доработанный под патронажем W3C. Задача стандарта — передавать краткую выжимку обновляемой информации в качестве так называемого «канала» (channel). Чаще всего в формате RSS предоставляются последние новости или анонсы информационных материалов. Интерес разработчиков, обращенный к обмену информацией в Рунете, постепенно возрастает, а в зарубежном сегменте Сети данный стандарт уже используется достаточно часто и находит все новых и новых приверженцев.

Для того, чтобы понять, как устроен RSS 2.0, достаточно ознакомиться со спецификацией стандарта и просмотреть примеры. Настоятельно рекомендую ознакомиться с этим документом.

Задача

Ныне популярный сервис LiveJournal предоставляет возможность доступа к RSS-каналам дневников. Именно это и натолкнуло меня на мысль о том, что дневник можно «прикрутить» к любому сайту, даже будучи бесплатным пользователем. Оговорюсь, разработчики Живого Журнала предоставляют относительно удобные средства интегрирования журнала и сайта только платным пользователям.

LiveJournal RSS

http://www.livejournal.com/users/bikman/data/rss — по этому адресу располагается RSS-канал дневника. Конечно, имя пользователя (в данном случае bikman) нужно изменить на то, которое интересует читателя. Я же во всех примерах буду использовать свой жe. Думаю, большой проблемой смена имени пользователя не станет.

Лучшим способом понять, как устроен RSS, автоматически генерируемый Живым Журналом, является анализ примера, приведенного ниже:

<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0">
<channel>
<title>bikman's</title>
<link>http://www.livejournal.com/users/bikman/</link>
<description>bikman's - LiveJournal.com</description>
<lastBuildDate> Wed, 14 Jan 2004 01:20:41 GMT </lastBuildDate>
<generator>LiveJournal / LiveJournal.com</generator>
<image>
  <url>http://userpic.livejournal.com/8412362/1106951</url>
  <title>bikman's</title>
  <link>http://www.livejournal.com/users/bikman/</link>
  <width>100</width>
  <height>100</height>
</image>

<item>
  <guid isPermaLink="true">
	http://www.livejournal.com/users/bikman/94200.html</guid>
  <pubDate> Wed, 14 Jan 2004 01:10:34 GMT </pubDate>
  <title>Заголовок второго поста.</title>
  <link>http://www.livejournal.com/users/bikman/94200.html</link>
  <description>
	Здесь находится текст второго поста.
  </description>
</item>

<item>
  <guid isPermaLink="true">
	http://www.livejournal.com/users/bikman/94200.html</guid>
  <pubDate> Wed, 14 Jan 2004 01:10:34 GMT </pubDate>
  <title>Заголовок второго поста.</title>
  <link>http://www.livejournal.com/users/bikman/94200.html</link>
  <description>
	Здесь находится текст второго поста.
  </description>
</item>
</channel>
</rss>

Согласно стандарту RSS 2.0 элементов <item> может быть сколь угодно много. Я умышленно привожу только два, будучи уверенным, что этого вполне достаточно для понимания устройства данного конкретного документа. Стандартно LiveJournal генерирует 25 элементов <item>.

PHP и XML

Поскольку одним из самых распространенных языков в Сети является PHP, я как раз и опишу, как воспользоваться этим интерпретатором для «сращивания» дневника и сайта.

Широко распространенных способов обработки XML-документов существует два — Event-based APIs и Document Object Model (DOM) APIs. В PHP стандартная поддержка XML организована с помощью Event-based API (основана на событиях). Это диктует принципы кодирования. Я не стану подробно останавливаться на вопросе обработки XML-документов. Полагаю, читателю, знакомому с вопросом, это покажется неинтересным, а для непосвященных в таинства XML, напечатаны тонны книг высоко профессиональных авторов.

Функции PHP предназначенные для обработки XML

  • xml_parser_create()

    Создает экземпляр обработчика XML.

  • xml_set_element_handler(parser, startElementFunction, endElementFunction)

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

    startElementFunction определяет функцию для открывающих тэгов, endElementFunction соответственно для закрывающих. parser — это объект, возвращенный функцией xml_parser_create().

  • xml_set_chracter_data_handler(parser, characterDataFunction)

    Данная функция определяет обратный вызов, осуществляемый при обработке события, связанного с нахождением данных, содержащихся между XML-тэгами. Назначение параметров аналогично xml_set_element_handler().

  • xml_parse(parser, data, endOfDocument)

    Эта функция инициализирует анализ переданного ей XML-кода. Параметр endOfDocument должен быть равен true, если параметр data содержит конец документа, или же false, если data не включает в себя документ до конца. Это позволяет обработчику корректно обрабатывать незавершенные тэги и прочие ошибки форматирования документа, возникающие в связи с тем, что переменная data не может содержать данные длиной более 4-х килобайт.

  • xml_parser_free(parser)

    Функция освобождает память, занятую объектом parser.

Описание данных функций — это всего лишь краткий ликбез. Более подробную информацию по данной теме можно получить на официальном сайте PHP.

Основные принципы и алгоритмы

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

Естественно, сначала стоит создать HTML-страницу, на которой будет отображаться форматированная информация, полученная из RSS-канала.

Сперва создадим класс RSSParser, внутри которого будет выполняться вся работа по разбору XML, и который будет выводить на печать (или же в поток вывода от сервера к браузеру) форматированные HTML-данные.

После создания класса, получим RSS-данные от сервиса LiveJournal и инициализируем обработчик XML, который будет использовать для событийной обработки (Event-based API) класс RSSParser.

Пример реализации

Подробные разъяснения приведены после примера.

<html>

<head>
  <title>Заголовок сайта</title>
</head>

<body>

<?php

class RSSParser {
  var $insideItem = false;
  var $tag = "";
  var $title = "";
  var $description = "";
  var $originalLink = "";
  var $dt = "";

  function startElement($parser, $tagName, $attrs)
  {
	if($this->insideItem)
	{
	  $this->tag = $tagName;
	}
	elseif($tagName == "ITEM")
	{
	  $this->insideItem = true;
	}
  }

  function endElement($parser, $tagName)
  {
	if($tagName == "ITEM")
	{
	  printf("<h2>%s</h2>", $this->title);
	  printf("<h3>%s</h3>", $this->dt);
	  printf("<p>%s</p>", $this->description);
	  printf("<p><a href=\"%s\" target=\"_blank\">%s</a></p>",
			  trim($this->originalLink), " Коментарии ");

	  $this->title = "";
	  $this->originalLink = "";
	  $this->description = "";
	  $this->dt = "";
	  $this->insideItem = false;
	}
  }

  function characterData($parser, $data)
  {
	if($this->insideItem)
	{
	  switch($this->tag)
	  {
		case "TITLE":
		  $this->title .= $data;
		  break;
		case "DESCRIPTION":
		  $this->description .= $data;
		  break;
		case "LINK":
		  $this->originalLink .= $data;
		  break;
		case "PUBDATE":
		  $this->dt .= $data;
		  break;
	  }
	}
  }
}

$xml_parser = xml_parser_create("UTF-8");
$rss_parser = new RSSParser();

xml_set_object($xml_parser, &$rss_parser);
xml_set_element_handler($xml_parser, "startElement", "endElement");
xml_set_character_data_handler($xml_parser, "characterData");

$fp = fopen("http://www.livejournal.com/users/bikman/data/rss", "r")
		   or die("Error reading RSS data!");
while($data = fread($fp, 4096))
{
  xml_parse($xml_parser, $data, feof($fp))
		   or die("Error parsing RSS data!");
}
fclose($fp);
xml_parser_free($xml_parser);
?>

</body>

</html>

Стоит учитывать, что чаще всего XML-документы хранятся в Unicode кодировке UTF-8, а в Рунете наиболее часто используемой кодировкой является Windows-1251. В этом раскладе приходится решать проблему перекодировки, или же достаточно сохранить саму страницу в UTF-8 и указать браузеру, что используется именно эта кодировка.

Для перекодировки следует использовать функции iconv() и mb_convert_encoding(). Поскольку данные функции являются дополнительными для интерпретатора PHP и содержатся в подключаемых модулях, я не могу гарантировать их работоспособность в условиях конкретного сервера и оставляю это на совести читателя, считая, что подал идеи для решения проблемы переокдировки.

Согласно стандарту RSS 2.0 дата в RSS-документах должна храниться в формате, описанном в RFC 822 (Sat, 07 Sep 2002 00:00:01 GMT). Читателя может не устроить данный формат даты, и ему может потребоваться его изменить. Могу предложить вот такой способ.

Вместо:

printf("<h3>%s</h3>", $this->dt);

Сделать так:

printf("<h3>%s</h3>", date("d.m.y - H.i.s", strtotime($this->dt));

Чтобы выбрать любой другой необходимый читателю формат, достаточно изменить строку, передаваемую в функцию date() в качестве формата. За подробными объяснениями следует обратиться к описанию функции на официальном сайте PHP.

Разъяснения

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

Класс RSSParser

Лично я считаю (и не только я), что использование директивы global в скриптах является примером «ленивого» программирования. Поэтому предлагаю реализацию функций обратного вызова на события обработчика XML оформить в виде специального класса RSSParser.

class RSSParser {
var $insideItem = false;
var $tag = "";
var $title = "";
var $description = "";
var $originalLink = "";
var $dt = "";

Класс объявляется стандартной директивой class и содержит несколько свойств. Свойство insideItem нужно для того, чтобы отследить момент, когда обработчик находится внутри тэга <item>. В этот момент оно устанавливается в true.

Остальные свойства отвечают за информацию, которую несет в себе запись в дневнике. Это дата (dt), текст (description) и заголовок (title).

Свойство tag сохраняет в себе тот XML-тэг, внутри которого находится обработчик.

function startElement($parser, $tagName, $attrs)
{
  if($this->insideItem)
  {
	$this->tag = $tagName;
  }
  elseif($tagName == "ITEM")
  {
	$this->insideItem = true;
  }
}

Функция обратного вызова startElement($parser, $tagName, $attrs) начинает работать в тот момент, когда обработчик натыкается на открывающий тэг. Параметр parser — это уже известный объект, созданный функцией xml_parser_create(). Параметр tagName — название того тэга, который анализирует обработчик XML, а параметр attrs — это атрибуты текущего тэга.

Стоит отметить, что имена тэгов в PHP коде должны задаваться только(!) прописными буквами.

function endElement($parser, $tagName)
{
  if($tagName == "ITEM")
  {
	printf("<h2>%s</h2>", $this->title);
	printf("<h3>%s</h3>", $this->dt);
	printf("<p>%s</p>", $this->description);
	printf("<p><a href=\"%s\" target=\"_blank\">%s</a></p>",
	trim($this->originalLink), "Коментарии");

	$this->title = "";
	$this->originalLink = "";
	$this->description = "";
	$this->dt = "";
	$this->insideItem = false;
  }
}

Функция endElement($parser, $tagName) отвечает за обработку события, возникающего при обнаружении закрывающего тэга. Параметр tagName содержит имя этого тэга.

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

function characterData($parser, $data)
{
  if($this->insideItem)
  {
	switch($this->tag)
	{
	  case "TITLE":
		$this->title .= $data;
		break;
	  case "DESCRIPTION":
		$this->description .= $data;
		break;
	  case "LINK":
		$this->originalLink .= $data;
		break;
	  case "PUBDATE":
		$this->dt .= $data;
		break;
	}
  }
}

Функция characterData($parser, $data), как было сказано выше, обрабатывает данные, которые находятся внутри XML-тэгов, присваивая свойствам класса соответствующие значения.

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

Основной код

На мой взгляд, код, выполняющий инициализацию обработчика и получение данных из RSS-канала достаточно тривиален.

$xml_parser = xml_parser_create("UTF-8");
$rss_parser = new RSSParser();

xml_set_object($xml_parser, &$rss_parser);
xml_set_element_handler($xml_parser, "startElement", "endElement");
xml_set_character_data_handler($xml_parser, "characterData");

$fp = fopen("http://www.livejournal.com/users/bikman/data/rss", "r")
			or die("Error reading RSS data!");
while($data = fread($fp, 4096))
{
		xml_parse($xml_parser, $data, feof($fp))
				  or die("Error parsing RSS data!");
}
fclose($fp);
xml_parser_free($xml_parser);

Заключение

На этом я заканчиваю статью, посвященную обработке RSS-каналов, которые автоматически генерирует сервис LiveJournal. Думаю, что мой вариант скрипта можно легко адаптировать под конкретные нужды. Он вполне общий, чтобы не ограничивать фантазию программиста.

 

Примечание:

Хочу выразить благодарность Кэвину Янку (Kevin Yank) за его статью на SitePoint«PHP and XML: Parsing RSS 1.0», которая послужила вдохновением для моих изысканий. Вы можете посмотреть результат работы кода и скачать исходный код.

 


© Дмитрий Бикман 2002—2004