Доброе время суток!
Сфера применения XML-формата достаточно обширна. Наряду с CSV, JSON и другими, XML — один из самых распространенных способов представить данные для обмена между различными сервисами, программами и сайтами. В качестве примера можно привести формат CommerceML для обмена товарами и заказами между 1С «Управление торговлей» и интернет-магазином.
Поэтому практически всем, кто занимается созданием веб-сервисов, время от времени приходится сталкиваться с необходимостью разбора XML-документов. В своем посте я предлагаю один из методов, как это сделать по возможности наглядно и прозрачно, используя XMLReader.
PHP предлагает несколько способов работы с форматом XML. Не вдаваясь в подробности, скажу, что принципиально их можно разделить на две группы:
Первый способ более понятен на интуитивном уровне, код выглядит прозрачней. Этот способ хорошо подходит для небольших файлов.
Второй способ — это более низкоуровневый подход, что дает нам ряд преимуществ, и вместе с тем несколько омрачает жизнь. Остановимся на нем поподробней. Плюсы:
Но: мы жертвуем читаемостью кода. Если целью нашего парсинга является, скажем, подсчет суммы значений в определенных местах внутри XML с простой структурой, то проблем никаких.
Однако если структура файла сложна, еще работа с данными зависит от полного пути к этим данным, а результат должен включать в себя множество параметров, то здесь мы придем к довольно сумбурному коду.
Поэтому я написал класс, который впоследствии облегчил мне жизнь. Его использование упрощает написание правил и сильно улучшает читаемость программ, их размер становится в разы меньше, а код — красивее.
Основная идея в следующем: и схему нашего XML, и то, как с ней работать, мы будем хранить в одном-единственном массиве, повторяющем иерархию только необходимых нам тегов. Также для любого из тегов в этом же массиве мы сможем прописать нужные нам функции-обработчики открытия тега, его закрытия, чтения атрибутов или чтения текста, либо все вместе. Таким образом, мы храним структуру нашего XML и обработчики в одном месте. Одного взгляда на нашу структуру обработки будет достаточно для того, чтобы понять, что мы делаем с нашим XML-файлом. Оговорюсь, что на простых задачах (как в примерах ниже) преимущество в читаемости невелико, однако оно будет очевидно при работе с файлами относительно сложной структуры — например, форматом обмена с 1С.
Теперь конкретика. Вот наш класс:
Debug-версия (с параметром $debug):
Release-версия (без параметра $debug и комментариев):
Как видите, наш класс расширяет возможности стандартного класса XMLReader, к которому мы добавили один метод:
Параметры:
Аргумент $structure.
Это ассоциативный массив, структура которого повторяет иерархию тегов XML-файла плюс при необходимости в каждом из элементов структуры могут быть функции-обработчики (определены как поля с соответствующим ключом):
Если какой-либо из обработчиков возвратит false, то парсинг прервется, и функция xmlStruct() возвратит false. На приведенных ниже примерах видно, как конструировать аргумент $structure:
Сфера применения XML-формата достаточно обширна. Наряду с CSV, JSON и другими, XML — один из самых распространенных способов представить данные для обмена между различными сервисами, программами и сайтами. В качестве примера можно привести формат CommerceML для обмена товарами и заказами между 1С «Управление торговлей» и интернет-магазином.
Поэтому практически всем, кто занимается созданием веб-сервисов, время от времени приходится сталкиваться с необходимостью разбора XML-документов. В своем посте я предлагаю один из методов, как это сделать по возможности наглядно и прозрачно, используя XMLReader.
PHP предлагает несколько способов работы с форматом XML. Не вдаваясь в подробности, скажу, что принципиально их можно разделить на две группы:
- Загрузка всего XML-документа в память в виде объекта и работа с этим объектом
- Пошаговое чтение XML-строки на уровне тегов, атрибутов и текстового содержимого
Первый способ более понятен на интуитивном уровне, код выглядит прозрачней. Этот способ хорошо подходит для небольших файлов.
Второй способ — это более низкоуровневый подход, что дает нам ряд преимуществ, и вместе с тем несколько омрачает жизнь. Остановимся на нем поподробней. Плюсы:
- Скорость парсинга. Более подробно можете прочитать здесь.
- Потребление меньшего объема оперативной памяти. Мы не храним все данные в виде объекта, весьма затратного по памяти.
Но: мы жертвуем читаемостью кода. Если целью нашего парсинга является, скажем, подсчет суммы значений в определенных местах внутри XML с простой структурой, то проблем никаких.
Однако если структура файла сложна, еще работа с данными зависит от полного пути к этим данным, а результат должен включать в себя множество параметров, то здесь мы придем к довольно сумбурному коду.
Поэтому я написал класс, который впоследствии облегчил мне жизнь. Его использование упрощает написание правил и сильно улучшает читаемость программ, их размер становится в разы меньше, а код — красивее.
Основная идея в следующем: и схему нашего XML, и то, как с ней работать, мы будем хранить в одном-единственном массиве, повторяющем иерархию только необходимых нам тегов. Также для любого из тегов в этом же массиве мы сможем прописать нужные нам функции-обработчики открытия тега, его закрытия, чтения атрибутов или чтения текста, либо все вместе. Таким образом, мы храним структуру нашего XML и обработчики в одном месте. Одного взгляда на нашу структуру обработки будет достаточно для того, чтобы понять, что мы делаем с нашим XML-файлом. Оговорюсь, что на простых задачах (как в примерах ниже) преимущество в читаемости невелико, однако оно будет очевидно при работе с файлами относительно сложной структуры — например, форматом обмена с 1С.
Теперь конкретика. Вот наш класс:
Debug-версия (с параметром $debug):
Класс XMLReaderStruct - кликните, чтобы раскрыть
class XMLReaderStruct extends XMLReader {
public function xmlStruct($xml, $structure, $encoding = null, $options = 0, $debug = false) {
$this->xml($xml, $encoding, $options);
$stack = array();
$node = &$structure;
$skipToDepth = false;
while ($this->read()) {
switch ($this->nodeType) {
case self::ELEMENT:
if ($skipToDepth === false) {
// Если текущая ветка не входит в структуру, то просто игнорируем открытие тегов, иначе смотрим: если текущий узел структуры содержит
// текущий тег, то открываем его, предварительно запоминая в стеке текущую позицию, чтобы при закрытии можно было вернуться. Если
// не содержит, то открываем режим пропуска, пока не встретим закрывающий тег с текущей глубиной.
if (isset($node[$this->name])) {
if ($debug) echo "[ Открытие ]: ",$this->name," - найден в структуре. Спуск по структуре.\r\n";
$stack[$this->depth] = &$node;
$node = &$node[$this->name];
if (isset($node["__open"])) {
if ($debug) echo " Найден обработчик открытия ",$this->name," - выполняю.\r\n";
if (false === $node["__open"]()) return false;
}
if (isset($node["__attrs"])) {
if ($debug) echo " Найден обработчик атрибутов ",$this->name," - выполняю.\r\n";
$attrs = array();
if ($this->hasAttributes)
while ($this->moveToNextAttribute())
$attrs[$this->name] = $this->value;
if (false === $node["__attrs"]($attrs)) return false;
}
if ($this->isEmptyElement) {
if ($debug) echo " Элемент ",$this->name," пустой. Возврат по структуре.\r\n";
if (isset($node["__close"])) {
if ($debug) echo " Найден обработчик закрытия ",$this->name," - выполняю.\r\n";
if (false === $node["__close"]()) return false;
}
$node = &$stack[$this->depth];
}
} else {
$skipToDepth = $this->depth;
if ($debug) echo "[ Открытие ]: ",$this->name," - не найден в структуре. Запуск режима пропуска тегов до достижения вложенности ",$skipToDepth,".\r\n";
}
} else {
if ($debug) echo "( Открытие ): ",$this->name," - в режиме пропуска тегов.\r\n";
}
break;
case self::TEXT:
if ($skipToDepth === false) {
if ($debug) echo "[ Текст ]: ",$this->value," - в структуре.\r\n";
if (isset($node["__text"])) {
if ($debug) echo " Найден обработчик текста - выполняю.\r\n";
if (false === $node["__text"]($this->value)) return false;
}
} else {
if ($debug) echo "( Текст ): ",$this->value," - в режиме пропуска тегов.\r\n";
}
break;
case self::END_ELEMENT:
if ($skipToDepth === false) {
// Если $skipToDepth не установлен, то это значит, что предшествующее ему открытие тега было внутри структуры,
// и поэтому текущий узел структуры надо откатить.
if ($debug) echo "[ Закрытие ]: ",$this->name," - мы в структуре. Подьем по структуре.\r\n";
if (isset($node["__close"])) {
if ($debug) echo " Найден обработчик закрытия ",$this->name," - выполняю.\r\n";
if (false === $node["__close"]()) return false;
}
$node = &$stack[$this->depth];
} elseif ($this->depth === $skipToDepth) {
// Если $skipToDepth установлен, то игнорируем все, что имеет бОльшую глубину, пока не дойдем до закрытие игнора с текущей глубиной.
if ($debug) echo "[ Закрытие ]: ",$this->name," - достигнута вложенность ",$skipToDepth,". Отмена режима пропуска тегов.\r\n";
$skipToDepth = false;
} else {
if ($debug) echo "( Закрытие ): ",$this->name," - в режиме пропуска тегов.\r\n";
}
break;
}
}
return true;
}
}
Release-версия (без параметра $debug и комментариев):
Класс XMLReaderStruct - кликните, чтобы раскрыть
class XMLReaderStruct extends XMLReader {
public function xmlStruct($xml, $structure, $encoding = null, $options = 0) {
$this->xml($xml, $encoding, $options);
$stack = array();
$node = &$structure;
$skipToDepth = false;
while ($this->read()) {
switch ($this->nodeType) {
case self::ELEMENT:
if ($skipToDepth === false) {
if (isset($node[$this->name])) {
$stack[$this->depth] = &$node;
$node = &$node[$this->name];
if (isset($node["__open"]) && (false === $node["__open"]()))
return false;
if (isset($node["__attrs"])) {
$attrs = array();
if ($this->hasAttributes)
while ($this->moveToNextAttribute())
$attrs[$this->name] = $this->value;
if (false === $node["__attrs"]($attrs))
return false;
}
if ($this->isEmptyElement) {
if (isset($node["__close"]) && (false === $node["__close"]()))
return false;
$node = &$stack[$this->depth];
}
} else {
$skipToDepth = $this->depth;
}
}
break;
case self::TEXT:
if ($skipToDepth === false) {
if (isset($node["__text"]) && (false === $node["__text"]($this->value)))
return false;
}
break;
case self::END_ELEMENT:
if ($skipToDepth === false) {
if (isset($node["__close"]) && (false === $node["__close"]()))
return false;
$node = &$stack[$this->depth];
} elseif ($this->depth === $skipToDepth) {
$skipToDepth = false;
}
break;
}
}
return true;
}
}
Как видите, наш класс расширяет возможности стандартного класса XMLReader, к которому мы добавили один метод:
xmlStruct($xml, $structure, $encoding = null, $options = 0, $debug = false)
Параметры:
- $xml, $encoding, $options: как в XMLReader::xml()
- $structure: ассоциативный массив, полностью описывающий то, как мы должны работать с нашим файлом. Подразумевается, что его вид заранее известен, и мы точно знаем, с какими тегами и что мы должны делать.
- $debug: (только для Debug-версии) делать ли вывод отладочной информации (по умолчанию — откл.).
Аргумент $structure.
Это ассоциативный массив, структура которого повторяет иерархию тегов XML-файла плюс при необходимости в каждом из элементов структуры могут быть функции-обработчики (определены как поля с соответствующим ключом):
- "__open" — функция при открытии тега — function()
- "__attrs" — функция для обработки атрибутов тега (при наличии) — function($assocArray)
- "__text" — функция при наличии текстового значения тега — function($text)
- "__close" — функция при закрытии тега — function()
Если какой-либо из обработчиков возвратит false, то парсинг прервется, и функция xmlStruct() возвратит false. На приведенных ниже примерах видно, как конструировать аргумент $structure:
Пример 1, показывающий порядок вызова обработчиков
Пусть есть XML-файл:
Будут вызваны обработчики (в хронологическом порядке):
атрибуты root->a
текстовое поле root->a
открытие root->b
открытие root->b->x
текст root->b->x
закрытие root->b->x
закрытие root->b
Остальные поля обработаны не будут (в т.ч. root->d->x будет проигнорирован, т.к. он вне структуры)
<?xml version="1.0" encoding="UTF-8"?>
<root>
<a attr_1="123" attr_2="456">Abc</a>
<b>
<x>This is node x inside b</x>
</b>
<c></c>
<d>
<x>This is node x inside d</x>
</d>
<e></e>
</root>
$structure = array(
'root' => array(
'a' => array(
"__attrs" => function($array) { echo "ATTR ARRAY IS ",json_encode($array),"\r\n"; },
"__text" => function($text) use (&$a) { echo "TEXT a {$text}\r\n"; }
),
'b' => array(
"__open" => function() { echo "OPEN b\r\n"; },
"__close" => function() { echo "CLOSE b\r\n"; },
'x' => array(
"__open" => function() { echo "OPEN x\r\n"; },
"__text" => function($text) { echo "TEXT x {$text}\r\n"; },
"__close" => function() { echo "CLOSE x\r\n"; }
)
)
)
);
$xmlReaderStruct->xmlStruct($xml, $structure);
Будут вызваны обработчики (в хронологическом порядке):
атрибуты root->a
текстовое поле root->a
открытие root->b
открытие root->b->x
текст root->b->x
закрытие root->b->x
закрытие root->b
Остальные поля обработаны не будут (в т.ч. root->d->x будет проигнорирован, т.к. он вне структуры)
Пример 2, иллюстрирующий простую практическую задачу
Пусть есть XML-файл:
Это некий кассовый чек с товарами и услугами.
Каждая запись чека содержит идентификатор записи, тип (товар «product» или услуга «service»), наименование, количество и цена.
Задача: посчитать сумму чека, но раздельно по товарам и услугам.
<?xml version="1.0" encoding="UTF-8"?>
<shop>
<record>
<id>0</id>
<type>product</type>
<name>Some product name. ID:0</name>
<qty>0</qty>
<price>0</price>
</record>
<record>
<id>1</id>
<type>service</type>
<name>Some product name. ID:1</name>
<qty>1</qty>
<price>15</price>
</record>
<record>
<id>2</id>
<type>product</type>
<name>Some product name. ID:2</name>
<qty>2</qty>
<price>30</price>
</record>
<record>
<id>3</id>
<type>service</type>
<name>Some product name. ID:3</name>
<qty>3</qty>
<price>45</price>
</record>
<record>
<id>4</id>
<type>product</type>
<name>Some product name. ID:4</name>
<qty>4</qty>
<price>60</price>
</record>
<record>
<id>5</id>
<type>service</type>
<name>Some product name. ID:5</name>
<qty>5</qty>
<price>75</price>
</record>
</shop>
Это некий кассовый чек с товарами и услугами.
Каждая запись чека содержит идентификатор записи, тип (товар «product» или услуга «service»), наименование, количество и цена.
Задача: посчитать сумму чека, но раздельно по товарам и услугам.
include_once "xmlreaderstruct.class.php";
$x = new XMLReaderStruct();
$productsSum = 0;
$servicesSum = 0;
$structure = array(
'shop' => array(
'record' => array(
'type' => array( "__text" => function($text) use (&$currentRecord) {
$currentRecord['isService'] = $text === 'service';
} ),
'qty' => array( "__text" => function($text) use (&$currentRecord) {
$currentRecord['qty'] = (int)$text;
} ),
'price' => array( "__text" => function($text) use (&$currentRecord) {
$currentRecord['price'] = (int)$text;
} ),
'__open' => function() use (&$currentRecord) {
$currentRecord = array();
},
'__close' => function() use (&$currentRecord, &$productsSum, &$servicesSum) {
$money = $currentRecord['qty'] * $currentRecord['price'];
if ($currentRecord['isService']) $servicesSum += $money;
else $productsSum += $money;
}
)
)
);
$x->xmlStruct(file_get_contents('example.xml'), $structure);
echo 'Overal products price: ', $productsSum, ', Overal services price: ', $servicesSum;