我一直在将原始的xml.etree.ElementTree
(ET
)代码转换为lxml.etree
(lxmlET
)。幸运的是,两者之间有很多相似之处。 然而,我偶然发现了一些我在任何文档中都找不到的奇怪行为。它考虑了后代节点的内部表示。
在ET中,iter()
用于迭代Element的所有后代,可选地按标记名称进行过滤。因为我在文档中找不到关于此的任何细节,所以我期望lxmlET的类似行为。问题是,通过测试我得出结论,在lxmlET中,存在一个不同的树内部表示。
在下面的示例中,我迭代树中的节点并打印每个节点的子节点,但此外我还创建了这些子节点的所有不同组合并打印它们。这意味着,如果元素有子('A', 'B', 'C')
,我会创建更改,即树[('A'), ('A', 'B'), ('A', 'C'), ('B'), ('B', 'C'), ('C')]
。
# import lxml.etree as ET
import xml.etree.ElementTree as ET
from itertools import combinations
from copy import deepcopy
def get_combination_trees(tree):
children = list(tree)
for i in range(1, len(children)):
for combination in combinations(children, i):
new_combo_tree = ET.Element(tree.tag, tree.attrib)
for recombined_child in combination:
new_combo_tree.append(recombined_child)
# when using lxml a deepcopy is required to make this work (or make change in parse_xml)
# new_combo_tree.append(deepcopy(recombined_child))
yield new_combo_tree
return None
def parse_xml(tree_p):
for node in ET.fromstring(tree_p):
if not node.tag == 'node_main':
continue
# replace by node.xpath('.//node') for lxml (or use deepcopy in get_combination_trees)
for subnode in node.iter('node'):
children = list(subnode)
if children:
print('-'.join([child.attrib['id'] for child in children]))
else:
print(f'node {subnode.attrib["id"]} has no children')
for combo_tree in get_combination_trees(subnode):
combo_children = list(combo_tree)
if combo_children:
print('-'.join([child.attrib['id'] for child in combo_children]))
return None
s = '''<root>
<node_main>
<node id="1">
<node id="2" />
<node id="3">
<node id="4">
<node id="5" />
</node>
<node id="6" />
</node>
</node>
</node_main>
</root>
'''
parse_xml(s)
这里的预期输出是用连字符连接在一起的每个节点的子节点的id,以及自上而下的广度优先方式的子节点的所有可能组合(参见上文)。
2-3
2
3
node 2 has no children
4-6
4
6
5
node 5 has no children
node 6 has no children
但是,当您使用lxml
模块而不是xml
时(取消注释lxmlET的导入并注释ET的导入),并运行代码,您将看到输出
2-3
2
3
node 2 has no children
因此永远不会访问更深层次的后代节点。这可以通过以下任一方式来规避:
deepcopy
(评论/取消注释get_combination_trees()
中的相关部分)或for subnode in node.xpath('.//node')
中的parse_xml()
代替iter()
。所以我知道有一种解决方法,但我主要想知道发生了什么?!我花了很长时间来调试这个,而且我找不到任何关于它的文档。发生了什么,这两个模块之间的实际底层差异是什么?在使用非常大的树木时,最强大的有效解决方法是什么?
答案 0 :(得分:6)
虽然路易斯的答案是正确的,但我完全同意在您遍历它时修改数据结构通常是一个坏主意(tm),您还问为什么代码适用于xml.etree.ElementTree
和不是lxml.etree
而且有一个非常合理的解释。
.append
xml.etree.ElementTree
此库直接在Python中实现,可能因您使用的Python运行时而异。假设您正在使用CPython,您正在寻找的实现已实现in vanilla Python:
def append(self, subelement):
"""Add *subelement* to the end of this element.
The new element will appear in document order after the last existing
subelement (or directly after the text, if it's the first subelement),
but before the end tag for this element.
"""
self._assert_is_element(subelement)
self._children.append(subelement)
最后一行是我们唯一关注的部分。事实证明,self._children
已初始化为towards the top of that file:
self._children = []
因此,将一个子项添加到树中只是将一个元素附加到列表中。直觉上,这正是您正在寻找的(在这种情况下),并且实现的行为完全不令人惊讶。
.append
lxml.etree
lxml
是作为Python,非平凡的Cython和C代码的混合实现的,因此通过它进行处理比纯Python实现要困难得多。首先,.append
is implemented as:
def append(self, _Element element not None):
u"""append(self, element)
Adds a subelement to the end of this element.
"""
_assertValidNode(self)
_assertValidNode(element)
_appendChild(self, element)
_appendChild
已在apihelper.pxi
中实施:
cdef int _appendChild(_Element parent, _Element child) except -1:
u"""Append a new child to a parent element.
"""
c_node = child._c_node
c_source_doc = c_node.doc
# prevent cycles
if _isAncestorOrSame(c_node, parent._c_node):
raise ValueError("cannot append parent to itself")
# store possible text node
c_next = c_node.next
# move node itself
tree.xmlUnlinkNode(c_node)
tree.xmlAddChild(parent._c_node, c_node)
_moveTail(c_next, c_node)
# uh oh, elements may be pointing to different doc when
# parent element has moved; change them too..
moveNodeToDocument(parent._doc, c_source_doc, c_node)
return 0
这里肯定会有更多的事情发生。特别是,lxml
显式地从树中删除节点,然后将其添加到其他位置。这可以防止您在操作节点时意外创建循环XML 图形(这可能与xml.etree
版本有关。)
lxml
现在我们知道xml.etree
副本节点在追加但lxml.etree
移动时,为什么这些变通办法有效?基于tree.xmlUnlinkNode
方法(实际上是defined in C inside of libxml2
),取消链接仅仅是一堆指针。因此,复制节点元数据的任何事情都可以解决问题。因为我们关心的所有元数据都是the xmlNode
struct上的直接字段,所以浅复制节点的任何内容都可以解决问题
copy.deepcopy()
绝对有效node.xpath
返回节点wrapped in proxy elements,这些节点恰好浅层复制树元数据copy.copy()
也可以解决问题new_combo_tree = []
也会为您提供与xml.etree
类似的列表。如果你真的关心性能和大树,我可能会先用copy.copy()
进行浅层复制,尽管你应该绝对描述一些不同的选项,看哪哪个最适合你。
答案 1 :(得分:4)
一般情况下,当您操作XML树并希望在树中的多个位置复制信息时(通过反对移动信息),可以安全地执行此操作一个地方到另一个地方)是对这些元素执行深度复制操作,而不是仅将它们添加到新位置。绝大多数生成树的XML解析库需要如果要复制结构,请执行深层复制。如果你不进行深层复制,他们就不会给你你想要的结果。 lxml
就是这样一个库,它要求您深层复制要复制的结构。
xml.etree.ElementTree
以.append
有效地允许您在树中的两个位置拥有相同元素的方式工作的事实是绝对不寻常在我的经验中。
你提到for subnode in node.xpath('.//node')
也解决了你的问题。请注意,如果您使用for subnode in list(node.iter('node'))
,您将获得相同的结果。这里发生的事情是使用list(node.iter('node'))
或node.xpath('.//node')
或使用deepcopy
复制节点而不是移动它们可以保护您免受代码的另一个问题: 您在修改结构时正在走路。
node.iter('node')
创建一个迭代器,在迭代它时遍历XML结构。如果将其包装在list()
中,则立即走结构并将结果放入列表中。所以你在走之前已经有效地拍摄了结构的快照。这可以防止您的步行操作受到树的更改的影响。如果执行node.xpath('.//node')
,则在执行之前还会获取树的快照,因为该方法返回节点列表。如果您执行deepcopy
个节点并附加节点的副本而不是附加原始节点,那么您在行走时不会修改您正在行走的树。
您是否可以使用XPath或使用node.xpath('.//node')
而不是使用deepcopy
取决于您计划对组合执行的操作。您在问题中显示的代码会在创建后立即将组合打印到屏幕上。打印时它们看起来很好,但是如果你不使用deepcopy
来创建它们,那么只要你创建一个新组合,旧的组合就会搞砸,因为旧组合中出现的任何节点并且需要显示在新的中,而不是复制。
使用非常大的树木时,最有效的解决方法是什么?
这取决于您的应用程序的细节和您需要解析的数据。你举了一个例子,这是一个小文件,但你问的是“大树”。什么适用于小型文件不一定转移到大型文件。您可以针对案例X进行优化,但如果案例X在真实数据中极少发生,那么您的优化可能不会成功。在某些情况下,它实际上可能是有害的。
在我的一个应用程序中,我不得不用结构本身替换对某些结构的引用。简化图示将是包含<define id="...">...</def>
等元素和<ref idref="..."/>
等引用的文档。 ref
的每个实例都必须替换为它指向的define
。一般来说,这可能意味着多次复制一个define
,但有时define
可能仅由一个ref
引用,因此一个优化是检测到这一点并在这些情况下跳过深层复制那里只有一个参考。我“免费”获得此优化,因为应用程序已经需要记录ref
和define
的每个实例用于其他目的。如果我不得不为这个优化添加簿记,那么不清楚它是否值得。
答案 2 :(得分:1)
一开始我并没有想到会有这样的差异(我也没有检查过),但是@ supersam654和@Louis的答案都非常明确地指出了它。
但代码依赖于内部代表性(而不是接口)它使用的东西,似乎不对 (从设计 PoV )到我。另外,正如我在评论中提到的那样: combo_children 似乎完全没用:
当事情可以轻松完成时:
显然, combo_children 方法也暴露了模块之间的行为差异。
code_orig_lxml.py :
import lxml.etree as ET
#import xml.etree.ElementTree as ET
from itertools import combinations
from copy import deepcopy
def get_combination_trees(tree):
children = list(tree)
for i in range(1, len(children)):
for combination in combinations(children, i):
#new_combo_tree = ET.Element(tree.tag, tree.attrib)
#for recombined_child in combination:
#new_combo_tree.append(recombined_child)
# when using lxml a deepcopy is required to make this work (or make change in parse_xml)
# new_combo_tree.append(deepcopy(recombined_child))
#yield new_combo_tree
yield combination
return None
def parse_xml(tree_p):
for node in ET.fromstring(tree_p):
if not node.tag == 'node_main':
continue
# replace by node.xpath('.//node') for lxml (or use deepcopy in get_combination_trees)
for subnode in node.iter('node'):
children = list(subnode)
if children:
print('-'.join([child.attrib['id'] for child in children]))
else:
print(f'node {subnode.attrib["id"]} has no children')
#for combo_tree in get_combination_trees(subnode):
for combo_children in get_combination_trees(subnode):
#combo_children = list(combo_tree)
if combo_children:
print('-'.join([child.attrib['id'] for child in combo_children]))
return None
s = """
<root>
<node_main>
<node id="1">
<node id="2" />
<node id="3">
<node id="4">
<node id="5" />
</node>
<node id="6" />
</node>
</node>
</node_main>
</root>
"""
parse_xml(s)
备注强>:
<强>输出强>:
(py36x86_test) e:\Work\Dev\StackOverflow\q050749937>"e:\Work\Dev\VEnvs\py36x86_test\Scripts\python.exe" code_orig_lxml.py 2-3 2 3 node 2 has no children 4-6 4 6 5 node 5 has no children node 6 has no children
在我调查的过程中,我进一步修改了代码,以便:
xml_data.py :
DATA = """
<root>
<node_main>
<node id="1">
<node id="2" />
<node id="3">
<node id="4">
<node id="5" />
</node>
<node id="6" />
</node>
</node>
</node_main>
</root>
"""
code.py :
import sys
import xml.etree.ElementTree as xml_etree_et
import lxml.etree as lxml_etree
from itertools import combinations
from xml_data import DATA
MAIN_NODE_NAME = "node_main"
def get_children_combinations(tree):
children = list(tree)
for i in range(1, len(children)):
yield from combinations(children, i)
def get_tree(xml_str, parse_func, tag=None):
root_node = parse_func(xml_str)
if tag:
return [item for item in root_node if item.tag == tag]
return [root_node]
def process_xml(xml_node):
for node in xml_node.iter("node"):
print(f"\nNode ({node.tag}, {node.attrib['id']})")
children = list(node)
if children:
print(" Children: " + " - ".join([child.attrib["id"] for child in children]))
for children_combo in get_children_combinations(node):
if children_combo:
print(" Combo: " + " - ".join([child.attrib["id"] for child in children_combo]))
def main():
parse_funcs = (xml_etree_et.fromstring, lxml_etree.fromstring)
for func in parse_funcs:
print(f"\nParsing xml using: {func.__module__} {func.__name__}")
nodes = get_tree(DATA, func, tag=MAIN_NODE_NAME)
for node in nodes:
print(f"\nProcessing node: {node.tag}")
process_xml(node)
if __name__ == "__main__":
print("Python {:s} on {:s}\n".format(sys.version, sys.platform))
main()
<强>输出强>:
(py36x86_test) e:\Work\Dev\StackOverflow\q050749937>"e:\Work\Dev\VEnvs\py36x86_test\Scripts\python.exe" code.py Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32 Parsing xml using: xml.etree.ElementTree XML Processing node: node_main Node (node, 1) Children: 2 - 3 Combo: 2 Combo: 3 Node (node, 2) Node (node, 3) Children: 4 - 6 Combo: 4 Combo: 6 Node (node, 4) Children: 5 Node (node, 5) Node (node, 6) Parsing xml using: lxml.etree fromstring Processing node: node_main Node (node, 1) Children: 2 - 3 Combo: 2 Combo: 3 Node (node, 2) Node (node, 3) Children: 4 - 6 Combo: 4 Combo: 6 Node (node, 4) Children: 5 Node (node, 5) Node (node, 6)