W3docs

PHP XML Expat

Узнайте о парсере Expat в PHP: событийный потоковый разбор XML с обработчиками, чтением атрибутов и проверкой ошибок.

PHP поставляется со встроенным XML-парсером, основанным на библиотеке Expat. В отличие от древовидных парсеров, таких как SimpleXML или DOM, Expat является событийным (или «SAX-подобным») парсером: вместо загрузки всего документа в память в виде дерева он потоково обрабатывает XML и вызывает ваши функции обратного вызова при обнаружении каждого открывающего тега, закрывающего тега и фрагмента текста.

На этой странице объясняется, как работает Expat, когда стоит его использовать, и приводится полный запускаемый пример с обработкой атрибутов и проверкой ошибок.

Что такое парсер Expat?

Expat — это быстрый, легковесный, не валидирующий XML-парсер, написанный на C. Расширение PHP xml оборачивает его через семейство функций xml_parser_*. Его отличительные особенности:

  • Событийный — вы регистрируете функции обратного вызова; парсер вызывает их по мере чтения документа.
  • Потоковый — XML можно подавать частями (xml_parse() можно вызывать многократно), поэтому использование памяти остаётся низким даже для больших файлов.
  • Не валидирующий — он проверяет корректность формата документа, но не валидирует по DTD или схеме.

Это противоположный компромисс по сравнению с древовидным парсером. Древовидный парсер удобен (можно обходить весь документ через $xml->note->message), но держит всё в памяти. Expat сохраняет память на минимальном уровне ценой того, что вам самим приходится отслеживать состояние по мере поступления событий.

Когда использовать Expat?

  • Большие документы — многомегабайтные фиды или экспорты, где построение полного DOM-дерева было бы расточительным.
  • Потоковые источники — данные, поступающие через сокет или частями, когда нельзя дождаться получения всего документа.
  • Извлечение и отброс — нужно лишь несколько значений и не хочется накладных расходов на построение дерева.

Для небольших документов, которые просто нужно прочитать, SimpleXML потребует значительно меньше кода. Сравнение смотрите в разделе Типы XML-парсеров PHP.

Настройка расширения

Расширение xml входит в комплект PHP и по умолчанию включено в большинстве сборок. Если функции парсера отсутствуют, включите его в php.ini:

extension=xml        ; Linux/macOS
extension=php_xml.dll ; Windows

Проверить его доступность во время выполнения можно так:

<?php
var_dump(function_exists('xml_parser_create')); // bool(true)

Полный пример использования Expat

Пример ниже разбирает строку XML и выводит каждое событие. Регистрируются три обработчика — для открывающих тегов, закрывающих тегов и текста — и выполняется надлежащая проверка ошибок. Чтение из строки делает пример самодостаточным; следом приводится вариант с файлом.

<?php
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<note importance="high">
  <to>User</to>
  <from>System</from>
  <message>Hello from Expat</message>
</note>
XML;

// 1. Create the parser.
$parser = xml_parser_create();

// Keep tag names in their original case (Expat upper-cases them by default).
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);

// 2. Register handlers for start/end tags and for character data.
xml_set_element_handler(
    $parser,
    function ($parser, $name, $attrs) {
        echo "Start: $name";
        foreach ($attrs as $key => $value) {
            echo " [$key=$value]";
        }
        echo "\n";
    },
    function ($parser, $name) {
        echo "End:   $name\n";
    }
);

xml_set_character_data_handler($parser, function ($parser, $data) {
    $data = trim($data);           // text between tags includes whitespace
    if ($data !== '') {
        echo "Text:  $data\n";
    }
});

// 3. Feed the document to the parser (true = this is the final chunk).
if (!xml_parse($parser, $xml, true)) {
    $code = xml_get_error_code($parser);
    die(sprintf(
        "XML error: %s at line %d\n",
        xml_error_string($code),
        xml_get_current_line_number($parser)
    ));
}

// 4. Release the parser.
xml_parser_free($parser);

Вывод:

Start: note [importance=high]
Start: to
Text:  User
End:   to
Start: from
Text:  System
End:   from
Start: message
Text:  Hello from Expat
End:   message
End:   note

Как это работает. xml_parser_create() создаёт парсер. xml_set_element_handler() (справочник) регистрирует обратные вызовы для открывающих и закрывающих тегов, а xml_set_character_data_handler() (справочник) регистрирует обратный вызов для текста. Затем xml_parse() управляет всем процессом, вызывая эти функции в порядке появления в документе. Обработчик открывающего тега получает массив $attrs, поэтому чтение атрибута, например importance, выполняется просто как $attrs['importance'].

Два момента, на которых часто спотыкаются:

  • Пробельные символы — это символьные данные. Переносы строк и отступы между тегами также вызывают обработчик символьных данных — именно поэтому в примере вызывается trim() и пустые строки пропускаются.
  • Свёртывание регистра. По умолчанию Expat преобразует имена элементов в верхний регистр. Установка XML_OPTION_CASE_FOLDING в false сохраняет исходный регистр, что почти всегда и нужно.

Разбор файла частями

Главная сила Expat — потоковая обработка. Читайте файл блоками и передавайте каждый блок в xml_parse(), передавая true только в последнем вызове (определяется с помощью feof()):

<?php
$parser = xml_parser_create();
xml_set_element_handler($parser, 'startElement', 'endElement');
xml_set_character_data_handler($parser, 'characterData');

$fp = fopen('example.xml', 'r') or die("Could not open file\n");

while ($data = fread($fp, 4096)) {
    if (!xml_parse($parser, $data, feof($fp))) {
        $code = xml_get_error_code($parser);
        die(sprintf(
            "XML error: %s at line %d\n",
            xml_error_string($code),
            xml_get_current_line_number($parser)
        ));
    }
}

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

function startElement($parser, $name, $attrs) { echo "Start: $name\n"; }
function endElement($parser, $name)           { echo "End:   $name\n"; }
function characterData($parser, $data) {
    $data = trim($data);
    if ($data !== '') echo "Text:  $data\n";
}

Поскольку документ никогда полностью не хранится в памяти, этот подход позволяет обрабатывать файлы, намного превышающие лимит памяти. Обработчиками могут быть обычные имена функций (как здесь), замыкания или вызываемые объекты вида [$object, 'method'] через xml_set_object().

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

Всегда проверяйте возвращаемое значение xml_parse() — при некорректном документе оно возвращает false. Код ошибки, полученный из xml_get_error_code(), можно преобразовать в читаемое сообщение с помощью xml_error_string(), а определить место ошибки можно с помощью xml_get_current_line_number() и xml_get_current_column_number(). Если пропустить эту проверку, сломанный фид завершится без вывода ошибки.

Преимущества Expat

  • Низкое потребление памяти — обрабатывает документ потоково вместо построения дерева, поэтому память остаётся на минимальном уровне независимо от размера файла.
  • Высокая скорость — лежащий в основе C-парсер высоко оптимизирован.
  • Кроссплатформенность — поставляется в комплекте с PHP на всех поддерживаемых ОС.
  • Детальный контроль — вы сами решаете, что делать при каждом событии, игнорируя всё ненужное.

Компромисс: поскольку дерева нет, вам самим придётся отслеживать контекст (в каком элементе вы находитесь). Если эта работа становится сложной, лучшим выбором будет древовидный парсер — SimpleXML или DOM.

Заключение

XML-парсер на основе Expat предоставляет PHP быстрый, экономичный по памяти, событийно-ориентированный способ чтения XML. Зарегистрируйте обработчики для нужных событий, подайте документ через xml_parse(), проверьте ошибки и освободите парсер по завершении. Используйте его для больших или потоковых документов; для небольших SimpleXML обычно является более простым выбором.

Практика

Практика
Каковы характеристики парсера Expat в PHP?
Каковы характеристики парсера Expat в PHP?
Was this page helpful?