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






Хотя 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
Обратите внимание, что этот код удалит только первый обнаруженный элемент. Я подозреваю, что это связано с тем, что изменение данных во время итерации делает недействительной позицию итератора, что приводит к завершению цикла foreach. Я решил эту проблему, сохранив импортированные из dom узлы в массив, который я затем повторил, чтобы выполнить удаление. Не лучшее решение, но оно работает.
Фактически вы можете удалить элементы SimpleXML, используя unset, см. Ответ posthy для решения.
На самом деле вы можете удалить элементы SimpleXML, используя unset, но это в моем ответе;) stackoverflow.com/a/16062633/367456
Unset у меня не работал, но метод dom работал очень хорошо. Спасибо за это!
Есть способ удалить дочерний элемент через 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: То, что вы комментируете, правильно, но оно также содержит решение. Как получить доступ только к первому элементу? Смотрите здесь: stackoverflow.com/a/16062633/367456
Идея о вспомогательных функциях взята из одного из комментариев к 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?
Несмотря на то, что 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)), потому что удаление элемента естественным образом уменьшает порядковый номер элементов, следующих за ним.
По общему признанию, это что-то вроде обходного пути, но, похоже, все работает нормально.
Вы также можете использовать ссылку на себя, которая позволяет отключить любой элемент, не зная его смещения. Достаточно одной переменной.
Для справки в будущем, удаление узлов с помощью 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
Ваш первоначальный подход был правильным, но вы забыли одну мелочь о foreach. Он не работает с исходным массивом / объектом, но создает копию каждого элемента во время итерации, поэтому вы отключили копию. Используйте такую ссылку:
foreach($doc->seg as &$seg)
{
if ($seg['id'] == 'A12')
{
unset($seg);
}
}
Этот ответ требует гораздо большей любви, поскольку каждый придумывает очень сложные решения очень простой ошибки!
«Неустранимая ошибка: итератор нельзя использовать с foreach по ссылке»
Для тех, кто интересуется ошибкой итератора, см. комментарий здесь
Новая идея: 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 Это потрясающе идеально для того, что делает. Нет болвана. Не суетись.
Если вы расширите базовый класс 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.
Вопреки распространенному мнению в существующих ответах, каждый узел элемента 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".
И этот метод самосопоставления simplexml был ранее (июнь 2010 г.) продемонстрирован в: ответ на «Как я могу установить текстовое значение SimpleXmlElement без использования его родителя?»
Очень хорошо объясненный ответ. Одна деталь, которую я не сразу оценил, заключается в том, что вы не можете тривиально вывести XPath из цикла, потому что удаление элемента внутри обычного цикла foreach ( $doc->seg as $seg ) сбивает итератор с толку (эмпирическое правило: не изменяйте длину итератора в середине). петля). Реализация XPath в SimpleXML не имеет этой проблемы, потому что ее результаты представляют собой обычный массив несвязанных элементов.
@IMSoP: для любого Traversable и этой проблемы (живые списки) я настоятельно рекомендую iterator_to_array, в итераторах SimpleXML установите для параметра key значение FALSE, потому что SimpleXMLElement использует имя тега как ключ, который часто дублируется в таком листинге, и тогда эта функция будет возвращает только последний из этих узлов с таким же именем, если второй параметр не FALSE.
Хороший совет, особенно по поводу дополнительного параметра. :)
Я тоже боролся с этой проблемой, и ответить на этот вопрос проще, чем здесь. вы можете просто найти его с помощью 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);
Спасибо за это - изначально я был склонен избегать этого ответа, так как хотел избежать использования DOM. Я попробовал несколько других ответов, которые не сработали, прежде чем, наконец, попробовать ваш - который работал безупречно. Для тех, кто рассматривает возможность избежать этого ответа, сначала попробуйте его и посмотрите, не делает ли он именно то, что вы хотите. Я думаю, что меня сбило с толку, так это то, что я не осознавал, что dom_import_simplexml () по-прежнему работает с той же базовой структурой, что и simplexml, поэтому любые изменения в одном немедленно влияют на другие, нет необходимости писать / читать или перезагружать.