XML.etree.ElementTree против lxml.etree: различное представление внутреннего узла?

я трансформировал некоторые из моих оригинальных xml.etree.ElementTree (ET) код lxml.etree (lxmlET). К счастью, между ними много общего. , я наткнулся на какое-то странное поведение, которое я не могу найти в какой-либо документации. Рассматривается внутреннее представление узлов-потомков.

In 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)

ожидаемым результатом здесь является идентификатор дочерних элементов каждого узла, соединенных вместе дефисом, а также все возможные комбинации дочерних элементов (см. выше) в нисходящей широте-первый способ.

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

так чем глубже потомок узлы никогда не посещаются. Это можно обойти либо:

  1. используя deepcopy (закомментировать/раскомментировать соответствующую часть в get_combination_trees()), или
  2. используя for subnode in node.xpath('.//node') на parse_xml() вместо iter().

поэтому я знаю, что есть способ обойти это, но мне в основном интересно что происходит?! мне потребовались века, чтобы отладить это, и я не могу найти никакой документации по этому поводу. Что происходит, что такое фактический базовый разница между двумя модулями? И что самое эффективное work-around при работе с очень большими деревьями?

3 ответов


в то время как ответ Луиса правильный, и я полностью согласен с тем, что изменение структуры данных, как вы пересекаете его, как правило, плохая идея(tm), вы также спросили, почему код работает с xml.etree.ElementTree, а не lxml.etree и этому есть очень разумное объяснение.

реализация .append на xml.etree.ElementTree

эта библиотека реализована непосредственно в Python и может варьироваться в зависимости от того, какую среду выполнения Python вы используете. Предполагая, что вы используете CPython, реализация вы ищете реализуется в ванили на 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 инициализируется в верхней части этого файла as:

self._children = []

таким образом, добавление ребенка в дерево-это просто добавление элемента в список. Интуитивно, это именно то, что вы ищете (в этом случае) , и реализация ведет себя совершенно неудивительно путь.

реализация .append на lxml.etree

lxml реализован как смесь Python, нетривиального Cython и кода C, поэтому спелеология через него была значительно сложнее, чем реализация pure-Python. Во-первых, .append реализуется как:

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 способ (что на самом деле определено в C внутри libxml2), unlinking просто путает с кучей указателей. Таким образом, все, что копирует метаданные узла, будет делать трюк. Потому что все метаданные, о которых мы заботимся, являются прямыми полями на the xmlNode struct, что мелкий копии узлов сделают трюк

  • copy.deepcopy() наверняка работает
  • node.xpath возвращает узлы завернутый в прокси-элементы что происходит с мелкой копией метаданных дерева
  • copy.copy() также делает трюк
  • Если вам не нужны ваши комбинации, чтобы на самом деле быть в официальном дереве, установка new_combo_tree = [] также дает вам список, добавляя так же, как xml.etree.

если вы действительно беспокоитесь о производительности и больших деревьях, я бы, вероятно, начал с мелкого копирования с copy.copy() хотя вы должны абсолютно профиль несколько различных вариантов и посмотреть, какие один лучше подходит для вас.


Проблема Копирования

в общем, безопасная вещь, чтобы сделать, когда вы манипулируете 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 узлов и добавьте копию узла вместо добавления исходного узла, затем вы are не изменяя дерево вы идете, когда вы идете его.

можете ли вы уйти с помощью XPath или с помощью node.xpath('.//node') вместо используя deepcopy зависит от того, что вы планируете делать с вашим комбинаций. Код, который вы показываете в своем вопросе, выводит комбинации на экран, как только вы их создаете. Они выглядят хорошо, когда вы их печатаете, но если вы не используете deepcopy для их создания, а затем, как только вы создадите новую комбинацию, старый будет перепутан, потому что любой узел, который появился в старой комбинации и должен появиться в новом будет перемещен, а не скопирован.


и что является наиболее эффективной работой при работе с очень большими деревьями?

это зависит от специфики вашего приложения и данные нужно парсить. Вы привели один пример, который является небольшим документом, но вы спрашиваете о "больших деревьях". Что касается малые документы не обязательно переносятся на большие документы. Вы можете оптимизировать для случая X, но если случай X происходит крайне редко в реальные data, тогда ваша оптимизация может не сработать. В некоторых случаях это может быть действительно вредно.

в одном из моих приложений мне пришлось заменить ссылки на некоторые структуры самими структурами. Упрощенной иллюстрацией будет документ, содержащий такие элементы, как <define id="...">...</def> и ссылки вроде <ref idref="..."/>. Каждый экземпляр ref придется заменить на define он указывает. В общем, это может означать копирование одного define несколько раз, но иногда define может ссылаться только один ref таким образом, одна оптимизация заключалась в том, чтобы обнаружить это и пропустить глубокую копию в тех случаях, когда была только одна ссылка. Я получил эту оптимизацию "бесплатно", потому что приложение уже требовало записи каждого экземпляра ref и define для других целей. Если бы мне пришлось добавить бухгалтерия только для этой оптимизации, не ясно, стоило бы это делать.


в начале я не думал, что есть такая разница (и я не проверял), но и ответы @supersam654 и @Louis указали ее очень четко.

но код, который зависит от внутреннее представление (а не интерфейс) вещей, которые он использует, не так (от design PoV) для меня. Кроме того, как я просил в своем комментарии:combo_children кажется абсолютно бесполезно:

  1. получить дочерние узлы combo (в виде списка)
  2. добавлять каждый узел из списка, как ребенок combo_children
  3. возвращение combo_children
  4. Get combo_children дети (в виде списка)
  5. используйте список (combo)

когда все можно было легко сделать:

  1. получить дочерние узлы combo (в виде списка)
  2. возвращает список
  3. использовать список (комбо)

видимо,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)

Примечания:

  • это код, с изменениями выше
  • я ничего не удалил, вместо этого просто прокомментировал материал (который будет генерировать наименьший diff между старым и новым версии)

выход:

(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

пока я расследовал, я изменил ваш код дальше, чтобы:

  • Исправлена проблема
  • повысить скорость печати
  • сделайте его модульным
  • используйте оба метода разбора, чтобы сделать различия между ними яснее!--24-->

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)