Pull to refresh

Comments 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, хранить где-то внутри всех родительские и сестринские элементы, матчить выражения.

А для хранения текущего пути отдельный класс не нужен. Это делается несколькими пользовательскими строками.
UFO just landed and posted this here
UFO just landed and posted this here
Зачем такое создавать таблицу на много байт для того чтобы посчитать два поля, ладно бы я понял если взять xml файлы отсюда fias.nalog.ru/Updates.aspx и их да лучше будет сначала распарсить и занести в базу, потом работать с базой, но код который в этом пост-хабре, поможет прочитать атрибуты у тега, безболезненно)
Sign up to leave a comment.

Articles

Change theme settings