как удалить элемент в lxml

Мне нужно полностью удалить элементы на основе содержимого атрибута, используя python lxml. Пример:

import lxml.etree as et

xml="""
<groceries>
  <fruit state="rotten">apple</fruit>
  <fruit state="fresh">pear</fruit>
  <fruit state="fresh">starfruit</fruit>
  <fruit state="rotten">mango</fruit>
  <fruit state="fresh">peach</fruit>
</groceries>
"""

tree=et.fromstring(xml)

for bad in tree.xpath("//fruit[@state=\'rotten\']"):
  #remove this element from the tree

print et.tostring(tree, pretty_print=True)

Я хотел бы, чтобы это напечатало:

<groceries>
  <fruit state="fresh">pear</fruit>
  <fruit state="fresh">starfruit</fruit>
  <fruit state="fresh">peach</fruit>
</groceries>

Есть ли способ сделать это без сохранения временной переменной и ее печати вручную, например:

newxml="<groceries>\n"
for elt in tree.xpath('//fruit[@state=\'fresh\']'):
  newxml+=et.tostring(elt)

newxml+="</groceries>"

person ewok    schedule 02.11.2011    source источник


Ответы (6)


Используйте метод remove элемента xmlElement:

tree=et.fromstring(xml)

for bad in tree.xpath("//fruit[@state=\'rotten\']"):
  bad.getparent().remove(bad)     # here I grab the parent of the element to call the remove directly on it

print et.tostring(tree, pretty_print=True, xml_declaration=True)

Если бы мне пришлось сравнить с версией @Acorn, моя будет работать, даже если удаляемые элементы не находятся непосредственно в корневом узле вашего xml.

person Cédric Julien    schedule 02.11.2011
comment
Можете ли вы прокомментировать различия между этим ответом и ответом, предоставленным Acorn? - person ewok; 02.11.2011
comment
Жаль, что в классе Element нет метода pop. - person pumazi; 28.08.2015
comment
жаль, что xpath можно использовать только для выбора элементов. это похоже на SQL только с операторами выбора. - person Eric Chow; 12.01.2021
comment
Функция remove отсоединяет элемент от дерева и, следовательно, удаляет узел XML (элемент, PI или комментарий), его содержимое (элементы-потомки) и текст tail. Здесь сохранение текста tail излишне, потому что он содержит только пробелы и новую строку. Но в некоторых ситуациях вам может понадобиться сохранить его… - person Laurent LAPORTE; 17.03.2021
comment
Чтобы сохранить текст tail и при необходимости сохранить содержимое элемента, вы можете рассмотреть возможность использования функции remove_node, определенной ниже. - person Laurent LAPORTE; 17.03.2021

Вы ищете функцию remove. Вызовите метод удаления дерева и передайте ему удаляемый подэлемент.

import lxml.etree as et

xml="""
<groceries>
  <fruit state="rotten">apple</fruit>
  <fruit state="fresh">pear</fruit>
  <punnet>
    <fruit state="rotten">strawberry</fruit>
    <fruit state="fresh">blueberry</fruit>
  </punnet>
  <fruit state="fresh">starfruit</fruit>
  <fruit state="rotten">mango</fruit>
  <fruit state="fresh">peach</fruit>
</groceries>
"""

tree=et.fromstring(xml)

for bad in tree.xpath("//fruit[@state='rotten']"):
    bad.getparent().remove(bad)

print et.tostring(tree, pretty_print=True)

Результат:

<groceries>
  <fruit state="fresh">pear</fruit>
  <fruit state="fresh">starfruit</fruit>
  <fruit state="fresh">peach</fruit>
</groceries>
person Acorn    schedule 02.11.2011
comment
Вы только что получили для меня все ответы, связанные с lxml, не так ли? ;-) - person ewok; 02.11.2011
comment
Можете ли вы прокомментировать различия между этим ответом и ответом Седрика? - person ewok; 02.11.2011
comment
Ах, я упустил из виду тот факт, что .remove() требует, чтобы элемент был дочерним по отношению к элементу, для которого вы его вызываете. Поэтому вам нужно вызвать его для родителя элемента, который вы хотите удалить. Ответ исправлен. - person Acorn; 02.11.2011
comment
@Acorn: вот и все, если бы удаляемый элемент не находился непосредственно под корневым узлом, это привело бы к сбою. - person Cédric Julien; 02.11.2011
comment
понял. Должен ли это быть ребенок или любой потомок? Я спрашиваю, потому что, учитывая тот факт, что выражение xpath выполняется для tree, можно быть уверенным, что любой возвращаемый элемент является потомком tree, и поэтому tree.remove() будет работать правильно. - person ewok; 02.11.2011
comment
@ewok: это должен быть ребенок. Попробуйте tree.remove(bad) с обновленным xml выше, и вы увидите исключение. - person Acorn; 02.11.2011
comment
@ewok: дайте Седрику согласие, так как он ответил на 1 секунду раньше меня и, что более важно, его ответ был правильным :) - person Acorn; 02.11.2011
comment
Если вы можете удалить только дочерний элемент элемента, как удалить корневой элемент? - person davidA; 20.07.2016

Я встретил одну ситуацию:

<div>
    <script>
        some code
    </script>
    text here
</div>

div.remove(script) удалит часть text here, которую я не хотел.

после ответа здесь , я обнаружил, что etree.strip_elements является лучшим решением для меня, которое вы можете контролировать, будете ли вы удалять текст позади с параметром with_tail=(bool).

Но все же я не знаю, может ли это использовать фильтр xpath для тега. Просто поставьте это для информирования.

Вот документ:

strip_elements(tree_or_element, *tag_names, with_tail=True)

Удалить все элементы с указанными именами тегов из дерева или поддерева. Это приведет к удалению элементов и всего их поддерева, включая все их атрибуты, текстовое содержимое и потомков. Он также удалит хвостовой текст элемента, если вы явно не установите для параметра аргумента ключевого слова with_tail значение False.

Имена тегов могут содержать подстановочные знаки, например _Element.iter.

Обратите внимание, что это не удалит элемент (или корневой элемент ElementTree), который вы передали, даже если он совпадает. Он будет лечить только своих потомков. Если вы хотите включить корневой элемент, проверьте имя его тега непосредственно перед вызовом этой функции.

Пример использования::

   strip_elements(some_element,
       'simpletagname',             # non-namespaced tag
       '{http://some/ns}tagname',   # namespaced tag
       '{http://some/other/ns}*'    # any tag from a namespace
       lxml.etree.Comment           # comments
       )
person zephor    schedule 28.12.2016
comment
Обратите внимание, что strip_elementsstrip_tags тоже) удаляет все элементы-потомки, имя тега которых совпадает с одним из имен *tag_names*. - person Laurent LAPORTE; 17.03.2021

Как уже упоминалось, вы можете использовать метод remove() для удаления (под)элементов из дерева:

for bad in tree.xpath("//fruit[@state=\'rotten\']"):
  bad.getparent().remove(bad)

Но он удаляет элемент, включая его tail, что является проблемой, если вы обрабатываете документы со смешанным содержимым, такие как HTML:

<div><fruit state="rotten">avocado</fruit> Hello!</div>

становится

<div></div>

Я полагаю, что вы не всегда хотите :) Я создал вспомогательную функцию, чтобы удалить только элемент и сохранить его хвост:

def remove_element(el):
    parent = el.getparent()
    if el.tail.strip():
        prev = el.getprevious()
        if prev:
            prev.tail = (prev.tail or '') + el.tail
        else:
            parent.text = (parent.text or '') + el.tail
    parent.remove(el)

for bad in tree.xpath("//fruit[@state=\'rotten\']"):
    remove_element(bad)

Таким образом, он сохранит хвостовой текст:

<div> Hello!</div>
person Messa    schedule 01.12.2018
comment
Проверьте el.tail is not None, так как может быть такой случай. - person Eivydas Vilčinskas; 17.01.2019

Вы также можете использовать html из lxml, чтобы решить эту проблему:

from lxml import html

xml="""
<groceries>
  <fruit state="rotten">apple</fruit>
  <fruit state="fresh">pear</fruit>
  <fruit state="fresh">starfruit</fruit>
  <fruit state="rotten">mango</fruit>
  <fruit state="fresh">peach</fruit>
</groceries>
"""

tree = html.fromstring(xml)

print("//BEFORE")
print(html.tostring(tree, pretty_print=True).decode("utf-8"))

for i in tree.xpath("//fruit[@state='rotten']"):
    i.drop_tree()

print("//AFTER")
print(html.tostring(tree, pretty_print=True).decode("utf-8"))

Это должно вывести это:

//BEFORE
<groceries>
  <fruit state="rotten">apple</fruit>
  <fruit state="fresh">pear</fruit>
  <fruit state="fresh">starfruit</fruit>
  <fruit state="rotten">mango</fruit>
  <fruit state="fresh">peach</fruit>
</groceries>


//AFTER
<groceries>

  <fruit state="fresh">pear</fruit>
  <fruit state="fresh">starfruit</fruit>

  <fruit state="fresh">peach</fruit>
</groceries>
person Guven Degirmenci    schedule 23.11.2019

Функция remove отсоединяет элемент от дерева и, следовательно, удаляет узел XML (элемент, PI или комментарий), его содержимое (элементы-потомки) и текст tail. Здесь сохранение текста tail излишне, потому что он содержит только пробелы и новую строку, которые можно считать игнорируемыми пробелами.

Чтобы удалить элемент (и его содержимое), сохранив его tail, вы можете использовать следующую функцию:

def remove_node(child, keep_content=False):
    """
    Remove an XML element, preserving its tail text.

    :param child: XML element to remove
    :param keep_content: ``True`` to keep child text and sub-elements.
    """
    parent = child.getparent()
    parent_text = parent.text or u""
    prev_node = child.getprevious()
    if keep_content:
        # insert: child text
        child_text = child.text or u""
        if prev_node is None:
            parent.text = u"{0}{1}".format(parent_text, child_text) or None
        else:
            prev_tail = prev_node.tail or u""
            prev_node.tail = u"{0}{1}".format(prev_tail, child_text) or None
        # insert: child elements
        index = parent.index(child)
        parent[index:index] = child[:]
    # insert: child tail
    parent_text = parent.text or u""
    prev_node = child.getprevious()
    child_tail = child.tail or u""
    if prev_node is None:
        parent.text = u"{0}{1}".format(parent_text, child_tail) or None
    else:
        prev_tail = prev_node.tail or u""
        prev_node.tail = u"{0}{1}".format(prev_tail, child_tail) or None
    # remove: child
    parent.remove(child)

Вот демо:

from lxml import etree

tree = etree.XML(u"<root>text <bad>before <bad>inner</bad> after</bad> tail</root>")
bad1 = tree.xpath("//bad[1]")[0]
remove_node(bad1)

etree.dump(tree)
# <root>text  tail</root>

Если вы хотите сохранить содержимое, вы можете сделать следующее:

tree = etree.XML(u"<root>text <bad>before <bad>inner</bad> after</bad> tail</root>")
bad1 = tree.xpath("//bad[1]")[0]
remove_node(bad1, keep_content=True)

etree.dump(tree)
# <root>text before <bad>inner</bad> after tail</root>
person Laurent LAPORTE    schedule 17.03.2021