【问题标题】:Remove a child with a specific attribute, in SimpleXML for PHP在 SimpleXML for PHP 中删除具有特定属性的子项
【发布时间】:2015-06-14 17:15:54
【问题描述】:

我有几个具有不同属性的相同元素,我正在使用 SimpleXML 访问它们:

<data>
    <seg id="A1"/>
    <seg id="A5"/>
    <seg id="A12"/>
    <seg id="A29"/>
    <seg id="A30"/>
</data>

我需要删除一个特定的 seg 元素,其 id 为“A12”,我该怎么做?我尝试循环遍历 seg 元素并取消设置特定元素,但这不起作用,元素仍然存在。

foreach($doc->seg as $seg)
{
    if($seg['id'] == 'A12')
    {
        unset($seg);
    }
}

【问题讨论】:

    标签: php xml dom simplexml


    【解决方案1】:

    虽然SimpleXML 提供a way to remove XML 节点,但它的修改能力有些受限。另一种解决方案是使用DOM 扩展名。 dom_import_simplexml() 将帮助您将您的 SimpleXMLElement 转换为 DOMElement

    只是一些示例代码(使用 PHP 5.2.5 测试):

    $data='<data>
        <seg id="A1"/>
        <seg id="A5"/>
        <seg id="A12"/>
        <seg id="A29"/>
        <seg id="A30"/>
    </data>';
    $doc=new SimpleXMLElement($data);
    foreach($doc->seg as $seg)
    {
        if($seg['id'] == 'A12') {
            $dom=dom_import_simplexml($seg);
            $dom->parentNode->removeChild($dom);
        }
    }
    echo $doc->asXml();
    

    输出

    <?xml version="1.0"?>
    <data><seg id="A1"/><seg id="A5"/><seg id="A29"/><seg id="A30"/></data>
    

    顺便说一句:使用 XPath (SimpleXMLElement->xpath) 时选择特定节点要简单得多:

    $segs=$doc->xpath('//seq[@id="A12"]');
    if (count($segs)>=1) {
        $seg=$segs[0];
    }
    // same deletion procedure as above
    

    【讨论】:

    • 谢谢你 - 最初我倾向于避免这个答案,因为我想避免使用 DOM。在最终尝试您的答案之前,我尝试了其他几个不起作用的答案-完美无缺。对于任何考虑避免此答案的人,请先尝试一下,看看它是否完全符合您的要求。我认为让我失望的是我没有意识到 dom_import_simplexml() 仍然可以使用与 simplexml 相同的底层结构,因此其中一个的任何更改都会立即影响另一个,无需写入/读取或重新加载。
    • 请注意,此代码只会删除遇到的第一个元素。我怀疑这是因为在迭代过程中修改数据会使迭代器位置无效,从而导致 foreach 循环终止。我通过将 dom 导入的节点保存到一个数组来解决这个问题,然后我遍历该数组以执行删除。不是一个很好的解决方案,但它确实有效。
    • 您实际上可以使用 unset 删除 SimpleXML 元素,请参阅 posthy 的解决方案。
    • 其实你可以使用 unset 来删除 SimpleXML 元素,但这是我的回答;)stackoverflow.com/a/16062633/367456
    • Unset 对我不起作用,但 dom 方法效果非常好。谢谢!
    【解决方案2】:

    有一种方法可以通过 SimpleXml 删除子元素。该代码寻找一个 元素,什么都不做。否则,它将元素添加到字符串中。然后它将字符串写出到文件中。另请注意,代码会在覆盖原始文件之前保存备份。

    $username = $_GET['delete_account'];
    echo "DELETING: ".$username;
    $xml = simplexml_load_file("users.xml");
    
    $str = "<?xml version=\"1.0\"?>
    <users>";
    foreach($xml->children() as $child){
      if($child->getName() == "user") {
          if($username == $child['name']) {
            continue;
        } else {
            $str = $str.$child->asXML();
        }
      }
    }
    $str = $str."
    </users>";
    echo $str;
    
    $xml->asXML("users_backup.xml");
    $myFile = "users.xml";
    $fh = fopen($myFile, 'w') or die("can't open file");
    fwrite($fh, $str);
    fclose($fh);
    

    【讨论】:

      【解决方案3】:

      只需取消设置节点:

      $str = <<<STR
      <a>
        <b>
          <c>
          </c>
        </b>
      </a>
      STR;
      
      $xml = simplexml_load_string($str);
      unset($xml –> a –> b –> c); // this would remove node c
      echo $xml –> asXML(); // xml document string without node c
      

      此代码取自How to delete / remove nodes in SimpleXML

      【讨论】:

      • 这仅适用于节点名称在集合中唯一的情况。如果不是,您最终会删除所有同名节点。
      • @Dallas:您的评论是正确的,但它也包含解决方案。如何仅访问第一个元素?见这里:stackoverflow.com/a/16062633/367456
      【解决方案4】:

      关于辅助函数的想法来自php.net 上的 DOM 的 cmets 之一,关于使用 unset 的想法来自kavoir.com。对我来说,这个解决方案终于奏效了:

      function Myunset($node)
      {
       unsetChildren($node);
       $parent = $node->parentNode;
       unset($node);
      }
      
      function unsetChildren($node)
      {
       while (isset($node->firstChild))
       {
       unsetChildren($node->firstChild);
       unset($node->firstChild);
       }
      }
      

      使用它: $xml 是 SimpleXmlElement

      Myunset($xml->channel->item[$i]);
      

      结果存储在 $xml 中,因此不必担心将其分配给任何变量。

      【讨论】:

      • 我不明白这将如何工作。 firstChild 和 parentNode 不是 DOM 的一部分,而不是 SimpleXML 的一部分吗?
      【解决方案5】:

      尽管 SimpleXML 没有删除元素的详细方法,您可以使用 PHP 的 unset() 从 SimpleXML 中删除元素。做到这一点的关键是设法定位所需的元素。至少一种进行定位的方法是使用元素的顺序。先找出要移除的元素的序号(例如用循环),然后移除元素:

      $target = false;
      $i = 0;
      foreach ($xml->seg as $s) {
        if ($s['id']=='A12') { $target = $i; break; }
        $i++;
      }
      if ($target !== false) {
        unset($xml->seg[$target]);
      }
      

      您甚至可以通过将目标项目的订单号存储在数组中来删除多个元素。请记住以相反的顺序进行移除 (array_reverse($targets)),因为移除一个项目自然会减少其后项目的订单号。

      诚然,这有点小技巧,但它似乎工作正常。

      【讨论】:

      • 您还可以使用自引用,它允许在不知道它的偏移量的情况下取消设置任何元素。 A single variable is enough.
      【解决方案6】:

      为了将来参考,使用 SimpleXML 删除节点有时会很痛苦,尤其是当您不知道文档的确切结构时。这就是我编写SimpleDOM 的原因,这是一个扩展 SimpleXMLElement 以添加一些便利方法的类。

      例如,deleteNodes() 将删除与 XPath 表达式匹配的所有节点。而如果你想删除所有属性“id”等于“A5”的节点,你所要做的就是:

      // don't forget to include SimpleDOM.php
      include 'SimpleDOM.php';
      
      // use simpledom_load_string() instead of simplexml_load_string()
      $data = simpledom_load_string(
          '<data>
              <seg id="A1"/>
              <seg id="A5"/>
              <seg id="A12"/>
              <seg id="A29"/>
              <seg id="A30"/>
          </data>'
      );
      
      // and there the magic happens
      $data->deleteNodes('//seg[@id="A5"]');
      

      【讨论】:

        【解决方案7】:

        我相信 Stefan 的回答是正确的。如果您只想删除一个节点(而不是所有匹配的节点),这是另一个示例:

        //Load XML from file (or it could come from a POST, etc.)
        $xml = simplexml_load_file('fileName.xml');
        
        //Use XPath to find target node for removal
        $target = $xml->xpath("//seg[@id=$uniqueIdToDelete]");
        
        //If target does not exist (already deleted by someone/thing else), halt
        if(!$target)
        return; //Returns null
        
        //Import simpleXml reference into Dom & do removal (removal occurs in simpleXML object)
        $domRef = dom_import_simplexml($target[0]); //Select position 0 in XPath array
        $domRef->parentNode->removeChild($domRef);
        
        //Format XML to save indented tree rather than one line and save
        $dom = new DOMDocument('1.0');
        $dom->preserveWhiteSpace = false;
        $dom->formatOutput = true;
        $dom->loadXML($xml->asXML());
        $dom->save('fileName.xml');
        

        请注意,加载 XML...(第一个)和 Format XML...(最后一个)部分可以替换为不同的代码,具体取决于您的 XML 数据来自哪里以及您想对输出做什么;它是找到一个节点并将其删除之间的部分。

        此外,if 语句只是为了确保目标节点在尝试移动它之前存在。您可以选择不同的方式来处理或忽略这种情况。

        【讨论】:

        • 请注意,如果没有找到,xpath() 会返回一个空数组,因此检查 $target == false 应该为空($target)。 +1 xpath 解决方案
        【解决方案8】:

        您最初的方法是正确的,但是您忘记了有关 foreach 的一点小事。它不适用于原始数组/对象,但会在迭代时创建每个元素的副本,因此您确实取消了副本。像这样使用参考:

        foreach($doc->seg as &$seg) 
        {
            if($seg['id'] == 'A12')
            {
                unset($seg);
            }
        }
        

        【讨论】:

        • 这个答案需要更多的爱,因为每个人都在为一个非常简单的错误想出非常复杂的解决方案!
        • “致命错误:迭代器不能通过引用与 foreach 一起使用”
        • 对于那些想知道迭代器错误的人,请参阅comment here
        【解决方案9】:

        一个新想法:simple_xml 用作数组。

        我们可以搜索我们要删除的“数组”的索引,然后使用unset()函数删除这个数组索引。我的例子:

        $pos=$this->xml->getXMLUser();
        $i=0; $array_pos=array();
        foreach($this->xml->doc->users->usr[$pos]->u_cfg_root->profiles->profile as $profile) {
            if($profile->p_timestamp=='0') { $array_pos[]=$i; }
            $i++;
        }
        //print_r($array_pos);
        for($i=0;$i<count($array_pos);$i++) {
            unset($this->xml->doc->users->usr[$pos]->u_cfg_root->profiles->profile[$array_pos[$i]]);
        }
        

        【讨论】:

          【解决方案10】:

          这对我有用:

          $data = '<data>
          <seg id="A1"/>
          <seg id="A5"/>
          <seg id="A12"/>
          <seg id="A29"/>
          <seg id="A30"/></data>';
          
          $doc = new SimpleXMLElement($data);
          
          $segarr = $doc->seg;
          
          $count = count($segarr);
          
          $j = 0;
          
          for ($i = 0; $i < $count; $i++) {
          
              if ($segarr[$j]['id'] == 'A12') {
                  unset($segarr[$j]);
                  $j = $j - 1;
              }
              $j = $j + 1;
          }
          
          echo $doc->asXml();
          

          【讨论】:

          • +1 这非常适合它的功能。没有混乱。不用大惊小怪。
          【解决方案11】:

          如果你扩展了基础的 SimpleXMLElement 类,你可以使用这个方法:

          class MyXML extends SimpleXMLElement {
          
              public function find($xpath) {
                  $tmp = $this->xpath($xpath);
                  return isset($tmp[0])? $tmp[0]: null;
              }
          
              public function remove() {
                  $dom = dom_import_simplexml($this);
                  return $dom->parentNode->removeChild($dom);
              }
          
          }
          
          // Example: removing the <bar> element with id = 1
          $foo = new MyXML('<foo><bar id="1"/><bar id="2"/></foo>');
          $foo->find('//bar[@id="1"]')->remove();
          print $foo->asXML(); // <foo><bar id="2"/></foo>
          

          【讨论】:

          • 每次$foo-&gt;find('//bar[@id="1"]')返回null时都容易出现Fatal error: Call to a member function remove() on null
          【解决方案12】:

          与现有答案的普遍看法相反,每个 Simplexml 元素节点都可以单独从文档中删除,unset()。以防万一,您需要了解 SimpleXML 的实际工作原理。

          首先找到要移除的元素:

          list($element) = $doc->xpath('/*/seg[@id="A12"]');
          

          然后删除$element 中表示的元素,取消设置它的自引用

          unset($element[0]);
          

          这是可行的,因为任何元素的第一个元素都是 Simplexml 中的元素本身(自引用)。这与它的神奇特性有关,数字索引代表任何列表中的元素(例如父->子),甚至单个子也是这样的列表。

          非数字字符串索引表示属性(在数组访问中)或子元素(在属性访问中)。

          因此,属性访问中的数字不合理,例如:

          unset($element->{0});
          

          也可以。

          对于那个 xpath 示例,它自然是相当直接的(在 PHP 5.4 中):

          unset($doc->xpath('/*/seg[@id="A12"]')[0][0]);
          

          完整的示例代码(Demo):

          <?php
          /**
           * Remove a child with a specific attribute, in SimpleXML for PHP
           * @link http://stackoverflow.com/a/16062633/367456
           */
          
          $data=<<<DATA
          <data>
              <seg id="A1"/>
              <seg id="A5"/>
              <seg id="A12"/>
              <seg id="A29"/>
              <seg id="A30"/>
          </data>
          DATA;
          
          
          $doc = new SimpleXMLElement($data);
          
          unset($doc->xpath('seg[@id="A12"]')[0]->{0});
          
          $doc->asXml('php://output');
          

          输出:

          <?xml version="1.0"?>
          <data>
              <seg id="A1"/>
              <seg id="A5"/>
          
              <seg id="A29"/>
              <seg id="A30"/>
          </data>
          

          【讨论】:

          • 这种自引用技术早前(2010 年 11 月)已在:an answer to "PHP SimpleXML - Remove xpath node" 中得到证明。
          • 而这种 simplexml 自引用技术在早些时候(2010 年 6 月)已在:an answer to "How can I set text value of SimpleXmlElement without using its parent?" 中展示过
          • 很好解释的答案。我没有立即意识到的一个细节是,您不能轻易地将 XPath 带出循环,因为删除普通 foreach ( $doc-&gt;seg as $seg ) 循环内的元素会混淆迭代器(经验法则:不要更改迭代器的长度中环)。 SimpleXML 的 XPath 实现没有这个问题,因为它的结果是一个普通的不相关元素数组。
          • @IMSoP:对于任何Traversable 和那个问题(实时列表),我强烈推荐iterator_to_array,在SimpleXML 迭代器中将关键参数设置为FALSE,因为SimpleXMLElement使用标签名作为键,在这样的列表中经常重复,然后如果第二个参数不是FALSE,该函数将只返回这些同名节点中的最后一个。
          • 很好的提示,特别是关于额外参数。 :)
          【解决方案13】:

          我也在为这个问题而苦苦挣扎,答案比这里提供的要容易得多。 您可以使用 xpath 查找它并使用以下方法取消设置它:

          unset($XML->xpath("NODESNAME[@id='test']")[0]->{0});
          

          此代码将查找一个名为“NODESNAME”且 id 属性为“test”的节点并删除第一个出现的节点。

          记得使用 $XML->saveXML(...); 保存 xml;

          【讨论】:

            【解决方案14】:

            由于我遇到了和 Gerry 一样的致命错误,而且我对 DOM 不熟悉,所以我决定这样做:

            $item = $xml->xpath("//seg[@id='A12']");
            $page = $xml->xpath("/data");
            $id = "A12";
            
            if (  count($item)  &&  count($page) ) {
                $item = $item[0];
                $page = $page[0];
            
                 // find the numerical index within ->children().
                $ch = $page->children();
                $ch_as_array = (array) $ch;
            
                if (  count($ch_as_array)  &&  isset($ch_as_array['seg'])  ) {
                    $ch_as_array = $ch_as_array['seg'];
                    $index_in_array = array_search($item, $ch_as_array);
                    if (  ($index_in_array !== false)
                      &&  ($index_in_array !== null)
                      &&  isset($ch[$index_in_array])
                      &&  ($ch[$index_in_array]['id'] == $id)  ) {
            
                         // delete it!
                        unset($ch[$index_in_array]);
            
                        echo "<pre>"; var_dump($xml); echo "</pre>";
                    }
                }  // end of ( if xml object successfully converted to array )
            }  // end of ( valid item  AND  section )
            

            【讨论】:

              【解决方案15】:

              使用FluidXML,您可以使用 XPath 选择要删除的元素。

              $doc = fluidify($doc);
              
              $doc->remove('//*[@id="A12"]');
              

              https://github.com/servo-php/fluidxml


              XPath //*[@id="A12"] 表示:

              • 在文档的任意位置 (//)
              • 每个节点 (*)
              • 属性id 等于A12 ([@id="A12"])。

              【讨论】:

                【解决方案16】:

                如果您想剪切相似(非唯一)子元素的列表,例如 RSS 提要的项目,您可以使用以下代码:

                for ( $i = 9999; $i > 10; $i--) {
                    unset($xml->xpath('/rss/channel/item['. $i .']')[0]->{0});
                }
                

                它将RSS的尾部减少到10个元素。我试图删除

                for ( $i = 10; $i < 9999; $i ++ ) {
                    unset($xml->xpath('/rss/channel/item[' . $i . ']')[0]->{0});
                }
                

                但它以某种方式随机工作并且只剪切了一些元素。

                【讨论】:

                  【解决方案17】:

                  要删除/保留具有特定属性值或属于属性值数组的节点,您可以像这样扩展SimpleXMLElement 类(我的GitHub Gist 中的最新版本):

                  class SimpleXMLElementExtended extends SimpleXMLElement
                  {    
                      /**
                      * Removes or keeps nodes with given attributes
                      *
                      * @param string $attributeName
                      * @param array $attributeValues
                      * @param bool $keep TRUE keeps nodes and removes the rest, FALSE removes nodes and keeps the rest 
                      * @return integer Number o affected nodes
                      *
                      * @example: $xml->o->filterAttribute('id', $products_ids); // Keeps only nodes with id attr in $products_ids
                      * @see: http://stackoverflow.com/questions/17185959/simplexml-remove-nodes
                      */
                      public function filterAttribute($attributeName = '', $attributeValues = array(), $keepNodes = TRUE)
                      {       
                          $nodesToRemove = array();
                  
                          foreach($this as $node)
                          {
                              $attributeValue = (string)$node[$attributeName];
                  
                              if ($keepNodes)
                              {
                                  if (!in_array($attributeValue, $attributeValues)) $nodesToRemove[] = $node;
                              }
                              else
                              { 
                                  if (in_array($attributeValue, $attributeValues)) $nodesToRemove[] = $node;
                              }
                          }
                  
                          $result = count($nodesToRemove);
                  
                          foreach ($nodesToRemove as $node) {
                              unset($node[0]);
                          }
                  
                          return $result;
                      }
                  }
                  

                  然后拥有您的$doc XML,您可以删除您的&lt;seg id="A12"/&gt; 节点调用:

                  $data='<data>
                      <seg id="A1"/>
                      <seg id="A5"/>
                      <seg id="A12"/>
                      <seg id="A29"/>
                      <seg id="A30"/>
                  </data>';
                  
                  $doc=new SimpleXMLElementExtended($data);
                  $doc->seg->filterAttribute('id', ['A12'], FALSE);
                  

                  或删除多个&lt;seg /&gt; 节点:

                  $doc->seg->filterAttribute('id', ['A1', 'A12', 'A29'], FALSE);
                  

                  仅保留 &lt;seg id="A5"/&gt;&lt;seg id="A30"/&gt; 节点并删除其余节点:

                  $doc->seg->filterAttribute('id', ['A5', 'A30'], TRUE);
                  

                  【讨论】:

                    【解决方案18】:

                    我有一个类似的任务 - 删除已经具有指定属性的子元素。换句话说,删除 xml 中的重复项。我有以下xml结构:

                    <rups>
                        <rup id="1">
                             <profiles> ... </profiles>
                             <sections>
                                 <section id="1.1" num="Б1.В" parent_id=""/>
                                 <section id="1.1.1" num="Б1.В.1" parent_id="1.1"/>
                                 ...
                                 <section id="1.1" num="Б1.В" parent_id=""/>
                                 <section id="1.1.2" num="Б1.В.2" parent_id="1.1"/>
                                 ...
                             </sections>
                        </rup>
                        <rup id="2">
                             ...
                        </rup>
                        ...
                     </rups>
                    

                    例如,rups/rup[@id='1']/sections/section[@id='1.1'] 元素是重复的,我只需要留下第一个。 我正在使用对元素数组、loop-for 和 unset() 的引用:

                    $xml = simplexml_load_file('rup.xml');
                    foreach ($xml->rup as $rup) {
                        $r_s = [];
                        $bads_r_s = 0;
                        $sections = &$rup->sections->section;
                        for ($i = count($sections)-1; $i >= 0; --$i) {
                            if (in_array((string)$sections[$i]['id'], $r_s)) {
                                $bads_r_s++;
                                unset($sections[$i]);
                                continue;
                            }
                            $r_s[] = (string)$sections[$i]['id'];
                        }
                    }
                    $xml->saveXML('rup_checked.xml');
                    

                    【讨论】:

                      猜你喜欢
                      • 2015-05-06
                      • 1970-01-01
                      • 1970-01-01
                      • 2010-11-02
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多