Как получить количество отдельных узлов XML?

У меня возникают проблемы с использованием ссылок в рекурсивных вызовах.

То, что я пытаюсь сделать, – описать XML-документ с точки зрения максимального количества отдельных узлов в соответствующем элементе – не зная ни одного из имен узлов узла заранее.

Рассмотрим этот документ:

<Data> <Record> <SAMPLE> <TITLE>Superior Title</TITLE> <SUBTITLE>Sub Title</SUBTITLE> <AUTH> <FNAME>John</FNAME> <DISPLAY>No</DISPLAY> </AUTH> <AUTH> <FNAME>Jane</FNAME> <DISPLAY>No</DISPLAY> </AUTH> <ABSTRACT/> </SAMPLE> </Record> <Record> <SAMPLE> <TITLE>Interesting Title</TITLE> <AUTH> <FNAME>John</FNAME> <DISPLAY>No</DISPLAY> </AUTH> <ABSTRACT/> </SAMPLE> <SAMPLE> <TITLE>Another Title</TITLE> <AUTH> <FNAME>Jane</FNAME> <DISPLAY>No</DISPLAY> </AUTH> <ABSTRACT/> </SAMPLE> </Record> </Data> 

Вы можете видеть, что Record имеет 1 или 2 узла SAMPLE и что SAMPLE имеет 1 или 2 узла AUTH . Я пытаюсь создать массив, который будет описывать структуру документа в терминах максимального количества отдельных узлов внутри соответствующего узла.

Поэтому я пытаюсь получить такой результат:

 $result = [ "Data" => [ "max_count" => 1, "elements" => [ "Record" => [ "max_count" => 2, "elements" => [ "SAMPLE" => [ "max_count" => 2, "elements" => [ "TITLE" => [ "max_count" => 1 ], "SUBTITLE" => [ "max_count" => 1 ], "AUTH" => [ "max_count" => 2, "elements" => [ "FNAME" => [ "max_count" => 1 ], "DISPLAY" => [ "max_count" => 1 ] ] ], "ABSTRACT" => [ "max_count" => 1 ] ] ] ] ] ] ] ]; 

Чтобы сохранить немного моего здравомыслия, я использую saber / xml, чтобы обработать синтаксический анализ XML.

Я могу получить абсолютное количество элементов, используя рекурсивные вызовы со ссылкой на исходный массив.

  private function countArrayElements(&$array, &$result){ // get collection of subnodes foreach ($array as $node){ $name = $this->stripNamespace($node['name']); // get count of distinct subnodes if (empty($result[$name])){ $result[$name]["max_count"] = 1; } else { $result[$name]["max_count"]++; } if (is_array($node['value'])){ $this->countArrayElements($node['value'], $result[$name]["elements"]); } } } 

Поэтому я рассуждал, что я могу передать массив по ссылке и выполнить сравнение, которое работает для двух верхних узлов, но как-то сбрасывается на последующих узлах, что приводит к подсчету только 1 для узла AUTH .

  private function countArrayElements(&$array, &$previous){ // get collection of subnodes foreach ($array as $node){ $name = $this->stripNamespace($node['name']); // get count of distinct subnodes if (empty($result[$name]["max_count"])){ $result[$name]["max_count"] = 1; } else { $result[$name]["max_count"]++; } // recurse if (is_array($node['value'])){ $result[$name]["elements"] = $this->countArrayElements( $node['value'], $result[$name]["elements"] ); } // compare previous max if (!empty($previous[$name]["max_count"])){ $result[$name]["max_count"] = max( $previous[$name]["max_count"], $result[$name]["max_count"] ); } } return $result; } 

Я понимаю, что это довольно сложный вопрос, из которого это всего лишь небольшая часть гораздо более крупного проекта, поэтому я попытался максимально разрешить это для MCVE, и я дополнительно подготовил специальный репозиторий этих файлов с тестом phpunit.

Хотя ваше решение работает и довольно эффективно, учитывая, что оно работает в O(n*k) времени ( где n – количество узлов в дереве, а k – количество вершин ), я решил, что предлагаю альтернативное решение, которое не полагается на массивы или ссылки и более обобщен для работы не только для XML, но и для любого дерева DOM. Это решение также работает в O(n*k) времени, поэтому оно так же эффективно. Единственное различие заключается в том, что вы можете использовать значения из генератора, не создавая сначала весь массив.

Моделирование проблемы

Самый простой способ понять эту проблему – смоделировать ее как график. Если мы моделируем документ таким образом, что мы получаем уровни и вершины.

DOM tree figure1

Таким образом, это позволяет нам делить и побеждать, разбивая проблему на два разных этапа.

  1. Считайте имена дочерних узлов кардинального дочернего узла заданной вертикали в виде sum ( вершин )
  2. Найдите max sum коллективной sum по горизонтали ( уровням )

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

Дерево DOM figure2

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

Минимальный, полный, проверенный, самосохраняемый пример

Поэтому, чтобы обеспечить минимальный, полный, проверенный и самосогласованный пример, я буду полагаться на расширение DOMDocument PHP, а не на стороннюю библиотеку XML, которую вы используете в своем примере.

Вероятно, стоит отметить, что этот код не обратно совместим с PHP 5 (из-за использования yield from ), поэтому вы должны использовать PHP 7 для этой реализации.

Во-первых, я собираюсь реализовать функцию в DOMDocument которая позволяет нам перебирать дерево DOM в порядке уровня с помощью генератора .

 class SpecialDOM extends DOMDocument { public function level(DOMNode $node = null, $level = 0, $ignore = ["#text"]) { if (!$node) { $node = $this; } $stack = []; if ($node->hasChildNodes()) { foreach($node->childNodes as $child) { if (!in_array($child->nodeName, $ignore, true)) { $stack[] = $child; } } } if ($stack) { yield $level => $stack; foreach($stack as $node) { yield from $this->level($node, $level + 1, $ignore); } } } } 

Механика самой функции на самом деле довольно проста. Он не полагается на прохождение массивов или использование ссылок, но вместо этого сам использует объект DOMDocument , чтобы построить стек всех дочерних узлов в данном узле. Затем он может сразу yield весь этот стек. Это часть уровня . В этот момент мы полагаемся на рекурсию, чтобы получить от каждого элемента в этом стеке любые другие узлы на следующем уровне.

Вот очень простой XML-документ, демонстрирующий, насколько это прямолинейно.

 $xml = <<<'XML' <?xml version="1.0" encoding="UTF-8"?> <Data> <Record> <SAMPLE>Some Sample</SAMPLE> </Record> <Note> <SAMPLE>Some Sample</SAMPLE> </Note> <Record> <SAMPLE>Sample 1</SAMPLE> <SAMPLE>Sample 2</SAMPLE> </Record> </Data> XML; $dom = new SpecialDOM; $dom->loadXML($xml); foreach($dom->level() as $level => $stack) { echo "- Level $level\n"; foreach($stack as $item => $node) { echo "$item => $node->nodeName\n"; } } 

Результат будет выглядеть следующим образом.

 - Уровень 0
 0 => Данные
 - 1-й уровень
 0 => Запись
 1 => Примечание
 2 => Запись
 - Уровень 2
 0 => ОБРАЗЕЦ
 - Уровень 2
 0 => ОБРАЗЕЦ
 - Уровень 2
 0 => ОБРАЗЕЦ
 1 => ОБРАЗЕЦ

Итак, по крайней мере, теперь у нас есть способ узнать, на каком уровне находится узел, и в каком порядке он появляется на этом уровне, что полезно для того, что мы намереваемся сделать.

Теперь идея построения вложенного массива фактически не нужна для получения мощности, запрашиваемой max_count . Поскольку мы уже имеем доступ к самим узлам из дерева DOM. Это означает, что мы знаем, какие elements содержатся внутри нашего цикла на каждой итерации. Нам не нужно генерировать весь массив сразу, чтобы начать его изучать. Мы можем сделать это на уровне порядка, а это действительно здорово, потому что это означает, что вы можете построить плоский массив, чтобы добраться до max_count для каждой записи.

Позвольте мне продемонстрировать, как это будет работать.

 $max = []; foreach($dom->level() as $level => $stack) { $sum = []; foreach($stack as $item => $node) { $name = $node->nodeName; // the sum if (!isset($sum[$name])) { $sum[$name] = 1; } else { $sum[$name]++; } // the maximum if (!isset($max[$level][$name])) { $max[$level][$name] = 1; } else { $max[$level][$name] = max($sum[$name], $max[$level][$name]); } } } var_dump($max); 

Результат, который мы получим, будет выглядеть следующим образом.

 array (3) {
   [0] =>
   array (1) {
     [ "Данные"] =>
     INT (1)
   }
   [1] =>
   array (2) {
     [ "Запись"] =>
     Int (2)
     [ "Примечание"] =>
     INT (1)
   }
   [2] =>
   array (1) {
     [ "ОБРАЗЕЦ"] =>
     Int (2)
   }
 }

Это доказывает, что мы можем вычислить max_count без необходимости в ссылках или сложных вложенных массивах. Также проще обернуть голову, когда вы устраните одностороннюю семантику отображения массивов PHP.

конспект

Вот результат, полученный из этого кода в вашем XML-документе.

 array (5) {
   [0] =>
   array (1) {
     [ "Данные"] =>
     INT (1)
   }
   [1] =>
   array (1) {
     [ "Запись"] =>
     Int (2)
   }
   [2] =>
   array (1) {
     [ "ОБРАЗЕЦ"] =>
     Int (2)
   }
   [3] =>
   array (4) {
     [ "НАЗВАНИЕ"] =>
     INT (1)
     [ "СУБТИТРЫ"] =>
     INT (1)
     [ "AUTH"] =>
     Int (2)
     [ "РЕФЕРАТ"] =>
     INT (1)
   }
   [4] =>
   array (2) {
     [ "Fname"] =>
     INT (1)
     [ "DISPLAY"] =>
     INT (1)
   }
 }

Который идентичен max_count каждого из ваших вспомогательных массивов.

  • Уровень 0
    • Data => max_count 1
  • 1-й уровень
    • Record => max_count 2
  • Уровень 2
    • SAMPLE => max_count 2
  • Уровень 3
    • TITLE => max_count 1
    • SUBTITLE => max_count 1
    • AUTH => max_count 2
    • ABSTRACT => max_count 1
  • УРОВЕНЬ 4
    • FNAME => max_count 1
    • DISPLAY => max_count 1

Чтобы получить элементы для любого из этих узлов по всему циклу, просто посмотрите на $node->childNodes поскольку у вас уже есть дерево ( таким образом устраняя необходимость в ссылках ).

Единственная причина, по которой вам нужно было вложить элементы в ваш массив, является то, что ключи массива PHP должны быть уникальными, и поскольку вы используете имя узла в качестве ключа, для этого требуется вложение для получения нижних уровней дерева и структуры значение max_count правильно. Таким образом, это проблема структуры данных, и я решаю ее по-другому, избегая моделирования решения после структуры данных.

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

  private function countArrayElements(&$array, &$result){ // use local variable for temp storage $local_count = []; // get collection of subnodes foreach ($array as $node){ $name = $this->stripNamespace($node['name']); // get count of distinct subnodes if (empty($local_count[$name]["max_count"])){ $local_count[$name]["max_count"] = 1; } else { $local_count[$name]["max_count"]++; } // compare local to passed reference for max if(empty($result[$name]["max_count"])){ $result[$name]["max_count"] = $local_count[$name]["max_count"]; } else { $result[$name]["max_count"] = max( $local_count[$name]["max_count"], $result[$name]["max_count"] ); } if (is_array($node['value'])){ $this->countArrayElements($node['value'], $result[$name]["elements"]); } } }