xml.etree.ElementTree 与 lxml.etree:不同的内部节点表示?
xml.etree.ElementTree vs. lxml.etree: different internal node representation?
我一直在将我的一些原始 xml.etree.ElementTree
(ET
) 代码转换为 lxml.etree
(lxmlET
)。幸运的是,两者之间有很多相似之处。 但是,我确实偶然发现了一些我在任何文档中都找不到的奇怪行为。它考虑了后代节点的内部表示。
在 ET 中,iter()
用于迭代元素的所有后代,可选择按标签名称过滤。因为我在文档中找不到关于此的任何详细信息,所以我希望 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
(comment/uncomment get_combination_trees()
中的相关部分),或
- 在
parse_xml()
中使用 for subnode in node.xpath('.//node')
而不是 iter()
。
所以我知道有办法解决这个问题,但我主要想知道 发生了什么?! 我花了很长时间调试它,但我找不到关于它的任何文档。发生了什么,这两个模块之间的 actual 潜在区别是什么?在处理非常大的树时,最最有效 的解决方法是什么?
复制问题
一般来说,当您正在操作一棵 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')
你也会在你遍历它之前获得树的快照,因为该方法 returns 一个节点列表。如果您对节点执行 deepcopy
并附加节点的副本而不是附加原始节点,那么您 不会修改 您正在行走的树走吧。
您是否可以使用 XPath 或使用 node.xpath('.//node')
而不是 使用 deepcopy
取决于您打算如何处理您的组合。您在问题中显示的代码会在您创建组合后立即将它们打印到屏幕上。当你打印它们时它们看起来很好,但是如果你不使用 deepcopy
来创建它们,那么一旦你创建了一个新的组合,旧的组合就会变得混乱,因为任何节点出现在旧的组合中并且需要出现在新的将被移动而不是复制。
And what is the most efficient work-around when working with very large trees?
这取决于您的应用程序的具体情况以及您需要解析的数据。你举了一个例子,它是一个小文档,但你问的是 "large trees"。适用于小文件的不一定适用于大文件。您可以针对案例 X 进行优化,但如果案例 X 在 真实 数据中很少发生,那么您的优化可能不会成功。在某些情况下,它实际上可能是有害的。
在我的一个应用程序中,我不得不用结构本身替换对某些结构的引用。一个简化的例子是包含 <define id="...">...</def>
等元素和 <ref idref="..."/>
等引用的文档。 ref
的每个实例都必须用它指向的 define
替换。通常,这可能意味着多次复制单个 define
,但有时 define
可能仅被一个 ref
引用,因此一种优化是检测这一点并在这些情况下跳过深层复制只有一个参考。我得到了这个优化 "for free" 因为应用程序已经需要记录 ref
和 define
的每个实例用于其他目的。如果我不得不添加簿记 只是为了这个优化,目前还不清楚它是否值得。
虽然 Louis 的回答是正确的,而且我完全同意在遍历数据结构时修改数据结构通常是一个坏主意(tm),但您还询问了为什么代码与 xml.etree.ElementTree
而不是 lxml.etree
,对此有一个非常合理的解释。
在 xml.etree.ElementTree
中实施 .append
此库直接在 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 = []
因此,向树中添加一个子项只是将一个元素附加到列表中。直觉上,这正是您正在寻找的(在这种情况下),并且实现的行为方式完全不足为奇。
在 lxml.etree
中实施 .append
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 graph(这可能是 xml.etree
版本可以做到的)。
lxml
的解决方法
既然我们知道 xml.etree
复制 节点,但 lxml.etree
移动 它们,为什么这些解决方法有效吗?基于 tree.xmlUnlinkNode
方法(实际上是 defined in C inside of libxml2
), unlinking just messes with a bunch of pointers. So, anything that copies node metadata will do the trick. Because all of the metadata we care about are direct fields on the xmlNode
struct,任何 shallow 复制节点都可以做到这一点
copy.deepcopy()
绝对有效
node.xpath
returns 节点 wrapped in proxy elements 恰好浅拷贝树元数据
copy.copy()
也行
- 如果您不需要您的组合实际位于官方树中,设置
new_combo_tree = []
也可以像 xml.etree
. 一样为您添加列表
如果您真的很关心性能和大树,我可能会从 copy.copy()
的浅层复制开始,尽管您绝对应该分析几个不同的选项,看看哪个最适合您。
一开始我认为没有这么大的区别(我也没查),但是@supersam654 和@Louis 的回答都说得很清楚了。
但是依赖于它使用的东西的内部表示(而不是接口)的代码,不对我来说似乎是正确的(来自设计PoV)。另外,正如我在评论中所问:combo_children 似乎完全没用:
- 获取 child 个节点组合(作为列表)
- 将列表中的每个节点作为 child 附加到 combo_children
- Returncombo_children
- 获取combo_childrenchildren(作为列表)
- 使用列表(组合)
什么时候可以轻松完成:
- 获取 child 个节点组合(作为列表)
- Return列表
- 使用列表(组合)
显然,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\Whosebug\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\Whosebug\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)
我一直在将我的一些原始 xml.etree.ElementTree
(ET
) 代码转换为 lxml.etree
(lxmlET
)。幸运的是,两者之间有很多相似之处。 但是,我确实偶然发现了一些我在任何文档中都找不到的奇怪行为。它考虑了后代节点的内部表示。
在 ET 中,iter()
用于迭代元素的所有后代,可选择按标签名称过滤。因为我在文档中找不到关于此的任何详细信息,所以我希望 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
(comment/uncommentget_combination_trees()
中的相关部分),或 - 在
parse_xml()
中使用for subnode in node.xpath('.//node')
而不是iter()
。
所以我知道有办法解决这个问题,但我主要想知道 发生了什么?! 我花了很长时间调试它,但我找不到关于它的任何文档。发生了什么,这两个模块之间的 actual 潜在区别是什么?在处理非常大的树时,最最有效 的解决方法是什么?
复制问题
一般来说,当您正在操作一棵 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')
你也会在你遍历它之前获得树的快照,因为该方法 returns 一个节点列表。如果您对节点执行 deepcopy
并附加节点的副本而不是附加原始节点,那么您 不会修改 您正在行走的树走吧。
您是否可以使用 XPath 或使用 node.xpath('.//node')
而不是 使用 deepcopy
取决于您打算如何处理您的组合。您在问题中显示的代码会在您创建组合后立即将它们打印到屏幕上。当你打印它们时它们看起来很好,但是如果你不使用 deepcopy
来创建它们,那么一旦你创建了一个新的组合,旧的组合就会变得混乱,因为任何节点出现在旧的组合中并且需要出现在新的将被移动而不是复制。
And what is the most efficient work-around when working with very large trees?
这取决于您的应用程序的具体情况以及您需要解析的数据。你举了一个例子,它是一个小文档,但你问的是 "large trees"。适用于小文件的不一定适用于大文件。您可以针对案例 X 进行优化,但如果案例 X 在 真实 数据中很少发生,那么您的优化可能不会成功。在某些情况下,它实际上可能是有害的。
在我的一个应用程序中,我不得不用结构本身替换对某些结构的引用。一个简化的例子是包含 <define id="...">...</def>
等元素和 <ref idref="..."/>
等引用的文档。 ref
的每个实例都必须用它指向的 define
替换。通常,这可能意味着多次复制单个 define
,但有时 define
可能仅被一个 ref
引用,因此一种优化是检测这一点并在这些情况下跳过深层复制只有一个参考。我得到了这个优化 "for free" 因为应用程序已经需要记录 ref
和 define
的每个实例用于其他目的。如果我不得不添加簿记 只是为了这个优化,目前还不清楚它是否值得。
虽然 Louis 的回答是正确的,而且我完全同意在遍历数据结构时修改数据结构通常是一个坏主意(tm),但您还询问了为什么代码与 xml.etree.ElementTree
而不是 lxml.etree
,对此有一个非常合理的解释。
在 xml.etree.ElementTree
中实施 .append
此库直接在 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 = []
因此,向树中添加一个子项只是将一个元素附加到列表中。直觉上,这正是您正在寻找的(在这种情况下),并且实现的行为方式完全不足为奇。
在 lxml.etree
中实施 .append
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 graph(这可能是 xml.etree
版本可以做到的)。
lxml
的解决方法
既然我们知道 xml.etree
复制 节点,但 lxml.etree
移动 它们,为什么这些解决方法有效吗?基于 tree.xmlUnlinkNode
方法(实际上是 defined in C inside of libxml2
), unlinking just messes with a bunch of pointers. So, anything that copies node metadata will do the trick. Because all of the metadata we care about are direct fields on the xmlNode
struct,任何 shallow 复制节点都可以做到这一点
copy.deepcopy()
绝对有效node.xpath
returns 节点 wrapped in proxy elements 恰好浅拷贝树元数据copy.copy()
也行- 如果您不需要您的组合实际位于官方树中,设置
new_combo_tree = []
也可以像xml.etree
. 一样为您添加列表
如果您真的很关心性能和大树,我可能会从 copy.copy()
的浅层复制开始,尽管您绝对应该分析几个不同的选项,看看哪个最适合您。
一开始我认为没有这么大的区别(我也没查),但是@supersam654 和@Louis 的回答都说得很清楚了。
但是依赖于它使用的东西的内部表示(而不是接口)的代码,不对我来说似乎是正确的(来自设计PoV)。另外,正如我在评论中所问:combo_children 似乎完全没用:
- 获取 child 个节点组合(作为列表)
- 将列表中的每个节点作为 child 附加到 combo_children
- Returncombo_children
- 获取combo_childrenchildren(作为列表)
- 使用列表(组合)
什么时候可以轻松完成:
- 获取 child 个节点组合(作为列表)
- Return列表
- 使用列表(组合)
显然,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\Whosebug\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\Whosebug\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)