Удалить дочерний элемент с определенным атрибутом в SimpleXML для PHP

У меня есть несколько идентичных элементов с разными атрибутами, к которым я обращаюсь с помощью SimpleXML:

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

Мне нужно удалить определенный элемент сег с идентификатором «A12», как я могу это сделать? Я пробовал перебирать элементы сег и сброшенing конкретный, но это не работает, элементы остаются.

foreach($doc->seg as $seg)
{
    if ($seg['id'] == 'A12')
    {
        unset($seg);
    }
}
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
52
0
104 354
17
Перейти к ответу Данный вопрос помечен как решенный

Ответы 17

Ответ принят как подходящий

Хотя SimpleXML предоставляет XML-узлы способ удалить, его возможности модификации несколько ограничены. Еще одно решение - прибегнуть к использованию расширения ДОМ. 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, поэтому любые изменения в одном немедленно влияют на другие, нет необходимости писать / читать или перезагружать.

dimo414 05.07.2010 06:01

Обратите внимание, что этот код удалит только первый обнаруженный элемент. Я подозреваю, что это связано с тем, что изменение данных во время итерации делает недействительной позицию итератора, что приводит к завершению цикла foreach. Я решил эту проблему, сохранив импортированные из dom узлы в массив, который я затем повторил, чтобы выполнить удаление. Не лучшее решение, но оно работает.

Ryan Ballantyne 12.09.2010 07:50

Фактически вы можете удалить элементы SimpleXML, используя unset, см. Ответ posthy для решения.

François Feugeas 17.08.2011 20:05

На самом деле вы можете удалить элементы SimpleXML, используя unset, но это в моем ответе;) stackoverflow.com/a/16062633/367456

hakre 17.04.2013 18:27

Unset у меня не работал, но метод dom работал очень хорошо. Спасибо за это!

Jarvis 20.07.2016 20:15

Есть способ удалить дочерний элемент через 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);

Просто отключите узел:

$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

Этот код был взят из Как удалить / удалить узлы в SimpleXML.

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

Dallas 06.12.2012 20:34

@Dallas: То, что вы комментируете, правильно, но оно также содержит решение. Как получить доступ только к первому элементу? Смотрите здесь: stackoverflow.com/a/16062633/367456

hakre 17.04.2013 18:30

Идея о вспомогательных функциях взята из одного из комментариев к DOM на php.net, а идея об использовании 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?

mermshaus 16.06.2011 01:38

Несмотря на то, что SimpleXML не имеет подробного способа удаления элементов, вы может удаляете элементы из SimpleXML с помощью PHP unset(). Ключом к этому является достижение желаемого элемента. По крайней мере, один из способов сделать таргетинг - использовать порядок элементов. Сначала узнайте порядковый номер элемента, который вы хотите удалить (например, с помощью цикла), затем удалите элемент:

$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)), потому что удаление элемента естественным образом уменьшает порядковый номер элементов, следующих за ним.

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

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

hakre 17.04.2013 18:33

Для справки в будущем, удаление узлов с помощью 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"]');

Я считаю, что Стефан прав. Если вы хотите удалить только один узел (а не все совпадающие узлы), вот еще один пример:

//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');

Обратите внимание, что разделы Load XML ... (first) и Format XML ... (last) могут быть заменены другим кодом в зависимости от того, откуда поступают ваши XML-данные и что вы хотите делать с выводом; именно промежуточные секции находят узел и удаляют его.

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

Обратите внимание, что xpath () возвращает пустой массив, если ничего не найдено, поэтому проверка $ target == false должна быть пустой ($ target). +1 для решения xpath

Znarkus 13.01.2010 15:01

Ваш первоначальный подход был правильным, но вы забыли одну мелочь о foreach. Он не работает с исходным массивом / объектом, но создает копию каждого элемента во время итерации, поэтому вы отключили копию. Используйте такую ​​ссылку:

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

Этот ответ требует гораздо большей любви, поскольку каждый придумывает очень сложные решения очень простой ошибки!

François Feugeas 17.08.2011 20:04

«Неустранимая ошибка: итератор нельзя использовать с foreach по ссылке»

Gerry 20.03.2013 11:47

Для тех, кто интересуется ошибкой итератора, см. комментарий здесь

Robert Dundon 25.04.2017 22:45

Новая идея: 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]]);
}

Эта работа для меня:

$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 Это потрясающе идеально для того, что делает. Нет болвана. Не суетись.

Giacomo1968 04.10.2014 06:08

Если вы расширите базовый класс 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>

Он склонен к Fatal error: Call to a member function remove() on null каждый раз, когда $foo->find('//bar[@id = "1"]') возвращает null.

Krzysztof Przygoda 26.08.2016 16:18

Вопреки распространенному мнению в существующих ответах, каждый узел элемента Simplexml может быть удален из документа только сам по себе и из unset(). Дело в том, что вам нужно понимать, как на самом деле работает SimpleXML.

Сначала найдите элемент, который хотите удалить:

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

Затем удалите элемент, представленный в $element, вы отключите его ссылка на себя:

unset($element[0]);

Это работает, потому что первым элементом любого элемента является сам элемент в Simplexml (ссылка на себя). Это связано с его магической природой, числовые индексы представляют элементы в любом списке (например, parent-> children), и даже один дочерний элемент является таким списком.

Нечисловые строковые индексы представляют атрибуты (при доступе к массиву) или дочерние элементы (при доступе к свойству).

Поэтому числовые индексы в доступе к свойствам, например:

unset($element->{0});

тоже работать.

Естественно, с этим примером xpath это довольно просто (в PHP 5.4):

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

Полный пример кода (Демо):

<?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 г.) продемонстрирован в: ответ на "PHP SimpleXML - Удалить узел xpath".

hakre 21.05.2013 18:23

И этот метод самосопоставления simplexml был ранее (июнь 2010 г.) продемонстрирован в: ответ на «Как я могу установить текстовое значение SimpleXmlElement без использования его родителя?»

hakre 22.06.2013 12:20

Очень хорошо объясненный ответ. Одна деталь, которую я не сразу оценил, заключается в том, что вы не можете тривиально вывести XPath из цикла, потому что удаление элемента внутри обычного цикла foreach ( $doc->seg as $seg ) сбивает итератор с толку (эмпирическое правило: не изменяйте длину итератора в середине). петля). Реализация XPath в SimpleXML не имеет этой проблемы, потому что ее результаты представляют собой обычный массив несвязанных элементов.

IMSoP 13.09.2013 00:06

@IMSoP: для любого Traversable и этой проблемы (живые списки) я настоятельно рекомендую iterator_to_array, в итераторах SimpleXML установите для параметра key значение FALSE, потому что SimpleXMLElement использует имя тега как ключ, который часто дублируется в таком листинге, и тогда эта функция будет возвращает только последний из этих узлов с таким же именем, если второй параметр не FALSE.

hakre 30.09.2013 13:00

Хороший совет, особенно по поводу дополнительного параметра. :)

IMSoP 30.09.2013 13:21

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

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

этот код будет искать узел с именем «NODESNAME» с атрибутом id «test» и удаляет первое вхождение.

не забудьте сохранить xml с помощью $ XML-> saveXML (...);

Поскольку я столкнулся с той же фатальной ошибкой, что и Джерри, и я не знаком с 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 )

С помощью FluidXML вы можете использовать XPath для выбора элементов для удаления.

$doc = fluidify($doc);

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

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


XPath //*[@id = "A12"] означает:

  • в любой точке документа (//)
  • каждый узел (*)
  • с атрибутом id, равным A12 ([@id = "A12"]).

Если вы хотите вырезать список похожих (не уникальных) дочерних элементов, например элементов 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});
}

Но работает как-то хаотично и отсекает только некоторые элементы.

Чтобы удалить / сохранить узлы с определенным значением атрибута или попадающие в массив значений атрибутов, вы можете расширить класс 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);

Другие вопросы по теме