Доброе время суток!
Сфера применения 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;
