Итерации над текстом и элементами в lxml etree

Предположим, у меня есть следующий XML-документ:

<species> Mammals: <dog/> <cat/> Reptiles: <snake/> <turtle/> Birds: <seagull/> <owl/> </species> 

Затем я получаю элемент species следующим образом:

 import lxml.etree doc = lxml.etree.fromstring(xml) species = doc.xpath('/species')[0] 

Теперь я хотел бы напечатать список животных, сгруппированных по видам. Как я могу это сделать с помощью ElementTree API?

Если вы перечисляете все узлы, вы увидите текстовый узел с классом, за которым следуют узлы элементов с видом:

 >>> for node in species.xpath("child::node()"): ... print type(node), node ... <class 'lxml.etree._ElementStringResult'> Mammals: <type 'lxml.etree._Element'> <Element dog at 0xe0b3c0> <class 'lxml.etree._ElementStringResult'> <type 'lxml.etree._Element'> <Element cat at 0xe0b410> <class 'lxml.etree._ElementStringResult'> Reptiles: <type 'lxml.etree._Element'> <Element snake at 0xe0b460> <class 'lxml.etree._ElementStringResult'> <type 'lxml.etree._Element'> <Element turtle at 0xe0b4b0> <class 'lxml.etree._ElementStringResult'> Birds: <type 'lxml.etree._Element'> <Element seagull at 0xe0b500> <class 'lxml.etree._ElementStringResult'> <type 'lxml.etree._Element'> <Element owl at 0xe0b550> <class 'lxml.etree._ElementStringResult'> 

Поэтому вы можете построить его оттуда:

 my_species = {} current_class = None for node in species.xpath("child::node()"): if isinstance(node, lxml.etree._ElementStringResult): text = node.strip(' \n\t:') if text: current_class = my_species.setdefault(text, []) elif isinstance(node, lxml.etree._Element): if current_class is not None: current_class.append(node.tag) print my_species 

приводит к

 {'Mammals': ['dog', 'cat'], 'Reptiles': ['snake', 'turtle'], 'Birds': ['seagull', 'owl']} 

Это все хрупкие … небольшие изменения в том, как упорядочены текстовые узлы, могут испортить разбор.

Примечание к дизайну

Ответ @tdelaney в основном прав, но я хочу указать на один нюанс API дерева элементов Python. Вот цитата из учебника lxml :

Элементы могут содержать текст:

 <root>TEXT</root> 

Во многих документах XML (документах, ориентированных на данные) это единственное место, где текст можно найти. Он инкапсулируется листовым тегом в самом низу иерархии дерева.

Однако, если XML используется для помеченных текстовых документов, таких как (X) HTML, текст может также отображаться между разными элементами, прямо посередине дерева:

 <html><body>Hello<br/>World</body></html> 

Здесь тег <br/> окружен текстом. Это часто упоминается как XML-документ или смешанный контент. Элементы поддерживают это через свое свойство tail . Он содержит текст, который непосредственно следует за элементом, вплоть до следующего элемента в дереве XML.

Два text и tail свойств достаточно для представления любого текстового содержимого в документе XML. Таким образом, API ElementTree не требует каких-либо специальных текстовых узлов в дополнение к классу Element , которые, как правило, довольно часто встречаются (как вы можете узнать из классических API DOM).

Реализация

Принимая во внимание эти свойства, можно получить текст документа, не заставляя дерево выводить текстовые узлы.

 #!/usr/bin/env python3.3 import itertools from pprint import pprint try: from lxml import etree except ImportError: from xml.etree import cElementTree as etree def textAndElement(node): '''In py33+ recursive generators are easy''' yield node text = node.text.strip() if node.text else None if text: yield text for child in node: yield from textAndElement(child) tail = node.tail.strip() if node.tail else None if tail: yield tail if __name__ == '__main__': xml = ''' <species> Mammals: <dog/> <cat/> Reptiles: <snake/> <turtle/> Birds: <seagull/> <owl/> </species> ''' doc = etree.fromstring(xml) pprint(list(textAndElement(doc))) #[<Element species at 0x7f2c538727d0>, #'Mammals:', #<Element dog at 0x7f2c538728c0>, #<Element cat at 0x7f2c53872910>, #'Reptiles:', #<Element snake at 0x7f2c53872960>, #<Element turtle at 0x7f2c538729b0>, #'Birds:', #<Element seagull at 0x7f2c53872a00>, #<Element owl at 0x7f2c53872a50>] gen = textAndElement(doc) next(gen) # skip root groups = [] for _, g in itertools.groupby(gen, type): groups.append(tuple(g)) pprint(dict(zip(*[iter(groups)] * 2)) ) #{('Birds:',): (<Element seagull at 0x7fc37f38aaa0>, # <Element owl at 0x7fc37f38a820>), #('Mammals:',): (<Element dog at 0x7fc37f38a960>, # <Element cat at 0x7fc37f38a9b0>), #('Reptiles:',): (<Element snake at 0x7fc37f38aa00>, # <Element turtle at 0x7fc37f38aa50>)}