Удалить дочерний элемент с определенным атрибутом в SimpleXML для PHP
У меня есть несколько идентичных элементов с разными атрибутами, к которым я обращаюсь с помощью SimpleXML:
<data>
<seg id="A1"/>
<seg id="A5"/>
<seg id="A12"/>
<seg id="A29"/>
<seg id="A30"/>
</data>
Мне нужно удалить определенный элемент сегмента с идентификатором "A12", как я могу это сделать? Я попытался перебрать элементы seg и сбросить конкретный, но это не работает, элементы остаются.
foreach($doc->seg as $seg)
{
if($seg['id'] == 'A12')
{
unset($seg);
}
}
18 ответов
Хотя 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 ( SimpleXMLElement-> 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 (самостоятельная ссылка). Это связано с его магической природой, числовые индексы представляют элементы в любом списке (например, 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://stackru.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 существует только для того, чтобы убедиться, что целевой узел существует, прежде чем пытаться его переместить. Вы можете выбрать разные способы обработки или игнорировать этот случай.
Эта работа для меня:
$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();
Если вы расширяете базовый класс 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>
Чтобы удалить / сохранить узлы с определенным значением атрибута или попасть в массив значений атрибутов, вы можете расширить 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://stackru.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 может быть проблематичным, особенно если вы не знаете точную структуру документа. Вот почему я написал 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"]');
Несмотря на то, что в 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)
), поскольку удаление элемента естественным образом уменьшает порядковый номер элементов, следующих за ним.
По общему признанию, это что-то вроде взлома, но, кажется, работает нормально.
Новая идея: 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. Код ищет элемент и ничего не делает. В противном случае он добавляет элемент в строку. Затем он записывает строку в файл. Также обратите внимание, что код сохраняет резервную копию перед перезаписью исходного файла.
$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);
У меня была аналогичная задача - удалить дочерние элементы, которые уже присутствуют с указанным атрибутом. Другими словами, удалите дубликаты в 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');
Идея о вспомогательных функциях взята из одного из комментариев для 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, поэтому не беспокойтесь о присвоении его какой-либо переменной.
Я также боролся с этой проблемой, и ответ на этот вопрос намного проще, чем здесь. Вы можете просто найти его с помощью xpath и сбросить его следующим способом:
unset($XML->xpath("NODESNAME[@id='test']")[0]->{0});
этот код будет искать узел с именем "NODESNAME" с атрибутом id "test" и удалит первый случай.
не забудьте сохранить xml с помощью $XML->saveXML(...);
Если вы хотите сократить список похожих (не уникальных) дочерних элементов, например, элементов 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});
}
Но это работает как-то случайным образом и вырезает только некоторые элементы.
С FluidXML вы можете использовать XPath для выбора удаляемых элементов.
$doc = fluidify($doc);
$doc->remove('//*[@id="A12"]');
https://github.com/servo-php/fluidxml
XPath //*[@id="A12"]
средства:
- в любой точке документа (
//
) - каждый узел (
*
) - с атрибутом
id
равноA12
([@id="A12"]
).
Поскольку я столкнулся с той же фатальной ошибкой, что и Джерри, и я не знаком с 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 )
Ваш первоначальный подход был верным, но вы забыли одну маленькую вещь о foreach. Он не работает с исходным массивом / объектом, но создает копию каждого элемента во время итерации, поэтому вы удалили копию. Используйте ссылку как это:
foreach($doc->seg as &$seg)
{
if($seg['id'] == 'A12')
{
unset($seg);
}
}