Usar Python y lxml para eliminar solo las tags que tienen ciertos atributos / valores

Estoy familiarizado con los métodos strip_tags y strip_elements , pero estoy buscando una forma sencilla de eliminar tags (y dejar su contenido) que solo contenga atributos / valores particulares.

Por ejemplo: me gustaría quitar todas las tags span o div (u otros elementos) de un árbol ( xhtm l) que tiene un atributo / valor class='myclass' (conservar el contenido del elemento como strip_tags haría). Mientras tanto, los mismos elementos que no tienen class='myclass' deben permanecer intactos.

A la inversa: me gustaría una forma de quitar todos los spans o divs “desnudos” de un árbol. Es decir, solo los spans / divs (o cualquier otro elemento) que no tienen ningún atributo. Dejando intactos los mismos elementos que tienen atributos (cualesquiera).

Siento que me estoy perdiendo algo obvio, pero he estado buscando sin suerte durante bastante tiempo.

HTML

lxml elementos HTML de lxml tienen un método drop_tag() que puede invocar en cualquier elemento de un árbol analizado por lxml.html .

Actúa de forma similar a strip_tags que elimina el elemento, pero retiene el texto, y puede strip_tags en el elemento, lo que significa que puede seleccionar fácilmente los elementos que no le interesan con una expresión XPath , y luego hacer un bucle sobre ellos y eliminarlos:

doc.html

   
This is some Text.
Some more text.
Yet another line of text.
This span will get removed as well.
Nested elements will be left alone.
Unless they also match.

strip.py

 from lxml import etree from lxml import html doc = html.parse(open('doc.html')) spans_with_attrs = doc.xpath("//span[@attr='foo']") for span in spans_with_attrs: span.drop_tag() print etree.tostring(doc) 

Salida:

   
This is some Text.
Some more text.
Yet another line of text.
This span will get removed as well.
Nested elements will be left alone.
Unless they also match.

En este caso, la expresión XPath //span[@attr='foo'] selecciona todos los elementos span con un atributo attr de valor foo . Consulte este tutorial de XPath para obtener más detalles sobre cómo construir expresiones XPath.

XML / XHTML

Edición : Acabo de notar que mencionas específicamente XHTML en tu pregunta, que según los documentos se analiza mejor como XML. Desafortunadamente, el método drop_tag() realmente solo está disponible para elementos en un documento HTML.

Así que para XML es un poco más complicado:

doc.xml

  This is some text. Only this first span should be removed.  

strip.py

 from lxml import etree def strip_nodes(nodes): for node in nodes: text_content = node.xpath('string()') # Include tail in full_text because it will be removed with the node full_text = text_content + (node.tail or '') parent = node.getparent() prev = node.getprevious() if prev: # There is a previous node, append text to its tail prev.tail += full_text else: # It's the first node in , append to parent's text parent.text = (parent.text or '') + full_text parent.remove(node) doc = etree.parse(open('doc.xml')) nodes = doc.xpath("//span[@attr='foo']") strip_nodes(nodes) print etree.tostring(doc) 

Salida:

  This is some text. Only this first span should be removed.  

Como puede ver, esto reemplazará al nodo y todos sus elementos secundarios con el contenido de texto recursivo. Realmente espero que sea lo que quieres, de lo contrario las cosas se complican aún más 😉

NOTA La última edición ha cambiado el código en cuestión.

Acabo de tener el mismo problema, y ​​después de un poco de consideración tuve una idea bastante intrépida, que se tomó de la expresión de marcado en Perl onliners: ¿Algo improbable, y luego despojar todos esos elementos?

Sí, esto no es absolutamente limpio y robusto, ya que siempre puede tener un documento que realmente use el nombre de etiqueta “improbable” que ha elegido, pero el código resultante es bastante limpio y fácil de mantener. Si realmente necesitas asegurarte de que el nombre “improbable” que hayas escogido no exista ya en el documento, siempre puedes verificar si existe y hacer el cambio de nombre solo si no puedes encontrar ninguna preexistente. Etiquetas de ese nombre.

doc.xml

  This is some text. Only this first span should be removed.  

strip.py

 from lxml import etree xml = etree.parse("doc.xml") deltag ="xxyyzzdelme" for el in xml.iterfind("//span[@attr='foo']"): el.tag = deltag etree.strip_tag(xml, deltag) print(etree.tostring(xml, encoding="unicode", pretty_print=True)) 

Salida

  This is some text. Only this first span should be removed.  

Tengo el mismo problema. Pero en mi caso, el escenario es un poco más fácil, tengo una opción, no eliminar tags, solo borrarlo, nuestros usuarios ven el HTML procesado y, por ejemplo, si tengo

 
Hello awesome World!

Quiero borrar la etiqueta strong por css selector div > strong y guardar el contexto de la cola, en lxml no puedes usar strip_tags con keep_tail por selector, solo puedes eliminar por etiqueta, me vuelve loco. Y más aún si simplemente elimina el nodo awesome , también elimina esta cola: “¡Mundo!”, Texto que envuelve la etiqueta strong . La salida será como:

 
Hello

Para mi ok esto:

 
Hello World!

Ya no es impresionante para el usuario.

 doc = lxml.html.fromstring(markup) selector = lxml.cssselect.CSSSelector('div > strong') for el in list(selector(doc)): if el.tail: tail = el.tail el.clear() el.tail = tail else: #if no tail, we can safety just remove node el.getparent().remove(el) 

Puede adaptar el código con la etiqueta física de eliminación strong con el elemento element.remove(child) y adjuntarlo al padre, pero en mi caso fue una sobrecarga.