Я использую 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()
требует индекса, но я отредактирую вопрос, чтобы добавить образец.
для одного ребенка вы могли бы сделать parent.replace(container, container.getchildren()[0])
, но позже у вас все равно возникнут проблемы с вставкой других детей.
@Michail Ramendik, для вставки вам нужна позиция в списке тегов, да. Смотрите мой ответ ниже. Для этой задачи enumerate() также может оказаться полезным.
Вы можете использовать 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>, учитывая, что мы перебираем изменяющийся объект?
ну, хороший вопрос. Некоторые инструменты для очистки данных (например, Selenium) могут вызвать проблемы с этим, но мой тестовый код не создавал проблем с изменением объектов. Но, возможно, потребуется более сложный пример, для которого потребуется использовать больше памяти (и потребуется перемещать объекты в памяти). Вы бы проверили это самостоятельно, потому что у меня нет более крупного примера :)
Спасибо! И я думаю, что нашел надежное решение этой проблемы. containers = tree.findall("container")
, а затем перебираем контейнеры, на данный момент это обычный список в памяти. Или здесь ловушка?
Я думаю, что присвоение переменной работает так же, как и ее непосредственное использование в цикле for
, потому что findall()
всегда дает список с данными - это не итератор, который нужно было бы использовать list(...)
для получения всех данных одновременно.
Я думаю, что присвоение переменной работает так же, как и ее непосредственное использование в цикле for
, потому что findall()
всегда дает список с данными, а не итератор (например, iterfind()
), который должен был бы использовать list(...)
для получения всех данных одновременно.
В дополнение к ответу @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)
Для этого вам не нужен индекс, для этого есть методы
insert()
иappend()
,extend()
. Пожалуйста, поделитесь своим кодом и минимальным рабочим примером, включите пример входного XML и то, что вы хотите получить на выходе.