lxml:Element addnext() 和 insert() 在处理尾部的区别

3 投票
2 回答
6760 浏览
提问于 2025-04-18 04:09

给定一个 lxml 元素 xml,我通过调用 c.getnext() 来遍历它的所有子元素 c[0..n]。这样做是因为如果需要插入子元素,我必须动态地进行,而使用迭代器无法做到。所有元素都有 texttail 属性。

让我用一个例子来说明 addnext()insert() 的不同表现。假设有一个简单的 XML 字符串,我把它解析成一个 lxml 树,然后为了确保一切正常,检查一下它:

>>> import lxml.etree
>>> s = "<p>This is <b>bold</b> and this is italic text.</p>"
# Create a new lxml element.
>>> xml = lxml.etree.fromstring(s)
# Let's look at the element, its child, and all the texts and tails.
>>> lxml.etree.tostring(xml)
b'<p>This is <b>bold</b> and this is italic text.</p>'
>>> xml.text
'This is '
>>> xml.tail
>>> xml[0].text
'bold'
>>> xml[0].tail
' and this is italic text.'

到目前为止一切正常,正是我所期待的(关于 lxml 表示的更多信息可以查看 这里)。

现在我想把“italic”这个词用标签包裹起来,就像“bold”被包裹在 <b> 标签中一样。为此,我首先找到“italic”子字符串开始的索引:

# Find the index of the "italic" substring.
>>> idx = xml[0].tail.find("italic")
>>> idx
13

然后我创建一个新的 lxml 元素:

# Create a new element and inspect it.
>>> new_c = lxml.etree.fromstring("<i>italic</i>")
>>> new_c.text
'italic'
>>> new_c.tail
>>>

为了正确地将这个新元素插入到 XML 树中,我必须把原来的 xml[0].tail 字符串分成两个子字符串,并从中去掉“italic”:

>>> new_c.tail = xml[0].tail[idx+len("italic"):]
>>> xml[0].tail = xml[0].tail[:idx]

现在一切都准备好了,可以将这个新元素插入到 xml 元素中,而这正是让我困惑的地方。将新子元素 new_c 插入到指定的 xml[0] 之后,结果却不同,而 Element API 并没有给我提供任何新信息:

# Adds the element as a following sibling directly after this element.
# Note that tail text is automatically discarded when adding at the root level.
>>> xml[0].addnext(new_c)
>>> lxml.etree.tostring(xml)
b'<p>This is <b>bold</b><i>italic</i> text. and this is </p>'

还有

# Inserts a subelement at the given position in this element
>>> xml.insert(1 + xml.index(xml[0]), new_c)
>>> lxml.etree.tostring(xml)
b'<p>This is <b>bold</b> and this is <i>italic</i> text.</p>'

这两个调用似乎对 tail 的处理方式不同(请参见关于 addnext() 的评论,涉及 tail)。即使考虑到评论,文本并没有从 <b> 中丢失,而是附加到了 <i> 中,根级别的处理方式也与更深层级别没有区别(也就是说,通过将原始 XML 包裹在额外的 <foo> 标签中,可以观察到完全相同的行为)。

我在这里漏掉了什么呢?

编辑 关于 lxml 邮件列表的相关讨论可以在 这里找到。

2 个回答

2

tail 这个概念只在 lxml 的层面上存在;在 libxml2 中,它就像在 DOM 中一样,是一个文本节点。这样做的主要原因是为了方便解析格式良好的 XML(http://lxml.de/tutorial.html#elements-contain-text):

这两个属性 .text 和 .tail 就足以表示 XML 文档中的任何文本内容。这样,ElementTree API 不需要除了 Element 类之外的特殊文本节点,这些特殊节点往往会造成困扰(你可能从经典的 DOM API 中知道这一点)。

所有 lxml 的功能都努力保持这种抽象,至少从源代码来看是这样的。例如,index() 只计算元素、注释、实体引用和处理指令节点,而树的操作过程似乎总是会把节点的尾部一起移动。不过,由于这个概念:

  • 文档说明得很少
  • 是为 XML 量身定做的,用户通常不关心尾部文本
  • 与常规表示法相冲突

因此在应用上似乎存在不一致的情况。如果一致性是目标的话,这看起来像是一个问题(甚至是一个 bug)。我会和维护者讨论最后的说法,以澄清这个库在处理尾部时的预期行为。

4

elem.addnext(nextelem) 是在XML层面上进行操作的,也就是说,它是在某个元素后面直接添加新的内容,并把原本在这个元素后面的文字移动到新添加的元素后面。这样做的目的是让新元素成为紧跟在原元素后面的兄弟元素。

parent.insert(where,elem) 的工作方式就像把父元素看成一个etree.Element的列表一样。它会在这个列表中插入一个新元素,而不会改变任何etree.Element实例。parent.append(elem) 也会以这种方式工作,或者说任何其他的列表操作都可以。

所以,这些函数对元素树有两种不同的看法。

>>> from lxml import etree as et
>>> 
>>> x = et.XML('<a>foo<b/>bar</a>')
>>> y = et.XML('<c>C!</c>')
>>> 
>>> et.dump(x)
<a>foo<b/>bar</a>
>>> x.find('b').addnext(y)
>>> et.dump(x)
<a>foo<b/><c>C!</c>bar</a>

尾部内容从b元素移动到c元素,这样除了插入的新元素外,XML文档的其他部分保持不变。

现在,如果插入的元素已经有尾部内容,使用addnext可以在XML元素后面插入一个元素和它后面的文本。这个插入是在XML元素后面,而不是在带有尾部的etree元素后面。

>>> x = et.XML('<a>foo<b/>bar</a>')
>>> y = et.XML('<c>C!</c>')
>>> y.tail = 'more...'
>>> 
>>> x.find('b').addnext(y)
>>> et.dump(x)
<a>foo<b/><c>C!</c>more...bar</a>

撰写回答