Использование Python и lxml для удаления только тегов, которые имеют определенные атрибуты/значения

Я знаком с методами etree strip_tags и strip_elements, но я ищу простой способ удаления тегов (и оставляя их содержимое), которые содержат только определенные атрибуты/значения.

Например: я хотел бы удалить все теги span или div (или другие элементы) из дерева (xhtm l), которые имеют атрибут/значение class='myclass' (сохраняя содержимое элемента как strip_tags). Между тем те же элементы, которые не имеют class='myclass', должны оставаться нетронутыми.

И наоборот: я хотел бы удалить все "голые" spans или divs из дерева. Значит только те spans/divs (или любые другие элементы в этом отношении), которые не имеют абсолютно никаких атрибутов. Оставляя те же самые элементы, у которых есть атрибуты (любые) нетронутые.

Я чувствую, что мне не хватает чего-то очевидного, но я довольно долго искал поиски.

Ответ 1

HTML

lxml Элементы HTML имеют метод drop_tag(), который вы можете вызвать для любого элемента в дереве, обработанном lxml.html.

Он действует подобно strip_tags тем, что он удаляет элемент, но сохраняет текст, и его можно вызвать на элементе - это означает, что вы можете легко выбрать элементы, которые вам не интересны, с помощью XPath, а затем перебирать их и удалять:

doc.html

<html>
    <body>
        <div>This is some <span attr="foo">Text</span>.</div>
        <div>Some <span>more</span> text.</div>
        <div>Yet another line <span attr="bar">of</span> text.</div>
        <div>This span will get <span attr="foo">removed</span> as well.</div>
        <div>Nested elements <span attr="foo">will <b>be</b> left</span> alone.</div>
        <div>Unless <span attr="foo">they <span attr="foo">also</span> match</span>.</div>
    </body>
</html>

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)

Вывод:

<html>
    <body>
        <div>This is some Text.</div>
        <div>Some <span>more</span> text.</div>
        <div>Yet another line <span attr="bar">of</span> text.</div>
        <div>This span will get removed as well.</div>
        <div>Nested elements will <b>be</b> left alone.</div>
        <div>Unless they also match.</div>
    </body>
</html>

В этом случае выражение XPath //span[@attr='foo'] выбирает все элементы span с атрибутом attr значения foo. См. Этот учебник XPath для получения более подробной информации о том, как создавать выражения XPath.

XML/XHTML

Изменить. Я просто заметил, что вы конкретно упоминаете XHTML в своем вопросе, который в соответствии с документами лучше анализируется как XML. К сожалению, метод drop_tag() действительно доступен только для элементов в документе HTML.

Итак, для XML это немного сложнее:

doc.xml

<document>
    <node>This is <span>some</span> text.</node>
    <node>Only this <span attr="foo">first <b>span</b></span> should <span>be</span> removed.</node>
</document>

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 the first node in <parent/>, append to parent 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)

Вывод:

<document>
    <node>This is <span>some</span> text.</node>
    <node>Only this first span should <span>be</span> removed.</node>
</document>

Как вы можете видеть, это заменит node и все его дочерние элементы рекурсивным текстовым контентом. Я действительно надеюсь, что вы хотите, иначе все станет еще сложнее: -)

ПРИМЕЧАНИЕ Последнее изменение изменило данный код.

Ответ 2

У меня была одна и та же проблема, и после некоторого объяснения была эта довольно хакерская идея, которая заимствована из regex-ing Markup в Perl onliners: как насчет первого захвата всех нежелательных элементов со всей мощью, которую приносит element.iterfind, переименование эти элементы к чему-то маловероятному, а затем разделите все эти элементы?

Да, это не совсем чистый и надежный, поскольку у вас всегда может быть документ, на самом деле использующий "маловероятное" имя тега, которое вы выбрали, но полученный код довольно чистый и легко ремонтируемый. Если вам действительно нужно быть уверенным, что любое "маловероятное" имя, которое вы выбрали, уже не существует в документе, вы всегда можете проверить его существующий и переименовать, только если вы не можете найти какие-либо ранее существовавшие теги этого имени.

doc.xml

<document>
    <node>This is <span>some</span> text.</node>
    <node>Only this <span attr="foo">first <b>span</b></span> should <span>be</span> removed.</node>
</document>

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))

Выход

<document>
     <node>This is <span>some</span> text.</node>
     <node>Only this first <b>span</b> should <span>be</span> removed.</node>
</document>

Ответ 3

У меня та же проблема. Но в моем случае сценарий немного проще, у меня есть опция - не удалять теги, просто очистить их, наши пользователи видят визуализированный html, и если у меня есть, например,

<div>Hello <strong>awesome</strong> World!</div>

Я хочу очистить тег strong css selector div > strong и сохранить контекст хвоста, в lxml вы не можете использовать strip_tags с keep_tail по селектору, вы можете удалить только тегом, что делает меня сумасшедшим. И более того, если вы просто удалите <strong>awesome</strong> node, вы также удалите этот хвост - "Мир!", Текст, обернутый тегом strong. Вывод будет выглядеть следующим образом:

<div>Hello</div>

Для меня это нормально:

<div>Hello <strong></strong> World!</div>

Нет awesome для пользователя.

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)

Вы можете адаптировать код с помощью физического удаления тега strong с помощью вызова element.remove(child) и прикрепить его к родительскому объекту, но для моего случая это было накладным.