Найти индекс дочернего элемента в lxml

Я использую Python 3.12 и lxml.

Я хочу найти определенный тег и могу сделать это с помощью elem.find("tag"). elem имеет тип Element.

Но я хочу переместить дочерние элементы этого дочернего элемента в родительский элемент, где находился дочерний элемент. Для этого мне нужен индекс ребенка. И я не могу найти способ найти этот индекс.

В описании API lxml есть метод _Element.index(), но я понятия не имею, как получить экземпляр _Element из экземпляра Element.

Подскажите, пожалуйста, как определить этот индекс. (Это можно сделать с помощью цикла вместо find(), но мне нужен более аккуратный способ).

Обновлено: вот пример элемента XML

<parent>
  <child-a/>
  <container>
     <child-b/>
     <child-c/>
  </container>
  <child-d/>
  <child-e/>
</parent>

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

<parent>
  <child-a/>
  <child-b/>
  <child-c/>
  <child-d/>
  <child-e/>
</parent>

Итак, я могу найти <container>, используя parent.find(). Но чтобы переместить его дочерние элементы в то же место под <parent>, мне нужен индекс <container>, поскольку метод insert() требует индекса. На данный момент я использую этот ключ:

    while True:
        index = None
        found = None
        for i in range(len(parent)):
            if parent[i].tag = = "container":
                found = parent[i]
                index = i
                break
        if found is None:
            break

        offset = 0
        while len(found) > 0:
            parent.insert(index+offset,found[0])
            offset+=1
        parent.remove(found)

Я знаю, что offset излишне, так как можно просто увеличить index, я сделал это из эстетических соображений. Но сам цикл — это довольно сложная задача. Вот что бы я сделал, если бы у Element был метод index(), но его нет:

    found = parent.find("container")
    while found:
        index = parent.index(found)
        offset = 0
        while len(found) > 0:
            parent.insert(index+offset,found[0])
            offset+=1
        parent.remove(found)
        found = parent.find("container")

Но Element.index() не существует; _Element.index() существует, но я не знаю, как получить к нему доступ _Element.

Для этого вам не нужен индекс, для этого есть методы insert() и append(), extend(). Пожалуйста, поделитесь своим кодом и минимальным рабочим примером, включите пример входного XML и то, что вы хотите получить на выходе.

Hermann12 06.07.2024 06:19

Насколько я знаю, insert() требует индекса, но я отредактирую вопрос, чтобы добавить образец.

Mikhail Ramendik 06.07.2024 18:10

для одного ребенка вы могли бы сделать parent.replace(container, container.getchildren()[0]), но позже у вас все равно возникнут проблемы с вставкой других детей.

furas 06.07.2024 21:50

@Michail Ramendik, для вставки вам нужна позиция в списке тегов, да. Смотрите мой ответ ниже. Для этой задачи enumerate() также может оказаться полезным.

Hermann12 07.07.2024 08:25
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
4
91
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы можете использовать container.getchildren() или list(container), чтобы получить всех дочерних элементов, и использовать addnext(), чтобы поместить их (один за другим) после контейнера, а позже вы можете удалить (уже) пустой контейнер. Нужно reversed() расставить детей в правильном порядке.

parent = container.getparent()

#for child in reversed(container.getchildren()):
for child in reversed(container):
    container.addnext(child)

parent.remove(container)    

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

  • Я использовал два контейнера без отступов — для проверки последующих методов очистки отступов.
  • Я добавил child.tail = container.tail, чтобы убрать некоторые углубления.
  • Я добавил ET.indent(tree), чтобы очистить все углубления.
  • Я добавил reversed(container) вместо reversed(container.getchildren())
  • Я также добавил container.iterchildren(reversed=True) вместо reversed(container)
html = '''
<parent>
  <child-a/>
  <container><child-b/><child-c/></container>
  <child-d/>
  <container><child-e/><child-f/></container>
  <child-g/>
</parent>
'''

import lxml.html

tree = lxml.html.fromstring(html)

for container in tree.findall('container'):
    parent = container.getparent()
    #for child in reversed(container.getchildren()):  # getchildren() - deprecated
    #for child in reversed(container):
    for child in container.iterchildren(reversed=True):
        #child.tail = None   # clean indentations  # elements in one line
        #child.tail = "\n"   # clean indentations  # next tag starts in first column
        #child.tail = container.tail   # clean indentations 
        container.addnext(child)
    parent.remove(container)    

# https://lxml.de/apidoc/lxml.etree.html#lxml.etree.indent
import lxml.etree as ET
#ET.indent(tree, space='    ')  # clean all indentations - use 4 spaces
#ET.indent(tree, space='....')  # clean all indentations - use 4 dots - looks like TOC (Table Of Contents) in book :)
ET.indent(tree)   # clean all indentations - use (default) 2 spaces

#html = lxml.html.tostring(tree, pretty_print=True).decode() 
#html = lxml.html.tostring(tree).decode() 
html = lxml.html.tostring(tree, encoding='unicode')  # not `utf-8` but `unicode` ???

print(html)

Результат без child.tail = container.tail и без ET.indent(tree):

<parent>
  <child-a></child-a>
  <child-b></child-b><child-c></child-c><child-d></child-d>
  <child-e></child-e><child-f></child-f><child-g></child-g>
</parent>

Результат с child.tail = container.tail или с ET.indent(tree):

<parent>
  <child-a></child-a>
  <child-b></child-b>
  <child-c></child-c>
  <child-d></child-d>
  <child-e></child-e>
  <child-f></child-f>
  <child-g></child-g>
</parent>

Док: addnext(), addprevious(), lxml.etree.indent(), getparen(), getchildren(), iterchildren(перевернутое значение=Истина)

Спасибо! Мой единственный вопрос здесь касается for container in tree.findall('container'): всегда ли это будет корректно работать с несколькими экземплярами <container>, учитывая, что мы перебираем изменяющийся объект?

Mikhail Ramendik 07.07.2024 00:24

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

furas 07.07.2024 00:30

Спасибо! И я думаю, что нашел надежное решение этой проблемы. containers = tree.findall("container"), а затем перебираем контейнеры, на данный момент это обычный список в памяти. Или здесь ловушка?

Mikhail Ramendik 07.07.2024 00:40

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

furas 07.07.2024 12:50

Я думаю, что присвоение переменной работает так же, как и ее непосредственное использование в цикле for, потому что findall() всегда дает список с данными, а не итератор (например, iterfind()), который должен был бы использовать list(...) для получения всех данных одновременно.

furas 07.07.2024 12:55

В дополнение к ответу @Furas, вот шаги, которые также работают со встроенным xml.etree.ElementTree - нет необходимости в lxml или html. Имейте в виду, что getchildren() из lxml устарел начиная с Python 3.9. Для глубоко вложенного дерева необходимо определение функции.

from lxml import etree

xml_ = """\
<parent>
  <child-a/>
  <container>
     <child-b/>
     <child-c/>
  </container>
  <child-d/>
  <child-e/>
</parent>"""

root = etree.fromstring(xml_)

# collect elements in container
container = root.findall(".//container/*")
# print([x.tag for x in container])

# position of container tag to insert
le = root.findall("./")
i = [le.index(i) for i in le if i.tag == "container"]
# print(i)

# Remove child from parent if the position is unknown
# root.find('.//container/..').remove(root.find('.//container'))
root.remove(root.find('.//container'))

# insert elements from container
for n in range(len(container)):
    root.insert(int(*i)+n, container[n])
            
etree.indent(root, space = '  ')
etree.dump(root)

Выход:

<parent>
  <child-a/>
  <child-b/>
  <child-c/>
  <child-d/>
  <child-e/>
</parent>

Кстати, если вам нужен только контент, а не само дерево, есть гораздо более простое решение: pandas выровнять структуру. https://stackoverflow.com/a/78628067/12621346

В качестве более короткого РЕШЕНИЯ с XPath(), pop() и extend():

root = etree.fromstring(xml_)

childs = root.findall(".//*")
[childs.pop(childs.index(x)) for x in childs if x.tag == "container"]

parent = etree.Element("parent")
parent.extend(childs)
    
etree.indent(parent, space='  ')
etree.dump(parent)

Другие вопросы по теме