在SimpleXML for PHP中删除具有特定属性的子项

时间:2008-11-04 16:24:51

标签: php xml dom simplexml

我有几个相同的元素,我使用SimpleXML访问不同的属性:

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

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

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

17 个答案:

答案 0 :(得分:55)

与现有答案中的普遍看法相反,每个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>

答案 1 :(得分:51)

虽然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

答案 2 :(得分:23)

取消设置节点:

$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

答案 3 :(得分:10)

我相信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数据的来源以及您想要对输出执行的操作,可以使用不同的代码替换部分加载XML ...(第一个)和格式XML ...(最后一个);中间的部分找到一个节点并将其删除。

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

答案 4 :(得分:5)

这项工作对我来说:

$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();

答案 5 :(得分:4)

如果扩展基本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>

答案 6 :(得分:2)

要移除/保留具有特定属性值的节点或属于属性值数组,您可以像这样扩展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,您可以移除<seg id="A12"/>节点呼叫:

$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);

或删除多个<seg />节点:

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

仅保留<seg id="A5"/><seg id="A30"/>个节点并删除其余节点:

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

答案 7 :(得分:2)

为了将来参考,使用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"]');

答案 8 :(得分:1)

一个新想法: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]]);
}

答案 9 :(得分:1)

有一种方法可以通过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);

答案 10 :(得分:0)

尽管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))进行删除,因为删除项目会自然地减少后面项目的订单号。

不可否认,这有点令人讨厌,但似乎工作正常。

答案 11 :(得分:0)

关于辅助函数的想法来自php.net上关于DOM的注释之一,关于使用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中,因此不必担心将其分配给任何变量。

答案 12 :(得分:0)

我也对此问题感到不满,答案比这里提供的答案更容易。 您可以使用xpath查找它并使用以下方法取消它:

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

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

请记住使用$ XML-&gt; saveXML(...);

保存xml

答案 13 :(得分:0)

由于我遇到了和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 )

答案 14 :(得分:0)

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

$doc = fluidify($doc);

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

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

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

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

答案 15 :(得分:0)

如果要剪切类似(非唯一)子元素的列表,例如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});
}

但它以某种方式随机工作,只削减了一些元素。

答案 16 :(得分:-2)

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

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