Все ленты — последние статьи

Использование XML в PHP

 

Наш план такой. Сначала мы узнаем, какие функции есть для работы с XML в PHP и как ими пользоваться. Чтобы это лучше понять, мы рассмотрим небольшой скрипт, который будет отображать структуру нашего XML-документа.

Приступим. Не хочу я нудно и долго рассказывать общие слова про то, как работать с XML в PHP, лучше давайте разберем это все на примере. Итак, постановка задачи: написать скрипт, который будет показывать структуру XML-документа. В примерах это файл xml.php.

Сначала создадим XML-документ (в примерах это test.xml). Пусть в этом файле будут описываться фотографии. Особо мудрить мы не будем, и обойдемся без описания DTD (не путать с DDT :)). Здесь появляется первая неприятная особенность PHP: XML-документы, которые должны обрабатываться из скрипта могут буть написаны в следующих кодировках: US-ASCII, ISO-8859-1 и UTF-8. Т.к. нам нужно описывать фотографии по-русски, то придется выбрать последнюю кодировку, т.к. в первых друх нет русских букв. Не все текстовые редакторы могут работать с этой кодировкой. Я, например, набирал XML в редакторе SciTE. Он маленький, бесплатный и у него хорошая подсветка синтаксиса (в том числе PHP и XML). Наш XML-документ будет выглядеть так:

 

<?xml version="1.0" encoding="UTF-8"?>
<album>
<foto smallfoto="Fotos/1smallvelo.jpg " bigfoto="Fotos/1bigvelo.jpg ">
<title>Название 1</title>
<comment>Длинный комментарий
на несколько строк 1</comment>
<date>26.05.2003</date>
<color/>
<detailed>0</detailed>
</foto>
<foto smallfoto="Fotos/smallbardak.jpg " bigfoto="Fotos/bigbardak.jpg ">
<title>Название 2</title>
<comment> Длинный комментарий
на несколько строк 2</comment>
<date>27.05.2003</date>
<color/>
<detailed>1</detailed>
</foto>
</album>

"Физический" смысл тегов в XML сейчас значения не имеет (хотя там вроде и так все понятно). Единственное, что только <color/> здесь может обозначать цветная фотка или нет. Это здесь только для примера тега, у которого нет закрывающегося.

А теперь напишем скрипт, который показывал бы структуру XML-документа. Для работы с XML в PHP есть больше 20 функций. Рассмотрим для начала самые необходимые. Вот этот скрипт:

<?
$xmlfilename = "test.xml";
$code = "UTF-8"; // Кодировка xml-а
$curcode = "Windows-1251"; // Текущая кодировка

$level = 0; // Уровень вложенности
$list = array(); // Список элементов в xml-файле

// Преобразует строку из Unicode
function encoding ($str)
{
global $code;
global $curcode;

$str = mb_convert_encoding($str, $curcode, $code);
return $str;
}

function drawspace()
{
global $level;
for ($i = 0; $i < $level * 10; $i++)
{
echo " ";
}
}

// Обрабатывает текст между тегами
function characterhandler ($parser, $data)
{
global $code;
global $curcode;

drawspace();
$data = encoding($data, $curcode, $code);
$data = trim($data)."<br>";
echo $data;
}

// Обрабатывает открывающиеся теги
function starthandler ($parser, $name, $attribs)
{
global $level;
global $list;

global $code;
global $curcode;

$name = encoding($name, $curcode, $code);
$list[] = $name;
drawspace();
echo "<<font color='blue' size='+1'>$name</font>";
foreach ($attribs as $atname => $val)
{
echo encoding("$atname => $val");
}
echo "><br>";
$level++;
}

// Обрабатывает закрывающиеся теги
function endhandler ($parser, $name)
{
global $level;
global $list;
array_pop($list);
$level--;
drawspace();
echo "<<font color='blue' size='+1'>/$name</font>><p>";
}

// Создадим парсер
$parser = xml_parser_create($code);
if (!$parser)
{
exit ("Не могу создать парсер");
}
else
{
echo "Парсер успешно создан<p>";
}

// Установим обработчики тегов и текста между ними
xml_set_element_handler($parser, 'starthandler', 'endhandler');
xml_set_character_data_handler($parser, 'characterhandler');

// Откроем файл с xml
$fp = fopen ($xmlfilename, "r");
if (!$fp)
{
xml_parser_free($parser);
exit("Не могу открыть файл");
}

while ($data = fread($fp, 4096))
{
if (!xml_parse($parser, $data, feof($fp)))
{
die(sprintf("Ошибочка вышла: %s в строке %d",
xml_error_string(xml_get_error_code($parser)),
xml_get_current_line_number($parser)));
}
}

fclose ($fp);
xml_parser_free($parser);
?>

После объявлений вспомогательных функций, необходимо в первую очередь создать парсер. Это можно сделать одной из функциий xml_parser_create или xml_parser_create_ns. Первая имеет один необязательный параметр, который обозначает кодировку, в которой написан XML-документ. Если его не указать, то по-умолчанию считается, что он написан как ISO-8859-1. Но, как я писал выше, это нам не подходит и мы выбирает UTF-8. Т.к. обозначение этой кодировки нам еще понадобится, то вынесем ее в глобальную переменную ($code = "UTF-8";). Также вынесем туда кодировку, в которой будет выводиться текст в браузер ($curcode = "Windows-1251";). Функция xml_parser_create_ns имеет дополнительный (тоже необязательный) параметр, который обозначает символ, которым в документе будут разделяться пространства имен. Т.к. нам сейчас это не надо, то мы воспользовались первой функцией. Если парсер создан успешно, то паременная $parser получит значение, отличное от нуля.

После этого надо указать парсеру XML, какие функции вызывать при появлении в тексте тегов XML. В нашем примере это сделано так:

// Установим обработчики тегов и текста между ними
xml_set_element_handler($parser, 'starthandler', 'endhandler');
xml_set_character_data_handler($parser, 'characterhandler');

Функция xml_set_element_handler устанавливает обработчики для открывающихся и закрывающихся тегов. В качестве первого параметра им передается парсер, который мы создали до этого. А в качестве второго и третьего — имена функций, которые будут вызываться по мере того, как будут попадаться открывающиеся и закрывающиеся тего соответственно. Эти функции должны быть определены определенным образом. Функция для открывающихся тегов должна выглядеть примерно так:

// Обрабатывает открывающиеся теги
function starthandler ($parser, $name, $attribs)
{
}

При ее вызове ей передаются парсер, который мы создали, имя обрабатываемого тега и его атрибуты (то, что находится в угловых скобках после имени). Если с именем никаких особенностей нет, то атрибуты передаются как ассоциативный массив, т.е. в виде ключ => значение. Поэтому мы их и обрабатываем следующим образом:

foreach ($attribs as $atname => $val)
{
echo encoding("$atname => $val");
}

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

function endhandler ($parser, $name)
{
}

Тут есть одна интересная деталь. Даже если у тега нет закрывающегося, то вторая функция все-равно вызывается. Если Вы посмотрите на работу скрипта, то увидите, что для тега <color/> у нас получилось:

<COLOR>
</COLOR>

А чтобы обрабатывать текст, который располагается между тегами, надо установить соответствующий обработчик функцией xml_set_character_data_handler. Ей пользоваться точно так же, только ее вторым аргументом должно быть имя функции, которая объявлена таким образом:

function characterhandler ($parser, $data)

То есть так же, как и для закрывающегося тега. Именно в нее передаются все данные наподобие "Название 1" или "Длинный комментарий на несколько строк 2" из нашего примера. Ну и, наконец, самое главное — как читать XML-документ. Оказывается просто — как обычный текстовый файл. Т.е. открываем его функцией fopen, например так:

$fp = fopen ($xmlfilename, "r");

И читаем из него все строки, которые потом передаем в функцию xml_parse:

while ($data = fread($fp, 4096))
{
if (!xml_parse($parser, $data, feof($fp)))
{
die(sprintf("Ошибочка вышла: %s в строке %d",
xml_error_string(xml_get_error_code($parser)),
xml_get_current_line_number($parser)));
}
}

У xml_parse три аргумента. Первый — переменная созданного нами раньше парсера, второй — прочитанная строка, а третий (необязательный) — признак того, что пора заканчивать парсить (вот мы туда и передаем значение того, кончился ли файл). У нас еще вставлена проверка ошибок. Там вроде все ясно из названия. xml_get_error_code возвращает код ошибки, по которому xml_error_string создает строку, которая описывает эту ошибку.

После всего этого надо не забыть уничтожить парсер. Это делается функцией xml_parser_free:

xml_parser_free($parser);

Теперь одна из самых неприятных особенностей. Т.к. мы писали XML как Unicode, то и строки нам передаются в той же кодировке. А так как обычно сайт строят на более привычной кодировке (Koi8, Windows), то с этим Unicod'ом надо что-то делать. И вот здесь начинается самое неприятное. В расширении PHP, которое отвечает за XML, есть две функции для перекодировки UTF-8. Это функция utf8_decode, которая преобразует строку из UTF-8, и функция utf8_encode, которая наоборот преобразует в UTF-8. Но они нам не подходят по той причине, что могут работать с кодировкой ISO-8859-1, в которой нет русских букв. К счастью, разработчики PHP все-таки сделали функции, которые могут буз проблем работать и с другими кодировками — это mb_convert_encoding. В данном случае мы ее использовали так:

$str = mb_convert_encoding($str, $curcode, $code);

$curcode и $code это переменные, в которых храняться названия кодировок (помните, мы их раньше объявили глобальными?). С этой функцией все понятно: первый аргумент — это исходная строка, второй — название кодировки, в которую преобразуем, а третий аргумент (необязательный) — кодировка, из которой преобразуем. Функция возвращает нам новую строку. Казалось бы, что все хорошо, есть функция, она здорово работает (это действительно так), но, чтобы она работала, надо, чтобы было подключено расширение к PHP — mbstring (multi byte string). Для этого, если вы работаете из Windows, в файле php.ini надо раскомментировать строку extension=php_mbstring.dll. Но если дома это сделать несложно, то вот на хостинге, где расположен Ваш сайт, оно (расширение) может быть не подключено. Именно поэтому я вынес перекодировку в отдельную функцию, чтобы ее можно было легко исправить:

// Преобразует строку из Unicode
function encoding ($str)
{
global $code;
global $curcode;

$str = mb_convert_encoding($str, $curcode, $code);
return $str;
}

Если у Вас есть идеи насчет того, как обойтись без mb_convert_encoding — пишите мне

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

Теперь давайте немного побалуемся и посмотрим, как работает обработка ошибок. Уберем из тега color слеш. То есть оставим <color>, как будто мы забыли его закрыть. И вот что нам выдает PHP: "Ошибочка вышла: mismatched tag в строке 16". И на этом обработка прекращается. Также "mismatched tag" будет, если мы перенесем закрывающийся тег <data/> после тега <foto/>.

Поиграемся с кодировками. Если сохранить наш XML-документ в кодировке Windows-1251 и честно это указать в заголовке <?xml version="1.0" encoding="Windows-1251"?> (не забудьте исправить соответствующую глобальную переменную в скрипте), то PHP… благополучно вылетает :) По крайней мере, так было у меня. Я этот скрипт испытывал на такой конфигурации: Win2000 + SP3; Apache 1.3.27; PHP 4.3.1.