如何在lxml中用文本替换元素?

13

使用lxml的ElementTree API可以轻松地从XML文档中完全删除给定元素,但我看不到一种简单的方法来始终将一个元素替换为一些文本。例如,给定以下输入:

input = '''<everything>
<m>Some text before <r/></m>
<m><r/> and some text after.</m>
<m><r/></m>
<m>Text before <r/> and after</m>
<m><b/> Text after a sibling <r/> Text before a sibling<b/></m>
</everything>
'''

你可以使用以下代码轻松删除每个 <r> 元素:

from lxml import etree
f = etree.fromstring(data)
for r in f.xpath('//r'):
    r.getparent().remove(r)
print etree.tostring(f, pretty_print=True)

不过,你如何才能用文本替换每个元素以获得以下输出:

<everything>
<m>Some text before DELETED</m>
<m>DELETED and some text after.</m>
<m>DELETED</m>
<m>Text before DELETED and after</m>
<m><b/>Text after a sibling DELETED Text before a sibling<b/></m>
</everything>

在我看来,由于ElementTree API是通过每个元素的.text.tail属性处理文本而不是树中的节点,这意味着你必须处理很多不同的情况,具体取决于该元素是否有兄弟元素,现有元素是否具有.tail属性等等。我错过了一些简单的方法吗?


如果<r/>有子元素,您是否也想将它们删除?还是合并到<r/>的父级元素中? - MattH
在这种情况下,我只想删除<r>节点及其所有子节点,并用文本字符串替换它。希望这样更容易 :) - Mark Longair
3个回答

20

我认为unutbu的XSLT解决方案可能是实现你目标的正确方式。

然而,这里有一种有点hacky的方法来实现它,通过修改标签的tail,然后使用etree.strip_elements函数。

from lxml import etree

data = '''<everything>
<m>Some text before <r/></m>
<m><r/> and some text after.</m>
<m><r/></m>
<m>Text before <r/> and after</m>
<m><b/> Text after a sibling <r/> Text before a sibling<b/></m>
</everything>
'''

f = etree.fromstring(data)
for r in f.xpath('//r'):
  r.tail = 'DELETED' + r.tail if r.tail else 'DELETED'

etree.strip_elements(f,'r',with_tail=False)

print etree.tostring(f,pretty_print=True)

给你:

<everything>
<m>Some text before DELETED</m>
<m>DELETED and some text after.</m>
<m>DELETED</m>
<m>Text before DELETED and after</m>
<m><b/> Text after a sibling DELETED Text before a sibling<b/></m>
</everything>

谢谢,这是一个不错的解决方案 - 我不知道strip_elementswith_tail选项。 - Mark Longair
4
想要继续使用lxml来处理HTML。但可能会转换到Beautifulsoup,它对基本的HTML修改更加直观,并且可以使用lxml作为解析器... soup = BeautifulSoup(text, "lxml") / soup.find_all('r').replace_with('DELETED') - benzkji
感谢@benzkij的提示!这真的很奇怪,因为在ElementTree API中,有时文本被视为其他节点的尾部,而不仅仅是xml所期望的普通文本节点。 - vlz
1
XML并没有具体意图,而你所想的DOM只是其中一种可能的对象模型。ElementTree的整个目的就是不使用DOM,如果你需要DOM,则有实现它的软件包可供选择。 - Masklinn
@Masklinn 感谢您澄清这一点!我想我已经习惯了来自其他语言/库的XML的DOM表示,以至于我认为这是表示XML的目的方式。 (仍然认为将文本作为节点放入类似元素的树中会更方便,但很高兴知道XML本身没有规定这一点) - vlz

8
使用 strip_elements 的劣势在于,您无法让它在替换其他元素时保留某些<r>元素。它还需要存在一个ElementTree实例(可能不存在)。最后,您不能使用它来替换XML注释或处理指令。 以下内容应该能够胜任您的工作:
for r in f.xpath('//r'):
    text = 'DELETED' + r.tail 
    parent = r.getparent()
    if parent is not None:
        previous = r.getprevious()
        if previous is not None:
            previous.tail = (previous.tail or '') + text
        else:
            parent.text = (parent.text or '') + text
        parent.remove(r)

2
我认为 text = 'DELETED' + r.tail 应该改为 text = 'DELETED' + r.tail if r.tail else 'DELETED' - mzjn

4

使用 ET.XSLT:

import io
import lxml.etree as ET

data = '''<everything>
<m>Some text before <r/></m>
<m><r/> and some text after.</m>
<m><r/></m>
<m>Text before <r/> and after</m>
<m><b/> Text after a sibling <r/> Text before a sibling<b/></m>
</everything>
'''

f=ET.fromstring(data)
xslt='''\
    <xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">    

    <!-- Replace r nodes with DELETED
         http://www.w3schools.com/xsl/el_template.asp -->
    <xsl:template match="r">DELETED</xsl:template>

    <!-- How to copy XML without changes
         http://mrhaki.blogspot.com/2008/07/copy-xml-as-is-with-xslt.html -->    
    <xsl:template match="*">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="@*|text()|comment()|processing-instruction">
        <xsl:copy-of select="."/>
    </xsl:template>
    </xsl:stylesheet>
'''

xslt_doc=ET.parse(io.BytesIO(xslt))
transform=ET.XSLT(xslt_doc)
f=transform(f)

print(ET.tostring(f))

产量
<everything>
<m>Some text before DELETED</m>
<m>DELETED and some text after.</m>
<m>DELETED</m>
<m>Text before DELETED and after</m>
<m><b/> Text after a sibling DELETED Text before a sibling<b/></m>
</everything>

3
+1 这是一个很好但真的不明显的答案 :) 我之所以会有这个问题,是因为我对另一个问题的回答不够充分,并希望有比这更简单的方法。即使像这样一个简短的例子,XSLT与我的问题中仅删除元素的代码相比,仍然冗长且难以理解。 - Mark Longair

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接