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 обычно является более простым выбором.