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