Использование 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.

Автор: Евгений Ильин