xml.etree.ElementTree и lxml.etree: другое представление внутреннего узла? - PullRequest
0 голосов
/ 07 июня 2018

Я преобразовал некоторые из моих исходных кодов 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)

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

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().

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

Ответы [ 3 ]

0 голосов
/ 10 июня 2018

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

Реализация .append в xml.etree.ElementTree

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

self._children = []

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

Реализация .append в lxml.etree

lxml реализованакак смесь Python, нетривиального кода Cython и кода C, настолько тщательно проработать его было значительно сложнее, чем в чисто 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), удаление связей просто мешает кучу указателей.Таким образом, все, что копирует метаданные узла, сделает свое дело.Поскольку все метаданные, которые нас интересуют, являются прямыми полями в xmlNode struct , все, что мелкие копирует узлы, справится с задачей

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

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

0 голосов
/ 13 июня 2018

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

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

  1. Получить комбинированные дочерние узлы (в виде списка)
  2. Добавить каждый узел изсписок как ребенок combo_children
  3. Return combo_children
  4. Get combo_children children (как список)
  5. Используйте список (комбинированный)

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

  1. Получите комбинированные дочерние узлы (в виде списка)
  2. Вернитеlist
  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)

Примечания :

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

Output :

(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)
0 голосов
/ 10 июня 2018

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

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

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

...