如何计算不同的XML节点?

时间:2016-08-31 23:33:15

标签: php 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
              ]

            ]
          ]

        ]
      ]

    ]
  ]

];

为了保持一点理智,我使用sabre/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}的计数仅为1 } node。

AUTH

我意识到这是一个非常复杂的问题,它只是一个更大的项目的一小部分,所以我试图尽可能地将它分解为这个MCVE并且我还准备了{{3}这些文件完成了phpunit测试。

2 个答案:

答案 0 :(得分:4)

虽然您的解决方案可以正常工作,并且非常有效,因为它在O(n*k)时间运行(其中n是树中的节点数,k是顶点),我认为我提出了一个不依赖于数组或引用的替代解决方案,并且更通用化,不仅适用于XML,而且适用于任何DOM树。此解决方案也可在O(n*k)时间运行,因此效率也同样高。唯一的区别是你可以使用generator中的值,而不必先构建整个数组。

对问题进行建模

我理解这个问题的最简单方法是将其建模为图形。如果我们对文档进行建模,那么我们得到的就是关卡和顶点。

DOM tree figure1

如此有效,这使我们能够分而治之,将问题分解为两个不同的步骤。

  1. 将给定垂直的基本子节点名称计为sum verticies
  2. 在水平(级别)上找到集合max的{​​{1}}
  3. 这意味着如果我们在这棵树上进行水平顺序遍历,我们应该能够轻松地将节点名称的基数作为所有垂直的最大总和。

    DOM tree figure2

    换句话说,存在获得每个节点的不同子节点名称的基数问题。然后是找到整个级别的最大总和的问题。

    最小,完整,可验证,自包含的示例

    因此,为了提供一个最小的,完整的,可验证的,自包含的示例,我将依赖于扩展PHP sum而不是第三方XML库。在你的例子中使用。

      

    可能值得注意的是,此代码不向后兼容PHP 5(因为使用了DOMDocument),所以必须使用PHP 7实施工作。

    首先,我将在yield from中实现一个函数,该函数允许我们使用generator按级别顺序迭代DOM树。

    DOMDocument

    函数本身的机制实际上非常简单。它并不依赖于传递数组或使用引用,而是使用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整个堆栈。这是级别部分。此时,我们依靠递归从此堆栈中的每个元素中产生 next 级别上的任何其他节点。

    这是一个非常简单的XML文档,用于演示这是多么简单。

    yield

    输出将如下所示。

    - Level 0
    0 => Data
    - Level 1
    0 => Record
    1 => Note
    2 => Record
    - Level 2
    0 => SAMPLE
    - Level 2
    0 => SAMPLE
    - Level 2
    0 => SAMPLE
    1 => SAMPLE
    

    所以至少现在我们有办法知道节点在什么级别以及它在该级别上出现的顺序,这对我们打算做的事情很有用。

    现在,构建嵌套数组的想法实际上不需要获得$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"; } } 所寻求的基数。因为我们已经可以从DOM树访问节点本身。这意味着我们知道在每次迭代时我们的循环内包含max_count。我们不必立即生成整个阵列以开始探索它。我们可以在级别顺序执行此操作,这实际上非常酷,因为这意味着您可以构建一个平面数组以获取每个记录的elements

    让我演示一下如何运作。

    max_count

    我们得到的输出看起来像这样。

    array(3) {
      [0]=>
      array(1) {
        ["Data"]=>
        int(1)
      }
      [1]=>
      array(2) {
        ["Record"]=>
        int(2)
        ["Note"]=>
        int(1)
      }
      [2]=>
      array(1) {
        ["SAMPLE"]=>
        int(2)
      }
    }
    

    这证明我们可以在不需要引用或复杂嵌套数组的情况下计算$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); 。当您消除PHP数组的单向映射语义时,它也更容易包围。

    概要

    以下是示例XML文档中此代码的结果输出。

    array(5) {
      [0]=>
      array(1) {
        ["Data"]=>
        int(1)
      }
      [1]=>
      array(1) {
        ["Record"]=>
        int(2)
      }
      [2]=>
      array(1) {
        ["SAMPLE"]=>
        int(2)
      }
      [3]=>
      array(4) {
        ["TITLE"]=>
        int(1)
        ["SUBTITLE"]=>
        int(1)
        ["AUTH"]=>
        int(2)
        ["ABSTRACT"]=>
        int(1)
      }
      [4]=>
      array(2) {
        ["FNAME"]=>
        int(1)
        ["DISPLAY"]=>
        int(1)
      }
    }
    

    这与每个子数组的max_count相同。

    • 等级0
      • max_count
    • 1级
      • Data => max_count 1
    • 等级2
      • Record => max_count 2
    • 等级3
      • SAMPLE => max_count 2
      • TITLE => max_count 1
      • SUBTITLE => max_count 1
      • AUTH => max_count 2
    • 第4级
      • ABSTRACT => max_count 1
      • FNAME => max_count 1

    要在整个循环中获取任何这些节点的元素,只需查看DISPLAY => max_count 1,因为您已经拥有树(因此无需引用)。

    您需要将元素嵌套到数组中的唯一原因是因为PHP数组的键必须是唯一的,并且因为您使用节点名作为键,这需要嵌套以获得较低级别的树仍然正确地构造$node->childNodes的值。因此,这是一个数据结构问题,我通过避免在数据结构之后对解决方案进行建模来解决它。

答案 1 :(得分:2)

我觉得愚蠢的是没有意识到解决方案在函数调用中简单地存储局部变量是多么容易,该函数调用比较了通过引用传递的现有值。

  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"]);
      }

    }
  }