Удалите дочерний элемент с определенным атрибутом в SimpleXML для PHP
У меня есть несколько идентичных элементов с разными атрибутами, к которым я обращаюсь с помощью SimpleXML:
<data>
<seg id="A1"/>
<seg id="A5"/>
<seg id="A12"/>
<seg id="A29"/>
<seg id="A30"/>
</data>
Мне нужно удалить определенный элемент seg с идентификатором "A12", как я могу это сделать? Я пробовал циклически перебирать элементы seg и отключать для конкретного, но это не работает, элементы остаются.
foreach($doc->seg as $seg)
{
if($seg['id'] == 'A12')
{
unset($seg);
}
}
17 answers
В то время как SimpleXML предоставляет способ удаления 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 (Симплексный элемент->xpath):
$segs=$doc->xpath('//seq[@id="A12"]');
if (count($segs)>=1) {
$seg=$segs[0];
}
// same deletion procedure as above
Вопреки распространенному мнению в существующих ответах, каждый узел элемента 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]);
Полный пример кода ( Демонстрационный):
<?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>
Просто отключите узел:
$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.
Я считаю, что ответ Стефана верен. Если вы хотите удалить только один узел (а не все соответствующие узлы), вот еще один пример:
//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 существует только для того, чтобы гарантировать, что цель узел существует до того, как его попытаются переместить. Вы можете выбрать разные способы справиться с этим делом или проигнорировать его.
Если вы расширяете базовый класс 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>
Эта работа для меня:
$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();
Для дальнейшего использования удаление узлов с помощью 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"]');
Чтобы удалить/сохранить узлы с определенным значением атрибута или попадающие в массив значений атрибутов, вы можете расширить класс 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);
Существует способ удалить дочерний элемент с помощью 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);
Новая идея: 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]]);
}
Хотя в 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)
), потому что удаление элемента естественным образом уменьшает порядковый номер элементов, которые следуют за ним.
По общему признанию, это немного обходной путь, но, похоже, он работает нормально.
Я также боролся с этой проблемой, и ответ намного проще, чем те, которые приведены здесь. вы можете просто найти его с помощью xpath и отключить его следующим способом:
unset($XML->xpath("NODESNAME[@id='test']")[0]->{0});
Этот код будет искать узел с именем "NODESNAME" с атрибутом id "тест" и удалит первое появление.
Не забудьте сохранить 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 )
Идея о вспомогательных функциях взята из одного из комментариев для 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 - это элемент SimpleXML
Myunset($xml->channel->item[$i]);
Результат хранится в $xml, поэтому не беспокойтесь о присвоении его какой-либо переменной.
С Жидкокристаллический вы можете использовать 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});
}
Но он работает как-то случайным образом и вырезает только некоторые элементы.
Ваш первоначальный подход был правильным, но вы забыли одну маленькую вещь о foreach. Он не работает с исходным массивом/объектом, но создает копию каждого элемента по мере его повторения, поэтому вы сняли настройку копии. Используйте ссылку следующим образом:
foreach($doc->seg as &$seg)
{
if($seg['id'] == 'A12')
{
unset($seg);
}
}