xml.etree.ElementTree vs. lxml.etree: different internal node representation?
我一直在将某些原始的
在ET中,
在下面的示例中,我遍历树中的节点并打印每个节点的子代,但此外,我还创建了这些子代的所有不同组合并打印了它们。这意味着,如果元素具有子元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | # 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,以及所有子集的所有可能组合(参见上文)(以自上而下的广度优先的方式)。
1 2 3 4 5 6 7 8 9 10 | 2-3 2 3 node 2 has no children 4-6 4 6 5 node 5 has no children node 6 has no children |
但是,当使用
1 2 3 4 | 2-3 2 3 node 2 has no children |
因此,永远不会访问更深的后代节点。可以通过以下任一方法来规避:
所以我知道有办法解决这个问题,但是我主要想知道正在发生什么?!我花了很长时间调试它,但找不到任何文档。这是怎么回事,这两个模块之间的实际根本区别是什么?在处理非常大的树木时,最有效的解决方法是什么?
尽管Louis的答案是正确的,并且我完全同意在遍历数据结构时对其进行修改通常是一个坏主意(tm),但您还询问了为什么代码使用
该库是直接在Python中实现的,并且可能会根据所使用的Python运行时而有所不同。假设您正在使用CPython,则要查找的实现是在普通Python中实现的:
1 2 3 4 5 6 7 8 | 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) |
最后一行是我们关心的唯一部分。事实证明,
1 | self._children = [] |
因此,将孩子添加到树中只是将元素添加到列表中。直观地讲,这正是您想要的(在这种情况下),实现的行为完全不令人惊讶。
1 2 3 4 5 6 7 | 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) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 |
这里肯定还有更多事情。特别地,
现在我们知道
-
copy.deepcopy() 绝对有效 -
node.xpath 返回包裹在代理元素中的节点,这些节点恰好浅复制了树元数据 -
copy.copy() 也能解决问题 -
如果您不需要组合实际上位于正式树中,则设置
new_combo_tree = [] 也会像xml.etree 一样为您提供列表附加。
如果您确实关心性能和大树,我可能会先从
复制问题
通常,在处理XML树并希望在树中的多个位置复制信息(反对将信息从一个位置移动到另一位置)时,安全的做法是对这些元素执行深度复制操作,而不是只需将它们添加到新位置即可。如果要复制结构,绝大多数生成树的XML解析库都要求您执行深层复制。如果您不进行深层复制,它们只会为您提供所需的结果。
根据我的经验,
边走边改善问题
您提到
使用XPath还是使用
And what is the most efficient work-around when working with very large trees?
这取决于您的应用程序的详细信息以及您需要解析的数据。您给出了一个小文件示例,但您询问了"大树"。适用于小文件的内容不一定会转移到大文件。您可以针对案例X进行优化,但是如果案例X仅在极少的实际数据中发生,那么您的优化可能不会成功。在某些情况下,它实际上可能是有害的。
在我的一个应用中,我不得不用结构本身替换对某些结构的引用。一个简化的插图是一个包含
一开始我并不认为有什么区别(我也没有检查),但是@ supersam654和@Louis答案都非常清楚地指出了这一点。
但是,依赖于它所使用的东西的内部表示(而不是接口)的代码在我看来(从设计PoV来看)并不正确。另外,正如我在评论中问的那样:combo_children似乎完全没有用:
当事情可以轻松完成时:
显然,combo_children方法还暴露了模块之间的行为差??异。
code_orig_lxml.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 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) |
笔记:
- 这是上面更改的代码
- 我没有删除任何内容,而是仅评论了内容(这将在新旧版本之间产生最小的差异)
输出:
1
2
3
4
5
6
7
8
9
10
11 (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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | 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" Node ({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" Parsing xml using: {func.__module__} {func.__name__}") nodes = get_tree(DATA, func, tag=MAIN_NODE_NAME) for node in nodes: print(f" Processing node: {node.tag}") process_xml(node) if __name__ =="__main__": print("Python {:s} on {:s} ".format(sys.version, sys.platform)) main() |
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 (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)