Как стать автором
Обновить

Комментарии 30

XML должен умереть, как формат плохо читаемый человеком, очень редундантный и требующий ненужный код для своей обработки.

рассмотрим как выглядели бы данные из примера в JSON, или например как принято в Питоне, просто в виде массива, потому что поля постоянные:

array = [
[product, Some product name. ID:0, 0,0,],
[service, Some product name. ID:1, 1 , 15],
[product, Some product name. ID:2, 2 , 30],
...]

fields = ['type' , 'name' , 'gtg', 'price']

поле 'id' может по умолчанию быть положением элемента в массиве.


в таком формате данные легко парсятся стандартными средствама практически любого нормального языка.

тогда подсчитать сумму можно лаконично в одну строку виде оператора эквивалентного select- where

summ_products = summ(array[[0] == 'product' ,3 ])

xml хорош самодокументированностью.
Например, что означает «30» в вашем json?
[product, Some product name. ID:2, 2, 30]
Сделайте в JSON документ вложенностью 5 уровней и потом приходите рассказывайте, про плохую читаемость человеком.

в таком формате данные легко парсятся стандартными средствама практически любого нормального языка.

Сделайте вложенность побольше. Потом сходите возьмите openapi и сделайте описание по его стандарту. Ой у нас получился xml.
Абсолютно согласен с тем, что XML — не всегда оптимальный вариант представления информации, но зачастую мы можем работать только с тем, что имеем по условию задачи, например, при работе со сторонним API и т.д.
Мне кажется создать классы под структуру было бы более удобно, — тогда бы работал автокомплит в IDE.
Причем можно сделать генератор классов на базе имеющихся документов. И я подозреваю, что такие инструменты уже должны быть в виде готовых библиотек.
  • $structure: ассоциативный массив, полностью описывающий то, как мы должны работать с нашим файлом. Подразумевается, что его вид заранее известен, и мы точно знаем, с какими тегами и что мы должны делать.

Получается, что структура строго детерминирована и никаких вариантов не предусматривает…
А как насчет того, чтобы не задавать это жестко, а верифицировать XML по XSD и создавать структуру динамически?
Да, но в XSD код не запихнешь, а простая проверка структуры — это уже немного другая задача. Структура получается детерминирована в рамках конкретного вызова, и как правило (99%) известна для конкретной задачи и на мой взгляд позволяет просто и быстро написать обработчик, не срывая сроков.

Хорошее решение. На самом деле, достаточно универсальное. Человек разобрал предметную область проблемы и дал общее решение. Не в каждом коде такое увидишь.

Не лучше ли будет подключить Serializer Component, создать на основе xsd схемы набор иммутабельных классов и потом десериализовать в них?

Да, там будет магия и много рефлексии, но на выходе вы получаете набор классов, с которыми можно работать, добавлять в них свою логику, вычислимые поля и прочее. А потом еще и сохранить через репозиторий в базу данных.

Единственный минус — если слишком большой xml, то все держать в памяти не самое лучшее решение. В таком случае ваше решение будет полезно.

Собственно автор несколько раз написал, что это решение для больших XML-документов.
И беда предложенного вами метода, в контексте больших документов, не столько в памяти, сколько как раз в рефлексии, которая не дешевая.

Можно добавить ещё магии и считывать данные по запросу.
Как разобрать сложный XML-файл и не утонуть в собственном коде

Не увидел у вас сложных xml файлов, а вот ваш класс имеет отвратительный код (вложенность, длина метода, стиль кодирования, мусорный эхо дебаг внутри класса), и еще более сложную в чтении структуру с кэлбеками обработчиками.


ЗЫ длинную запись array уже так давно не видел, спасибо понастальгировал)

Да, я не стал приводить в пример 1С-Bitrix CommerceML2, сберегая время и нервы читателей, хотя для обработки именно 1С-овских файлов написал эту несложную надстройку, что впоследствии сэкономило мне время. Для понимания принципа это посчитал ненужным.
Дебаг оставил намеренно, его убрать несложно — это для тех, кто захочет проверить порядок выполнения.
Постарался реализовать все одним методом, даже в ущерб эстетике, но это критично по производительности.
Добавил Release-вариант без комментариев и $debug. Спасибо.
закрытие root->b->x

нет там закрытия


Пусть есть XML-файл
<?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>

структура неправильная в плане вложенности тегов, и обрабатывается все не правильно

При публикации мое экранирование также превратилось в угловые скобки. Спасибо. Исправил.

Dom если разобраться нормально, нормальный код можно написать.
Класс напомнил php 5.3

Согласен, можно.
Я старался не загонять XML объектом или массивом в память, т.к. он может быть очень большим. Даже у XMLReader::xml() есть недостаток такого плана — строковый аргумент тела xml, хотя и меньший по объему хранения, нежели в виде объекта (на мой взгляд).
Однако и это обходится — можно, например, наш класс надстроить не над XMLReader, а написать аналогичный по функционалу парсер тегов низкого уровня (это не сложно), который будет читать не из строки, а порционно из файла, например.
а чем DomDocument не угодил? там можно даже все не читать при желании, а парсить просто xPath-ом
Код конечно отвратительный. Я удивляюсь почему люди до сих пор не пользуются тайп хинтами для аргументов

Потому, что это не всегда и не всем нужно

Как насчёт такого варианта?
<?php

class XMLReaderStruct extends XMLReader
{
    /**
     * @param string        $source
     * @param               $structure
     * @param string|null   $encoding
     * @param int           $options
     * @param callable|null $debug
     * @return bool
     */
    public function xmlStruct($source, $structure, $encoding = null, $options = 0, $debug = null)
    {
        $this->XML($source, $encoding, $options);
        $stack = [];
        $node = &$structure;
        $skipToDepth = false;
        while ($this->read()) {
            switch ($this->nodeType) {
                case self::ELEMENT:
                    if (false === $this->element($skipToDepth, $node, $stack, $debug)) {
                        return false;
                    }
                    break;
                case self::TEXT:
                    if (false === $this->text($skipToDepth, $node, $debug)) {
                        return false;
                    }
                    break;
                case self::END_ELEMENT:
                    if (false === $this->endElement($skipToDepth, $node, $debug)) {
                        return false;
                    }
                    break;
            }
        }

        return true;
    }

    /**
     * @param $skipToDepth
     * @param $node
     * @param $stack
     * @param $debug
     * @return bool
     */
    private function element(&$skipToDepth, &$node, &$stack, $debug)
    {
        // Если текущая ветка не входит в структуру, то просто игнорируем открытие тегов, иначе смотрим: если текущий узел структуры содержит
        // текущий тег, то открываем его, предварительно запоминая в стеке текущую позицию, чтобы при закрытии можно было вернуться. Если
        // не содержит, то открываем режим пропуска, пока не встретим закрывающий тег с текущей глубиной.

        if ($skipToDepth !== false) {
            if ($debug) {
                $debug("( Открытие ): {$this->name} - в режиме пропуска тегов.");
            }

            return true;
        }

        if (!isset($node[$this->name])) {
            $skipToDepth = $this->depth;
            if ($debug) {
                $debug("[ Открытие ]: {$this->name} - не найден в структуре. Запуск режима пропуска тегов до достижения вложенности {$skipToDepth}.");
            }

            return true;
        }

        if ($debug) {
            $debug("[ Открытие ]: {$this->name} - найден в структуре. Спуск по структуре.");
        }

        $stack[$this->depth] = &$node;
        $node = &$node[$this->name];

        if (isset($node['__open'])) {
            if ($debug) {
                $debug("              Найден обработчик открытия {$this->name} - выполняю.");
            }
            if (false === $node['__open']()) {
                return false;
            }
        }

        if (isset($node['__attrs'])) {
            if ($debug) {
                $debug("              Найден обработчик атрибутов {$this->name} - выполняю.");
            }
            $attrs = [];
            if ($this->hasAttributes) {
                while ($this->moveToNextAttribute()) {
                    $attrs[$this->name] = $this->value;
                }
            }
            if (false === $node['__attrs']($attrs)) {
                return false;
            }
        }

        if (!$this->isEmptyElement) {
            return true;
        }

        if ($debug) {
            $debug("              Элемент {$this->name} пустой. Возврат по структуре.");
        }
        if (isset($node['__close'])) {
            if ($debug) {
                $debug("              Найден обработчик закрытия {$this->name} - выполняю.");
            }
            if (false === $node['__close']()) {
                return false;
            }
        }
        $node = &$stack[$this->depth];

        return true;
    }

    /**
     * @param $skipToDepth
     * @param $node
     * @param $debug
     * @return bool
     */
    private function text(&$skipToDepth, &$node, $debug)
    {
        if ($skipToDepth !== false) {
            if ($debug) {
                $debug("( Текст    ): {$this->value} - в режиме пропуска тегов.");
            }

            return true;
        }

        if ($debug) {
            $debug("[ Текст    ]: {$this->value} - в структуре.");
        }

        if (!isset($node['__text'])) {
            return true;
        }

        if ($debug) {
            $debug('              Найден обработчик текста - выполняю.');
        }

        return (false !== $node['__text']($this->value));
    }

    /**
     * @param $skipToDepth
     * @param $node
     * @param $debug
     * @return bool
     */
    private function endElement(&$skipToDepth, &$node, $debug)
    {

        if (false === $skipToDepth) {
            // Если $skipToDepth не установлен, то это значит, что предшествующее ему открытие тега было внутри структуры,
            // и поэтому текущий узел структуры надо откатить.
            if ($debug) {
                $debug("[ Закрытие ]: {$this->name} - мы в структуре. Подьем по структуре.");
            }
            if (isset($node['__close'])) {
                if ($debug) {
                    $debug("              Найден обработчик закрытия {$this->name} - выполняю.");
                }
                if (false === $node['__close']()) {
                    return false;
                }
            }
            $node = &$stack[$this->depth];

            return true;
        }

        if ($this->depth === $skipToDepth) {
            // Если $skipToDepth установлен, то игнорируем все, что имеет бОльшую глубину, пока не дойдем до закрытие игнора с текущей глубиной.
            if ($debug) {
                $debug("[ Закрытие ]: {$this->name} - достигнута вложенность {$skipToDepth}. Отмена режима пропуска тегов.");
            }
            $skipToDepth = false;

            return true;
        }

        if ($debug) {
            $debug("( Закрытие ): {$this->name} - в режиме пропуска тегов.");
        }

        return true;
    }

}
То есть для XML вложенностью уровней 15 и больше вы предлагаете соорудить соответствующий массив в 15+ уровней глубиной?
Аналога для XPath "//elementName" нет?
Как отлавливать first/nth child?
В __open/__close не передается совсем никакой информации (уровень вложенности, количество предыдущих соседей)?
Класс простой и (по возможности) быстрый. Создать конкуренцию по удобству xPath целью не было. За элегантность всегда приходится платить ресурсами. DOM старался не использовать.
Все обработчики реализуются в зависимости от места в структуре, в котором определены. Это в том числе и уровень вложенности.
Обработчик для n-го дочернего элемента можно реализовать условием в теле обработчика. Возможно, получится органично вкрутить эту логику и в метод xmlStruct, если понадобится, пока не думал над этим, т.к. не надобилось.
На мой взгляд, это интересно для тех, кто в силу экономии ресурсов отказался от DOM.
Это не XPath, это просто путь к элементу. Нужны конструкции в духе:
//someEl/description[contains(./@lang, 'en')]/countryOfOrigin/text()

Желательно с parent и previous-sibling осями. Но это уже все сложно, надо парсить XPath, хранить где-то внутри всех родительские и сестринские элементы, матчить выражения.

А для хранения текущего пути отдельный класс не нужен. Это делается несколькими пользовательскими строками.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Зачем такое создавать таблицу на много байт для того чтобы посчитать два поля, ладно бы я понял если взять xml файлы отсюда fias.nalog.ru/Updates.aspx и их да лучше будет сначала распарсить и занести в базу, потом работать с базой, но код который в этом пост-хабре, поможет прочитать атрибуты у тега, безболезненно)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории